1. 程式人生 > 實用技巧 >C++預設構造,拷貝構造,賦值運算子構造,移動構造

C++預設構造,拷貝構造,賦值運算子構造,移動構造

  在C++的一個類中,有幾種常見的建構函式,預設建構函式,拷貝建構函式,賦值運算建構函式以及移動建構函式。單獨講解每一個建構函式的概念都比較清晰,結合函式引用和賦值,理解起來就存在許多問題。本文重點不在於概念講解,側重於對各種函式不同特性的理解。

1. 函式引數和返回值

對於一個函式,如下定義:

int func(int a)
{
    return a;
}

  傳入引數時,實際執行的是一個賦值操作,相當於臨時變數a=實參a。函式返回時,執行的也是一個賦值操作,把形參a賦值給另一個變數返回,然後銷燬形參a(整個函式執行了兩次拷貝,在函式完成時會銷燬兩個臨時變數,一個形參a,一個返回時賦值的返回引數)。

2. 拷貝建構函式

  對於函式引數和返回值有了一定的理解,我們再來看拷貝建構函式。先看下面的類定義:

class A
{
public:
    A() {
        cout << "default constructor" << endl;  //預設建構函式
    }

    A(int num):m_num(num){ 
        cout << "constructor" << endl;   //普通建構函式
    }

    ~A() {
        cout << "destructor" << endl;
    }
};

PS:預設建構函式不含任何引數,在沒有定義其他建構函式的情況下,編譯器會自動生成預設建構函式(一旦定義了其他建構函式,不會生成預設建構函式)。

定義一個函式和main函式:

A func(A a)
{
    A a1 = a;
    return a1;
}

int main()
{
    A a1;      //呼叫預設建構函式
    A a2(3);   //普通建構函式
    func(a2);  //函式呼叫,會呼叫兩次拷貝建構函式
    return 0;
}

執行結果如下:

default constructor
constructor
destructor
destructor
destructor
destructor
destructor

理解了函式傳參和函式返回都會進行拷貝的原理,上面的結果就很清晰了。但是我們並沒有自己定義拷貝建構函式,怎麼呼叫拷貝建構函式的呢?在沒有定義拷貝建構函式時,編譯器會自動為程式生成拷貝建構函式。自動合成的拷貝建構函式等價於下面自定義的拷貝建構函式:

class A
{
public:
    A() {
        cout << "default constructor" << endl;
    }

    A(int num):m_num(num){
        cout << "constructor" << endl;
    }

    A(A& a) {
        m_num = a.m_num;
        cout << "copy constructor" << endl;  //拷貝建構函式
    }

    ~A() {
        cout << "destructor" << endl;
    }

public:
    int m_num=0;
};

自定義拷貝構造之後,再執行上面的函式和main函式,得到結果如下:


default constructor
constructor
copy constructor //形參初始化呼叫一次copy constructor
copy constructor //函式內類賦值一次copy constructor
copy constructor //返回時拷貝一次copy constructor
destructor
destructor
destructor
destructor
destructor

可以看到函式執行時確實呼叫了兩次拷貝建構函式。

PS:

1. 通常情況下,編譯器合成的拷貝建構函式沒有什麼問題,但是當類中存在指標時,就會出現深拷貝和淺拷貝的問題,此時必須自定義拷貝建構函式實現深拷貝

2. 拷貝建構函式第一個引數必須是該類的一個引用(不能是普通引數)。

3. 賦值拷貝運算子

對2中定義的類再新增賦值拷貝運算子定義:

class A
{
public:
    A() {   //預設建構函式
        cout << "default constructor" << endl;
    }

    A(int num):m_num(num){
        cout << "constructor" << endl;
    }

    A(A& a) {  //拷貝建構函式
        m_num = a.m_num;
        cout << "copy constructor" << endl;
    }

    A& operator=(const A& a)  { //拷貝賦值運算子
        this->m_num = a.m_num;
        cout << "= constructor" << endl;
        return *this;
    }

    ~A() {
        cout << "destructor" << endl;
    }

public:
    int m_num=0;
};

定義下面的函式和main函式:


A func(A a)
{
  A a1;
  a1 = a;
  return a1;
}

int main()
{
    A a1;
    A a2(3);
    func(a2);
    return 0;
}

執行之後的結果為:

default constructor  //main函式第一句A a1; 執行預設建構函式
constructor          //main函式第二句A a2(3);執行普通建構函式
copy constructor     //函式形參拷貝,執行拷貝建構函式
default constructor  //函式func第一句A a1;執行預設建構函式
= constructor        //函式func第二句a1=a;執行等號賦值運算子,注意此時由於a1已經呼叫預設建構函式初始化,所以賦值運算子不會例項化一個物件,此句不對應解構函式
copy constructor     //return a1返回時,呼叫一次拷貝建構函式
destructor
destructor
destructor
destructor
destructor

PS:

1. 拷貝賦值運算子永遠不會例項化一個物件,因此也就不對應一個解構函式,即使像下面的語句此時也是呼叫拷貝建構函式進行初始化。

A a3=a2; //a2是一個A例項

2. 那麼拷貝建構函式與拷貝賦值運算子有什麼區別呢,即什麼情況下拷貝賦值運算子的定義才有意義?在shared_ptr的實現上,可以看出兩者的一個區別:

