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 destructordestructor
理解了函式傳參和函式返回都會進行拷貝的原理,上面的結果就很清晰了。但是我們並沒有自己定義拷貝建構函式,怎麼呼叫拷貝建構函式的呢?在沒有定義拷貝建構函式時,編譯器會自動為程式生成拷貝建構函式。自動合成的拷貝建構函式等價於下面自定義的拷貝建構函式:
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語句之後,該移動構造的臨時物件也被銷燬,因此呼叫了兩次建構函式(一次普通構造,一次移動構造,對應兩次析構,只釋放一次記憶體)。