何時需要自定義複製建構函式
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
本文涉及物件的賦值和複製(也稱為克隆)。必要時,先看譚浩強教材P291-295的相關內容或PPT,重溫一下有關概念。
一、一般情況
先看一個例子:
//例程1#include <iostream>using namespace std;class Complex{public: Complex(){real=0;imag=0;} Complex(double r,double i){real=r;imag=i;} friend Complex operator+(const Complex &c1, const Complex &c2); friend ostream& operator << (ostream& output,const Complex& c);private : double real; double imag;};//複數相加:(a+bi)+(c+di)=(a+c)+(b+d)i. Complex operator+(const Complex &c1, const Complex &c2){ Complex c; c.real=c1.real+c2.real; c.imag=c1.imag+c2.imag; return c;}//輸出的運算子過載ostream& operator << (ostream& output,const Complex& c){ output<<"(" <<c.real; if(c.imag>=0) output<<"+"; output<<c.imag<<"i)"; return output;}int main(){ Complex c1(3,4),c2(5,-10),c3; cout<<"c1="<<c1<<endl; cout<<"c2="<<c2<<endl; c3=c1+c2; cout<<"c1+c2="<<c3<<endl;; system("pause"); return 0;}
注意對相加運算過載函式的定義:
Complex operator+(const Complex &c1, const Complex &c2){ Complex c; c.real=c1.real+c2.real; c.imag=c1.imag+c2.imag; return c;}
從變數的作用域角度講,Complex c是函式的區域性變數,意味著當函式執行完後,c佔用的記憶體空間將被釋放。那麼,在main()函式中呼叫c3=c1+c2時,c3能夠得到正確的結果嗎?答案是肯定的,在operate+(c1,c2)的最後,執行return c 的時候,c 被返回,通過c3=c1+c2中的賦值(=),c 的值被(複製)賦值給了c3,在完成使命之後,c 瀟灑謝幕。
物件的賦值(=)運算子的過載是預設的,不需要專門定義對“=”的過載去完成自定義類中物件的賦值。但是,這裡有一個前提,類中不能包括動態分配的資料,否則“可能出現嚴重的後果”(譚浩強教材P293頁)。那這個嚴重的後果是什麼呢?稍後講。
與物件的賦值相類似的還有物件的複製。其實,在函式返回值為物件的時候,系統會將其中返回的物件複製出一個新的臨時物件,並傳遞給該函式的呼叫處。在複製中,需要用到複製建構函式,但這個複製建構函式一般也不需要使用者定義,系統可以自動完成。這個“一般”暗示著什麼?返回的物件中不能包括動態分配的資料。
那我們勇敢一些,去以身試法,看看如果物件中包含了動態分配的資料後究竟會發生什麼事情。領教一下後果不是目的,目的在於找到解決的辦法。因為這也是實際應用中必須面臨的問題。
二、以身試法——返回包含動態分配資料的臨時物件
也從一個例子開始。下面的例子建立一個二維陣列類Douary,完成矩陣的輸入、輸出和相加操作。與例程1 的區別是,資料成員中有指標,並其指標指向的空間在建構函式中動態分配,在解構函式中釋放。
//例程2#include <iostream>using namespace std;class Douary{public: Douary(int m, int n);//建構函式:用於建立動態陣列存放m行n列的二維陣列(矩陣)元素,並將該陣列元素初始化為0 ~Douary(); //解構函式:用於釋放動態陣列所佔用的儲存空間 friend istream &operator>>(istream &input, Douary &d);//過載運算子“>>”輸入二維陣列,其中d為Dousry類物件; friend ostream &operator<<(ostream &output, Douary &d);//過載運算子“<<”以m行n列矩陣的形式輸出二維陣列,其中d為Douary類物件。 friend Douary operator+(const Douary &d1,const Douary &d2);//兩個矩陣相加,規則:對應位置上的元素相加private: int *Array; //Array 為動態陣列指標。 int row; //row 為二維陣列的行數。 int col; //col 為二維陣列的列數。};Douary::Douary(int m, int n) //建構函式:用於建立動態陣列存放m行n列的二維陣列(矩陣)元素,並將該陣列元素初始化為{ row=m; col=n; Array = new int[m*n]; for(int i=0; i<row; ++i) for(int j=0; j<col; ++j) Array[i*col+j]=0;}Douary::~Douary() //解構函式:用於釋放動態陣列所佔用的儲存空間{ delete [] Array;}istream &operator>>(istream &input, Douary &d)//過載運算子“>>”輸入二維陣列,其中d為Dousry類物件{ for(int i=0; i<d.row; ++i) for(int j=0; j<d.col; ++j) cin>>d.Array[i*d.col+j]; return input;}ostream &operator<<(ostream &output, Douary &d)//過載運算子“<<”以m行n列矩陣的形式輸出二維陣列,其中d為Douary類物件{ for(int i=0; i<d.row; ++i) { for(int j=0; j<d.col; ++j) cout<<d.Array[i*d.col+j]<<"\t"; cout<<endl; } cout<<endl; return output;}Douary operator+(const Douary &d1,const Douary &d2)//兩個矩陣相加,規則:對應位置上的元素相加{ //在此可以先判斷d1和d2的行列是否相同,如果不相同可以報錯退出,不做運算。本參考解答忽略了這一前提 Douary d(d1.row,d1.col); for(int i=0; i<d1.row; ++i) { for(int j=0; j<d1.col; ++j) d.Array[i*d1.col+j]=d1.Array[i*d1.col+j]+d2.Array[i*d1.col+j]; } return d;}int main(){ Douary d1(2,3),d2(2,3); cout<<"輸入d1(2,3):"<<endl; cin>>d1; cout<<"輸入d2(2,3):"<<endl; cin>>d2; cout<<"d1+d2="<<endl; Douary d3=d1+d2; cout<<d3; system("pause"); return 0;}
在operate+函式中,與例程1的operate+ 一樣,聲明瞭一個臨時的區域性變數,經過一些運算後,函式返回這個區域性變數。
那結果又如何呢?看來是領教“嚴重後果”的時候了。
程式執行的結果是這樣的:
輸入d1(2,3):1 2 34 5 6輸入d2(2,3):9 8 76 5 4d1+d2=-17891602 1 00 -33686019 -1414812757請按任意鍵繼續. . .
我們看到,相加結果是錯誤的!在某些時候,類似的程式是彈出一個視窗,報告記憶體溢位。
原因何在?在例程2中,第62 行return d; 後仍然也執行預設的複製建構函式將 d 物件複製給main()函式中的一個臨時變數再賦值給了物件d3(第73行),複製完後,d 的空間被釋放。d3的Array(指標)指向的空間,此時顯然已經不能由d3繼續使用,而是可以被系統分配了。d3的Array指向一個無法控制的空間,後果真的很嚴重。
三、解決辦法
究其原因,是因為預設的建構函式過於簡單,幹不了複製“帶有需要動態分配空間的資料成員”類的“瓷器活”。實際上,當類中無動態分配空間的資料成員時,複製工作也就是對應的成員逐一複製,而有了動態分配空間的資料成員,那是各有各的樣,沒法統一。於是在這個時候,需要我們做的是,自己定義複製建構函式,關鍵是在複製的時時候,動態分配相應的空間,將完整的物件複製下來。
例程2改進之後為:(注意新增加的複製建構函式Douary(const Douary &d);的宣告(第8行)和定義(第28-36行)即可,其他位置同例程2完全一樣)
#include <iostream>using namespace std;class Douary{public: Douary(int m, int n);//建構函式:用於建立動態陣列存放m行n列的二維陣列(矩陣)元素,並將該陣列元素初始化為0 ~Douary(); //解構函式:用於釋放動態陣列所佔用的儲存空間 Douary(const Douary &d);//複製建構函式 friend istream &operator>>(istream &input, Douary &d);//過載運算子“>>”輸入二維陣列,其中d為Dousry類物件; friend ostream &operator<<(ostream &output, Douary &d);//過載運算子“<<”以m行n列矩陣的形式輸出二維陣列,其中d為Douary類物件。 friend Douary operator+(const Douary &d1,const Douary &d2);//兩個矩陣相加,規則:對應位置上的元素相加private: int *Array; //Array 為動態陣列指標。 int row; //row 為二維陣列的行數。 int col; //col 為二維陣列的列數。};Douary::Douary(int m, int n) //建構函式:用於建立動態陣列存放m行n列的二維陣列(矩陣)元素,並將該陣列元素初始化為{ row=m; col=n; Array = new int[m*n]; for(int i=0; i<row; ++i) for(int j=0; j<col; ++j) Array[i*col+j]=0;}Douary::Douary(const Douary &d){ row=d.row; col=d.col; Array = new int[row*col]; for(int i=0; i<row; ++i) for(int j=0; j<col; ++j) Array[i*col+j]=d.Array[i*col+j];}Douary::~Douary() //解構函式:用於釋放動態陣列所佔用的儲存空間{ delete [] Array;}istream &operator>>(istream &input, Douary &d)//過載運算子“>>”輸入二維陣列,其中d為Dousry類物件{ for(int i=0; i<d.row; ++i) for(int j=0; j<d.col; ++j) cin>>d.Array[i*d.col+j]; return input;}ostream &operator<<(ostream &output, Douary &d)//過載運算子“<<”以m行n列矩陣的形式輸出二維陣列,其中d為Douary類物件{ for(int i=0; i<d.row; ++i) { for(int j=0; j<d.col; ++j) cout<<d.Array[i*d.col+j]<<"\t"; cout<<endl; } cout<<endl; return output;}Douary operator+(const Douary &d1,const Douary &d2)//兩個矩陣相加,規則:對應位置上的元素相加{ //在此可以先判斷d1和d2的行列是否相同,如果不相同可以報錯退出,不做運算。本參考解答忽略了這一前提 Douary d(d1.row,d1.col); for(int i=0; i<d1.row; ++i) { for(int j=0; j<d1.col; ++j) d.Array[i*d1.col+j]=d1.Array[i*d1.col+j]+d2.Array[i*d1.col+j]; } return d;}int main(){ Douary d1(2,3),d2(2,3); cout<<"輸入d1(2,3):"<<endl; cin>>d1; cout<<"輸入d2(2,3):"<<endl; cin>>d2; cout<<"d1+d2="<<endl; Douary d3=d1+d2; cout<<d3; system("pause"); return 0;}
四、總結
當類中的資料成員需要動態分配儲存空間時,不可以依賴預設的複製建構函式。在需要時(包括這種物件要賦值、這種物件作為函式引數要傳遞、函式返回值為這種物件等情況),要考慮到自定義複製建構函式。
另外,複製建構函式一經定義,賦值運算也按新定義的複製建構函式執行。
<本文完>