class A
{
public:
    A():m_num(NULL),count(NULL){   //預設建構函式
    }

    A(int* p) :m_num(p) {
        *count=1;
    }

    A(A& a) {  //拷貝建構函式,之前count,m_num一定沒有指向其他值
        if (a.m_num)
        {
            m_num = a.m_num;
            count = a.count;
            *count++;
        }
    }

    A& operator=(const A& a)  { //拷貝賦值運算子
        if (a.m_num)
        {
            (*(a.count))++;
        }
        if (count && (--(*count)))  //此時此類例項指向其他物件,不為空,那麼其他物件的引用計數要減1
        {
            delete count;
            delete m_num;
        }
        m_num = a.m_num; //改變指向
        count = a.count;
    }

    ~A() {
        if (count && !(--(*count)))
        {
            delete count;
            count = nullptr;

            delete m_num;
            m_num = nullptr;
        }
    }

private:
    int* m_num;
    atomic<int>* count;
};

4. 移動構造

4.1 左值、右值、左值引用、右值引用

  左值,可以簡單理解為能用取地址運算子&取其地址的,在記憶體中可訪問的( primer C++第五版說當物件是左值時,用的是物件的身份,即在記憶體中的位置)。右值即臨時變數,即將要被銷燬的,不能獲取地址(primer C++第五版說當物件是右值時,用的是物件的值)。左值引用就是對左值的引用,右值引用就是對右值的引用(右值引用只能繫結到一個即將被銷燬的物件上)。下面看幾個例子:

int i=42; //i是左值
int &r=i; //r是左值引用
int &&ri; //錯誤,不能將右值引用繫結到左值
int &r2=i*42; //錯誤,i*42是一個臨時物件,為右值,不能將左值引用繫結到右值上
const int &r3=i*42; //正確,const引用可以繫結到右值上
int &&rr2=i*42; //正確

int getZero()
{
    int zero=0;
    return zero;
}

int a=getZero(); //a是左值,getZero是右值,之前提過函式返回值的原理

PS:const引用既可以繫結到左值,也可以繫結到右值

4.2 移動建構函式

下面的圖很好的說明了移動構造的原理。

為了說明移動構造,我們改造一下之前的類A。

class A
{
public:
    A() {   //預設建構函式
        cout << "default constructor" << endl;
    }

    A(int num) :m_ptr(new int(num)) {
        cout << "constructor" << endl;
    }

    A(A& a) {  //拷貝建構函式,此時要寫成深拷貝
        m_ptr = new int(*a.m_ptr);
        cout << "copy constructor" << endl;
    }

    //拷貝賦值運算子在此意義不大
    A& operator=(A& a) { //拷貝賦值運算子
        if (m_ptr)
            delete m_ptr; //刪除原物件
        m_ptr = a.m_ptr; //此時兩個指標指向同一物件
        cout << "= constructor" << endl;
        return *this;
    }

    //移動建構函式,傳進來的一定是右值引用,這樣保證a.ptr不會再被使用
    A(A&& a) :m_ptr(a.m_ptr)
    {
        a.m_ptr = NULL;
        cout << "move constructor" << endl;
    }

    ~A() {
        if(m_ptr)
            delete m_ptr;
        cout << "destructor" << endl;
    }

public:
    int* m_ptr; 
};

int main()
{
    A a1(3); //呼叫普通建構函式
    A a2(a1); //呼叫拷貝建構函式
    A a3(move(a1)); //move函式保證傳進去的是右值,移動構造
    cout << *a1.m_ptr << endl; //錯誤,呼叫移動構造後,a1.m_ptr的記憶體被a3接管,a1指標為空
    //這也是為什麼要求傳進移動建構函式的物件為右值,右值保證後續不會再被訪問
    return 0;
}

下面再看一個例子(注:此例子來自C++移動構造):

#include<iostream>
using namespace std;
class IntNum {
public:
    IntNum(int x = 0) : xptr(new int(x)) { //建構函式
        cout << "Calling constructor..." << endl;
    }
    IntNum(const IntNum & n) : xptr(new int(*n.xptr)) {//複製建構函式
        cout << "Calling copy constructor..." << endl;
    }
    IntNum(IntNum && n) : xptr(n.xptr) { //移動建構函式
        n.xptr = nullptr;
        cout << "Calling move constructor..." << endl;
    }
    ~IntNum() { //解構函式
        delete xptr;
        cout << "Destructing..." << endl;
    }
    int getInt() { return *xptr; }
private:
    int *xptr;
};
//返回值為IntNum類物件
IntNum getNum() {
    IntNum a;
    return a;
}
int main() {
    cout << getNum().getInt() << endl;
        return 0;
} 

該函式的例子執行結果如下:

Calling constructor...
Calling move constructor...
Destructing...
0
Destructing...

解釋:呼叫getNum()函式,首先生成區域性變數a(呼叫建構函式),在getNum return返回時,返回值是一個臨時變數(右值),因此採用移動構造返回,返回之後a銷燬,然後獲取移動構造的值打印出來,cout語句之後,該移動構造的臨時物件也被銷燬,因此呼叫了兩次建構函式(一次普通構造,一次移動構造,對應兩次析構,只釋放一次記憶體)。