程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++學習筆記13:運算符重載(賦值操作符2),學習筆記操作符

C++學習筆記13:運算符重載(賦值操作符2),學習筆記操作符

編輯:C++入門知識

C++學習筆記13:運算符重載(賦值操作符2),學習筆記操作符


移動語義

完成所有權的移交,當拷貝構造和賦值構造時,目標對象的所有權必須移交給我們的新的對象,原始對象將喪失所有權,_p指針將不再指向原來的那個數組;

 

左值與右值

C原始定義

  • 左值:可以出現在賦值號的左邊或者右邊
  • 右值:只能出現在賦值號的右邊

C++的定義

  • 左值:用於標識非臨時對象或者非成員函數的表達式
  • 右值:用於標識臨時對象的表達式或與任何對象無關的值(純右值),或用於標識即將失效的對象的表達式(失效值)

左值引用與右值引用

左值引用:&

右值引用:&&

  • 深拷貝需要頻繁分配和釋放內存,效率比較低
  • 移動語義的目的:所有權移交,不需要重新構造和析構
  • 為與構造函數兼容,移動語義必須為引用,而不能是指針或者普通量
  • 普通引用傳遞左值,以允許函數內部修改目標數據對象
  • 為區分左值引用,實現移動語義時必須傳遞右值引用
  • 為保證能夠修改目標數據對象,在函數內部必須將右值引用作為左值引用對待
class A
{
public:
    A() :_n(0), _p(nullptr){}
    explicit A(int n):_n(n),_p(new int[n]){}
    A(int n, int *p) :_n(n), _p(p) {}
    A(A && that);
    A & operator=(A && that);
    virtual ~A() { if (_p) { delete[]_p, _p = nullptr; } }
public:
    int & operator[](int i);
    const int & operator[](int i)const;
private:
    int _n;
    int *_p;
};

A::A(A && that)
{
    //nullptr:C++11預定義的空指針類型nullptr_t的常對象
    //可隱式轉換為任意指針類型和bool類型,但不能轉化為整數類型,以取代NULL
    _n = that._n, _p = that._p, that._n = 0, that._p = nullptr;
    //*this = that;//此代碼不會調用下面重載的賦值操作符函數
    //具名右值引用that在函數內部被當作左值,不是右值
    //匿名右值引用才會被當作右值,理論上如此.....
    //*this = static_cast<A &&>(that);//等價於*this = std::move(that);
    //上一行代碼可以調用下面重載的移動賦值操作符,但是有可能導致程序崩潰
    //因為this指向的本對象可能剛剛分配內存,_p字段所指向的目標數據對象無定義
}

A & A::operator=(A && that)
{
    if (_p)//刪除此行代碼可能導致內存洩漏
        delete[]_p;
    //可以測試是否為同一個對象,以避免自身復制操作,但意義不大
    _n = that._n, _p = that._p, that._n = 0, that._p = nullptr;
    return *this;
}

移動語義重載

class A
{
public:
    A() :_n(0), _p(nullptr) {}
    explicit A(int n) :_n(n), _p(new int[n]) {}
    A(int n, int *p) :_n(n), _p(p) {}
    //可以同時提供拷貝語義與移動語義版本,前者使用常左值引用
    //不能修改目標數據對象的值,後者則可以修改
    A(const A & that);
    A(A && that);
    A & operator =(const A & that);//深拷貝版本
    A & operator =(A && that);//移動賦值版本
    virtual ~A() { if (_p) { delete[]_p, _p = nullptr; } }
public:
    int & operator[](int i);
    const int & operator[](int i)const;
private:
    int _n;
    int *_p;
};

int main()
{
    A a(4);
    for (int i = 0; i < 4; i++)
    {
        a[i] = i + 1;
    }
    A b(a);//調用拷貝構造函數
    b = a; //調用普通賦值版本
    //把左值引用轉換為右值引用,否則會調用左值版本
    A c(static_cast<A &&>(a));//調用移動構造版本
    c = static_cast<A &&>(a);//調用移動賦值版本
    return 0;
}

左值引用同樣可以實現移動語義

class A
{
public:
    A() :_n(0), _p(nullptr) {}
    explicit A(int n) :_n(n), _p(new int[n]) {}
    A(int n, int *p) :_n(n), _p(p) {}
    A(A & that);//重載非常量版本;移動構造語義
    A(const A & that);//重載常量版本;深拷貝構造語義
    A & operator=(A &that);//重載非常量版本;移動賦值語義
    A & operator=(const A & that);//重載常量版本;深拷貝賦值語義
    virtual ~A() { if (_p) { delete[]_p, _p = nullptr; } }
public:
    int & operator[](int i) throw(std::out_of_range);
    const int & operator[](int i) const throw(std::out_of_range);
private:
    int _n;
    int *_p;
};

A::A(A & that)
{
    _n = that._n, _p = that._p, that._n = 0, that._p = nullptr;
}

A::A(const A & that)
{
    this->_n = that._n;
    _p = new int[_n];
    for (int i = 0; i < _n; i++)
    {
        _p[i] = that._p[i];
    }
}

A & A::operator=(A & that)
{
    _n = that._n, _p = that._p, that._n = 0, that._p = nullptr;
    return *this;
}

A & A::operator=(const A & that)
{
    this->_n = that._n;
    if (_p)
    {
        delete[]_p;
    }
    _p = new int[_n];
    for (int i = 0; i < _n; i++)
    {
        _p[i] = that._p[i];
    }
    return *this;
}
//main.cpp
int main()
{
    A a1;//缺省構造
    const A a2;//缺省構造
    A a3(a1);//調用A::A(A &),移動構造
    A a4(a2);//調用A::A(const A &),深拷貝構造
    //對於非常量,必須轉型為常量才能進行深拷貝
    A a5(const_cast<const A &>(a1));//調用A::A(const A &)
    A a6, a7, a8;//缺省構造
    a6 = a1;//調用A::operator=(A &),移動賦值
    a7 = a2;//調用A::operator=(const A &),深拷貝賦值
    a8 = const_cast<const A &>(a1);//調用A::operator=(const A &)
    return 0;
}

右值引用的意義

右值引用可以使用文字作為函數實際參數

//不接受文字作為實際參數,因無法獲取文字的左值
int f(int &x) { return ++x; }
//接受文字作為實際參數,傳遞右值引用
//具名右值引用作為左值,匿名右值引用作為右值
//在函數內部理論如此,但實際上...
int f(int && x) { return ++x; }
int main()
{
    //錯誤代碼,++操作符的操作數必須為左值
    //std::cout << ++10 << std::endl;
    //可能有問題,傳遞右值引用,但部分編譯器可能將其作為左值
    std::cout << f(10) << std::endl;//11?
    return 0;
}

右值引用的意義

避免編寫過多的構造與賦值函數

  • 不管是左值引用還是右值引用,若同時提供拷貝語義與移動語義,需要2對(4個)構造和賦值函數
  • 若通過單獨提供成員值的方式構造對象,單成員至少需要4個構造函數和賦值函數,雙成員至少需要8個構造和賦值函數
  • 使用右值引用,通過函數模板可以縮減代碼編寫量

實現完美轉發

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved