C++中經常使用一個常量或變量初始化另一個變量,例如:
double x=5.0;
double y=x;
使用類創建對象時,構造函數被自動調用以完成對象的初始化,那么能否象簡單變量的初始化一樣,直接用一個對象來初始化另一個對象呢?
答案是肯定的,以point類為例:
point pt1(2,3);
point pt2=pt1;
后一個語句也可寫成:
point pt2( pt1);
上述語句用pt1初始化pt2,相當於將pt1中每個數據成員的值復制到pt2中,這是表面現象。實際上,系統調用了一個復制構造函數。如果類定義中沒有顯式定義該復制構造函數時,編譯器會隱式定義一個缺省的復制構造函數,它是一個inline、public的成員函數,其原型形式為: 類名::類名(const 類名 &) 如:
point:: point (const point &);
注意:當我們自己定義了有參構造函數時,系統不再提供默認構造函數。這是容易忽略的一點。
復制構造函數調用機制
復制構造函數的調用示例:
point pt1(3,4); //構造函數 p
oint pt2(pt1); //復制構造函數
point pt3 = pt1;//復制構造函數
#include <iostream> using namespace std; class point { private: int xPos; int yPos; public: point(int x = 0, int y = 0) { cout << "調用構造函數" << endl; xPos = x; yPos = y; } point(const point & pt)//復制構造函數的定義及實現 { cout << "調用復制構造函數" << endl; xPos = pt.xPos; yPos = pt.yPos; } void print() { cout << "xPos: " << xPos << ",yPos: " << yPos << endl; } }; #include "point.h" int main() { point pt1(3, 4); pt1.print(); point pt2 = pt1; //等價於point pt2(pt1),調用復制構造函數 pt2.print(); point pt3; pt3.print(); point pt4(pt3); //等價於point pt4=pt3,調用復制構造函數 pt4.print(); // pt2 = pt1; //調用默認的賦值運算符重載函數 // pt2.print(); return 0; } #include <iostream> using namespace std; class CPoint { private: int x; int y; public: //缺省構造函數,如果定義類時未指定任何構造函數, //系統將自動生成不帶參數的缺省構造函數 CPoint() { cout << "默認構造函數 " << this << " " << endl; x = 0; y = 0; } //帶一個參數的可用於類型轉換的構造函數 CPoint(int ix) { cout << "1參數構造函數 " << this << " " << endl; x = ix; y = 0; } //帶參數的構造函數 CPoint(int ix, int iy) { cout << "2參數構造函數 " << this << " " << endl; x = ix; y = iy; } //拷貝構造函數,如果此函數不定義,系統將生成缺省拷貝構造函數功能, //缺省拷貝構造函數的行為是:用傳入的對象參數的成員初始化正要建立的對象的相應成員 CPoint(const CPoint &cp) { cout << "拷貝構造函數 " << this << " " << endl; x = cp.x; y = cp.y; } CPoint &operator=(const CPoint &cp) { cout << "賦值重載函數 " << this << " " << endl; if (this != &cp) { x = cp.x; y = cp.y; } return (*this); } //析構函數,一個類中只能有一個析構函數,如果用戶沒有定義析構函數, //系統會自動未類生成一個缺省的析構函數 ~CPoint() { cout << "析構函數 " << this << " " << endl; } }; void fun1(CPoint pt) { } CPoint fun2() { CPoint a; return a; } CPoint fun(CPoint a) { return a; } int main(int argc, char* argv[]) { //第1類 // CPoint pt1 = CPoint(); //當有析構函數的時候,CPoint()不會生成調用構造函數生成臨時的匿名對象。 //當沒有析構函數的時候,CPoint()會生成一個臨時的匿名對象,等價於CPoint pt1;這句話只會調用無參構造函數,不會調用拷貝構造函數 // CPoint pt2 = CPoint(1); //當有析構函數的時候,CPoint(1)不會生成調用構造函數生成臨時的匿名對象。 //當沒有析構函數的時候,CPoint()會生成一個臨時的匿名對象,等價於CPoint pt(1);這句話只會調用一個參數的構造函數,不會調用拷貝構造函數 // CPoint pt3 = 1; //普通數據類型轉換為類類型,利用相應的構造函數就可以實現。等價於CPoint pt(1); //第2類 /*拷貝構造函數與賦值運算符重載函數的區別: 1. 拷貝構造函數是用已經存在的對象的各成員的當前值來創建一個相同的新對象。 在下述3種情況中,系統會自動調用所屬類的拷貝構造函數。 1.1 當說明新的類對象的同時,要給它賦值另一個已經存在對象的各成員當前值。 1.2 當對象作為函數的賦值參數而對函數進行調用要進行實參和形參的結合時。 1.3 當函數的返回值是類的對象,在函數調用結束后返回主調函數處的時候。 2. 賦值運算符重載函數要把一個已經存在對象的各成員當前值賦值給另一個已經存在的同類對象 */ CPoint pt4; //調用無參構造函數 // CPoint pt5 = pt4; //調用拷貝構造函數 屬於1.1 // fun1(pt4); //調用拷貝構造函數 屬於1.2 // fun2(); //調用拷貝構造函數 屬於1.3 // CPoint pt6 = fun2();//調用無參構造函數,拷貝構造函數,此處如果沒有寫析構函數,則還會調用一次拷貝構造函數 //因為函數返回會生成一個臨時對象,然后再將這個臨時對象賦值給pt6,所以多調用一次拷貝構造函數;如果有析構函數 //則不會生成中間的臨時變量,所以少一次拷貝構造函數的調用 //還可以通過下面函數驗證 // CPoint pt7 = fun(pt4); //如果沒有析構函數,會調用3次拷貝構造函數 return 0; }
拷貝構造函數在以下三種情況會自動調用:
當把一個已經存在的對象賦值給另一個新的對象時。
當實參和形參都是對象,進行形參和實參的結合時。
當函數的返回值是對象,函數調用完成返回時。
缺省復制構造函數帶來的問題
缺省的復制構造函數並非萬金油,在一些情況下,必須由程序員顯式定義缺省復制構造函數,先來看一段錯誤代碼示例,見備注代碼。
其中語句
computer comp2(comp1)
等價於:
comp2.brand = comp1.brand;
comp2.price = comp1.price;
后一句沒有問題,但comp2.brand = comp1.brand卻有問題:經過這樣賦值后,兩個對象的brand指針都指向了同一塊內存,當兩個對象釋放時,其析構函數都要delete[]同一內存塊,便造成了2次delete[],從而引發了錯誤。
解決方案――顯式定義復制構造函數
如果類中含有指針型的數據成員、需要使用動態內存,程序員最好顯式定義自己的復制構造函數,避免各種可能出現的內存錯誤,見代碼。
computer(const computer &cp) //自定義復制構造函數
{
//重新為brand開辟和cp.brand同等大小的動態內存
brand = new char[strlen(cp.brand) + 1];
strcpy(brand, cp.brand); //字符串復制 price = cp.price;
}
關於構造函數和復制構造函數
復制構造函數可以看成是一種特殊的構造函數,這里姑且區分為“復制構造函數”和“普通構造函數”,因此,它也支持初始化表達式。
創建對象時,只有一個構造函數會被系統自動調用,具體調用哪個取決於創建對象時的參數和調用方式。C++對編譯器何時提供缺省構造函數和缺省復制構造函數有着獨特的規定,如下表所示:
構造函數調用實例
CTest t0(); //這是函數的聲明,不是實例化類
CTest t1; //缺省構造函數
CTest t2(1); //一個參數的構造函數
CTest t3(1, 2); //兩個參數的構造函數
CTest t4 = 1; //等價於CTest t4(1); //explicit
CTest t5 = t1; //CTest(t1);
CTest t6 = CTest();//CTest(1);
CTest(1,2);
t6 = CTest(1);
t6 = 1; //首先調用單個參數的構造函數,生成臨時 //對象CTest(1), 然后調用賦值運算符函數
t6 = t1; //調用賦值運算符函數 見備注代碼。請注意輸出的地址值,觀察構造函數和析構函數的配對情況。
//為了防止CPoint pt = 2;和CPoint pt2 = pt1;這種隱性轉換,可以在相應的構造函數前增加explicit標識符 #include <iostream> using namespace std; class CPoint { protected: int x; int y; public: //缺省構造函數,如果定義類時未指定任何構造函數, //系統將自動生成不帶參數的缺省構造函數 CPoint() { cout << "默認構造函數 " << this << " "; x = 0; y = 0; } //帶一個參數的可用於類型轉換的構造函數 // explicit //加上 explicit 可防止 CPoint pt1 = 1; 這種隱性轉換 CPoint(int ix) { cout << "1參數構造函數 " << this << " "; x = ix; y = 0; } //帶參數的構造函數 CPoint(int ix, int iy) { cout << "2參數構造函數 " << this << " "; x = ix; y = iy; } //拷貝構造函數,如果此函數不定義,系統將生成缺省拷貝構造函數功能, //缺省拷貝構造函數的行為是:用傳入的對象參數的成員初始化正要建立的對象的相應成員 // explicit //加上 explicit 可防止 CPoint pt2 = pt1; 這種隱性轉換 CPoint(const CPoint &cp) { cout << "拷貝構造函數 " << this << " "; x = cp.x; y = cp.y; } CPoint &operator=(const CPoint &cp) { cout << "賦值重載函數 " << this << " "; if (this != &cp) { x = cp.x; y = cp.y; } return (*this); } //析構函數,一個類中只能有一個析構函數,如果用戶沒有定義析構函數, //系統會自動未類生成一個缺省的析構函數 ~CPoint() { cout << "析構函數 " << this << " "; } }; int main(int argc, char* argv[]) { CPoint p0(); //這是函數的聲明,不是實例化類 cout << endl << "CPoint pt1;\t\t"; CPoint pt1; //缺省構造函數 cout << endl << "CPoint pt2(1);\t\t"; CPoint pt2(1); //一個參數的構造函數 cout << endl << "CPoint pt3(1, 2);\t"; CPoint pt3(1, 2); //兩個參數的構造函數 cout << endl << "CPoint pt4 = 1;\t\t"; CPoint pt4 = 1; //等價於CPoint t4(1); //explicit cout << endl << "CPoint pt5 = t1;\t"; CPoint pt5 = pt1; //CPoint(t1); cout << endl << "CPoint pt6 = CPoint();\t"; CPoint pt6 = CPoint(); //CPoint(1); CPoint(1,2); cout << endl << "pt6 = CPoint(1);\t"; pt6 = CPoint(1); cout << endl << "pt6 = 1;\t\t"; pt6 = 1; //首先調用單個參數的構造函數,生成臨時對象CPoint(1), 然后調用賦值運算符函數 cout << endl << "pt6 = t1;\t\t"; pt6 = pt1; //調用賦值運算符函數 cout << endl << endl; return 0; }