昨天晚上在看智能指針的時候,我發現自己連一個拷貝構造函數和賦值構造函數都寫不出來,自己就嘗試寫了一個版本,結果發現錯誤百出,對於拷貝構造函數和賦值構造函數的理解僅僅停留在理論的方面,而不知其中太多的內涵。
比如我們都知道拷貝構造函數和賦值構造函數最大的不同在於:
拷貝構造是確確實實構造一個新的對象,並給新對象的私有成員賦上參數對象的私有成員的值,新構造的對象和參數對象地址是不一樣的,所以如果該類中有一個私有成員是指向堆中某一塊內存,如果僅僅對該私有成員進行淺拷貝,那么會出現多個指針指向堆中同一塊內存,這是會出現問題,如果那塊內存被釋放了,就會出現其他指針指向一塊被釋放的內存,出現未定義的值的問題,如果深拷貝,就不會出現問題,因為深拷貝,不會出現指向堆中同一塊內存的問題,因為每一次拷貝,都會開辟新的內存供對象存放其值。
下面是淺拷貝構造函數的代碼:
#include <iostream> using namespace std; class A { private: int* n; public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) { n = a.n; cout<<"copy constructor is called\n"; } ~A() { cout<<"destructor is called\n"; delete n; } void get() { cout<<"n[0]: "<<n[0]<<endl; } }; int main() { A* a = new A(); A b = *a; delete a; b.get(); return 0; }
運行結果如下:
下面是深拷貝構造函數的代碼:
#include <iostream> #include <string.h> using namespace std; class A { private: int* n; public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) { n = new int[10]; memcpy(n, a.n, 10); //通過按字節拷貝,將堆中一塊內存存儲到另一塊內存 cout<<"copy constructor is called\n"; } ~A() { cout<<"destructor is called\n"; delete n; } void get() { cout<<"n[0]: "<<n[0]<<endl; } }; int main() { A* a = new A(); A b = *a; delete a; b.get(); return 0; }
運行截圖如下:
但是賦值構造函數是將一個參數對象中私有成員賦給一個已經在內存中占據內存的對象的私有成員,賦值構造函數被賦值的對象必須已經在內存中,否則調用的將是拷貝構造函數,當然賦值構造函數也有深拷貝和淺拷貝的問題。當然賦值構造函數必須能夠處理自我賦值的問題,因為自我賦值會出現指針指向一個已經釋放的內存。還有賦值構造函數必須注意它的函數原型,參數必須是引用類型,返回值也必須是引用類型,否則在傳參和返回的時候都會再次調用一次拷貝構造函數。
#include <iostream> #include <string.h> using namespace std; class A { private: int* n; public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) //拷貝構造函數的參數一定是引用,不能不是引用,不然會出現無限遞歸 { n = new int[10]; memcpy(n, a.n, 10); //通過按字節拷貝,將堆中一塊內存存儲到另一塊內存 cout<<"copy constructor is called\n"; } A& operator=(const A& a) //記住形參和返回值一定要是引用類型,否則傳參和返回時會自動調用拷貝構造函數 { if(this == &a) //為什么需要進行自我賦值判斷呢?因為下面要進行釋放n的操作,如果是自我賦值,而沒有進行判斷的話,那么就會出現講一個釋放了的內存賦給一個指針 return *this; if(n != NULL) { delete n; n == NULL; //記住釋放完內存將指針賦為NULL } n = new int[10]; memcpy(n, a.n, 10); cout<<"assign constructor is called\n"; return *this; } ~A() { cout<<"destructor is called\n"; delete n;
n = NULL; //記住釋放完內存將指針賦為NULL } void get() { cout<<"n[0]: "<<n[0]<<endl; } }; int main() { A* a = new A(); A* b = new A(); *b = *a; delete a; b->get(); return 0; }
運行截圖如下:
如果我們在賦值構造函數的形參和返回值不用引用類型,代碼如下:
#include <iostream> #include <string.h> using namespace std; class A { private: int* n; public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) //拷貝構造函數的參數一定是引用,不能不是引用,不然會出現無限遞歸 { n = new int[10]; memcpy(n, a.n, 10); //通過按字節拷貝,將堆中一塊內存存儲到另一塊內存 cout<<"copy constructor is called\n"; } A operator=(const A a) //傳參和返回值設置錯誤 { if(this == &a) return *this; if(n != NULL) { delete n; n == NULL; } n = new int[10]; memcpy(n, a.n, 10); cout<<"assign constructor is called\n"; return *this; } ~A() { cout<<"destructor is called\n"; delete n; } void get() { cout<<"n[0]: "<<n[0]<<endl; } }; int main() { A* a = new A(); A* b = new A(); *b = *a; delete a; b->get(); while(1) {} return 0; }
運行截圖如下:
多了兩次的拷貝構造函數的調用和兩次析構函數的調用。