技術(shù)交流QQ群:1027579432茫蛹,歡迎你的加入猖吴!
1.拷貝構(gòu)造函數(shù)
- 拷貝構(gòu)造函數(shù)是一種特殊的構(gòu)造函數(shù)憔恳,它在創(chuàng)建對象時瓤荔,是使用同一類中之前創(chuàng)建的對象來初始化新創(chuàng)建的對象≡孔椋拷貝構(gòu)造函數(shù)通常用于:
- a.當用類的一個對象去初始化該類的另一個對象(或引用)時系統(tǒng)自動調(diào)用拷貝構(gòu)造函數(shù)實現(xiàn)拷貝賦值
- b.若函數(shù)的形參為類對象输硝,調(diào)用函數(shù)時,實參賦值給形參程梦,系統(tǒng)自動調(diào)用拷貝構(gòu)造函數(shù)
- c.當函數(shù)的返回值是類對象時点把,系統(tǒng)自動調(diào)用拷貝構(gòu)造函數(shù)
-
如果在類中沒有定義拷貝構(gòu)造函數(shù),編譯器會自行定義一個屿附。如果類帶有指針變量郎逃,并有動態(tài)內(nèi)存分配,則它必須有一個拷貝構(gòu)造函數(shù)挺份“玻拷貝構(gòu)造函數(shù)的最常見形式如下:
類名 (const 類名 &obj){ // obj 是一個對象引用,該對象是用于初始化另一個對象的 拷貝構(gòu)造函數(shù)的主體; }
- 實例如下:
#include "iostream" using namespace std; class Line{ public: int getLength(); Line(int len); // 普通的構(gòu)造函數(shù) Line(const Line &obj); // 拷貝構(gòu)造函數(shù) ~Line(); // 析構(gòu)函數(shù) private: int *ptr; }; // 構(gòu)造函數(shù)定義 Line::Line(int len){ cout << "調(diào)用構(gòu)造函數(shù)..." << endl; ptr = new int; // 為指針分配內(nèi)存 *ptr = len; } // 拷貝構(gòu)造函數(shù)定義 Line::Line(const Line &obj){ // &obj是對象line的一個引用匀泊,用這個對象的引用來初始化另一個對象 cout << "調(diào)用拷貝構(gòu)造函數(shù)优训,并為指針ptr分配內(nèi)存" << endl; ptr = new int; *ptr = *obj.ptr; // 拷貝值 } // 析構(gòu)函數(shù)定義 Line::~Line(){ cout << "釋放內(nèi)存\n"; delete ptr; } // 普通成員函數(shù)定義 int Line::getLength(){ return *ptr; } void display(Line obj){ cout << "line大小: " << obj.getLength() << endl; } int main(){ Line line(10); display(line); return 0; }
- 下面的實例是對上面的例子進行修改,通過使用已有的同類型的對象來初始化新創(chuàng)建的對象:
#include "iostream" using namespace std; class Line{ public: int getLength(); Line(int len); // 普通的構(gòu)造函數(shù) Line(const Line &obj); // 拷貝構(gòu)造函數(shù) ~Line(); // 析構(gòu)函數(shù) private: int *ptr; }; // 構(gòu)造函數(shù)定義 Line::Line(int len){ cout << "調(diào)用構(gòu)造函數(shù)..." << endl; ptr = new int; // 為指針分配內(nèi)存 *ptr = len; } // 拷貝構(gòu)造函數(shù)定義 Line::Line(const Line &obj){ // &obj是對象line的一個引用各聘,用這個對象的引用來初始化另一個對象 cout << "調(diào)用拷貝構(gòu)造函數(shù)揣非,并為指針ptr分配內(nèi)存" << endl; ptr = new int; *ptr = *obj.ptr; // 拷貝值 } // 析構(gòu)函數(shù)定義 Line::~Line(){ cout << "釋放內(nèi)存\n"; delete ptr; } // 普通成員函數(shù)定義 int Line::getLength(){ return *ptr; } void display(Line obj){ cout << "line大小: " << obj.getLength() << endl; } int main(){ Line line(10); display(line); cout << "-------------------------------\n"; Line line2 = line; // 調(diào)用拷貝構(gòu)造函數(shù) display(line2); return 0; }
2.什么是拷貝構(gòu)造函數(shù)
- 首先對普通類型的對象來說,它們之間的賦值是簡單的伦吠,如:int a = 100;int b = a;而類的對象與普通對象是不同的妆兑,類的對象內(nèi)部結(jié)構(gòu)一般比較復雜魂拦,存在各種成員變量,下面是一個簡單的類對象拷貝的例子:
// 類的對象拷貝的簡單例子 class C{ private: int a; public: // 構(gòu)造函數(shù) C(int b): a(b){ cout << "這是構(gòu)造函數(shù)....\n"; } // 拷貝構(gòu)造函數(shù):一種特殊的構(gòu)造的函數(shù)搁嗓,函數(shù)的名稱必須與類名一樣芯勘,它必須的一個參數(shù)是本類類型的一個引用變量 C(const C &C_rename){ // 這里沒有自定義拷貝構(gòu)造函數(shù)的話,必須使用系統(tǒng)默認的拷貝構(gòu)造函數(shù) a = C_rename.a; cout << "這個是拷貝構(gòu)造函數(shù)......\n"; } // 一般的成員函數(shù) void show(){ cout << "a = " << a << endl; } }; C c1_obj(666); C c2_obj = c1_obj; // 等價于C c2_obj(c1_obj); c2_obj.show();
3.拷貝構(gòu)造函數(shù)的調(diào)用時機
3.1 對象以值傳遞的方式傳入作為函數(shù)參數(shù)
// 1.對象以值傳遞的方式傳入函數(shù)作為參數(shù)
class D{
private:
int a;
public:
// 構(gòu)造函數(shù)
D(int b): a(b){
cout << "這是D的構(gòu)造對象...\n";
}
// 拷貝構(gòu)造函數(shù)
D(const D& d_reference){
a = d_reference.a;
cout << "這是拷貝構(gòu)造函數(shù)...\n";
}
// 析構(gòu)函數(shù)
~D(){
cout << "這是析構(gòu)函數(shù),刪除a = " << a << endl;
}
// 普通成員函數(shù)
void show(){
cout << "a = " << a << endl;
}
};
// 全局函數(shù)腺逛,傳入的函數(shù)參數(shù)是D的某個對象
void d_Fun(D d){
cout << "test D\n";
}
D d_obj1(999);
d_Fun(d_obj1);
- 調(diào)用函數(shù)d_Fun()時荷愕,會產(chǎn)生以下幾個重要的步驟:
- 對象d_obj1傳入d_Fun()的形參時,會先產(chǎn)生一個臨時變量temp;
- 然后調(diào)用拷貝構(gòu)造函數(shù)會把d_obj1的值給temp,整個過程類似于D temp(d_obj1);
- 等d_Fun()執(zhí)行完后棍矛,析構(gòu)掉temp對象
3.2 對象以值傳遞的方式從函數(shù)返回
class E{
private:
int a;
public:
// 構(gòu)造函數(shù)
E(int b) : a(b){
cout << "這是E的構(gòu)造函數(shù)...\n";
}
// 拷貝構(gòu)造函數(shù)
E(const E& e_reference){
a = e_reference.a;
cout << "這是E的拷貝構(gòu)造函數(shù)...\n";
}
// 析構(gòu)函數(shù)
~E(){
cout << "這是析構(gòu)函數(shù),刪除a = " << a << endl;
}
// 普通成員函數(shù)
void show(){
cout << "a = " << a << endl;
}
};
// 全局函數(shù)
E e_fun(){
E temp(3);
return temp;
}
int main(){
e_fun();
return 0;
}
- 當調(diào)用e_fun()函數(shù)執(zhí)行到return時安疗,會產(chǎn)生幾個重要的步驟:
- 先產(chǎn)生一個臨時變量xxx
- 然后調(diào)用拷貝構(gòu)造函數(shù)把temp的值給xxx,整個過程類似于E xxx(temp);
- 在函數(shù)執(zhí)行到最后先析構(gòu)temp局部變量
- 等e_fun()執(zhí)行完后再析構(gòu)掉xxx對象
3.3 對象需要通過另一個對象來初始化
C c_obj1(100);
C c_obj2(c_obj1); // 類似于C c_obj2 = c_obj1;
4.淺拷貝與深拷貝
4.1 默認拷貝構(gòu)造函數(shù)
- 很多時候我們都不知道拷貝構(gòu)造函數(shù)的情況下,傳遞對象參數(shù)或從函數(shù)返回對象都能很好的進行够委,這是因為編譯器會給我們自動產(chǎn)生一個拷貝構(gòu)造函數(shù)荐类,這就是默認拷貝構(gòu)造函數(shù),這個構(gòu)造函數(shù)很簡單茁帽,僅僅使用老對象的數(shù)據(jù)成員的值來對新的對象的數(shù)據(jù)成員一一賦值玉罐,它具有以下的形式:
Rect::Rect(const Rect& r){ width = r.width; length = r.length; }
- 當然,上面的代碼不用我們自己寫潘拨,可以由編譯器自動生成吊输。但是如果認為這樣就可以解決對象的復制問題,那就錯了铁追!看下面的代碼:
class Rect{ public: Rect(){ count++; } ~Rect(){ count--; } static int getCount(){ // 靜態(tài)成員函數(shù) return count; } private: int width; int length; static int count; // 靜態(tài)成員變量count來計數(shù) }; Rect rect1; cout << "The count of Rect: " << Rect::getCount() << endl; Rect rect2(rect1); // 新的對象需要使用老的對象來進行初始化 cout << "The count of Rect: " << Rect::getCount() << endl;
- 上面的代碼對Rect類季蚂,加入了一個靜態(tài)成員,目的是進行計數(shù)琅束。在主函數(shù)中扭屁,首先創(chuàng)建對象rect1,輸出此時的對象個數(shù)涩禀,然后使用rect1復制出對象rect2疯搅,再輸出此時的對象個數(shù)。按照理解埋泵,此時應(yīng)該有兩個對象存在,但實際程序運行時罪治,輸出的都是1丽声,反應(yīng)出只有1個對象。此外觉义,在銷毀對象時雁社,由于會調(diào)用銷毀兩個對象,類的析構(gòu)函數(shù)會調(diào)用兩次晒骇,此時的計數(shù)器將變?yōu)樨摂?shù)霉撵。說白了磺浙,就是默認的拷貝構(gòu)造函數(shù)沒有處理靜態(tài)數(shù)據(jù)成員。出現(xiàn)這些問題最根本就在于在復制對象時徒坡,計數(shù)器沒有遞增撕氧,我們重新編寫拷貝構(gòu)造函數(shù),如下:
class Rect{ public: Rect(){ count++; } // 自定義拷貝構(gòu)造函數(shù) Rect(const Rect& r){ width = r.width; length = r.length; count++; } ~Rect(){ count--; } static int getCount(){ // 靜態(tài)成員函數(shù) return count; } private: int width; int length; static int count; // 靜態(tài)成員變量count來計數(shù) }; // 靜態(tài)成員變量初始化 int Rect::count = 0;
4.2 淺拷貝
- 所謂淺拷貝喇完,指的是在對象復制時伦泥,只對對象中的數(shù)據(jù)成員進行簡單的賦值,默認拷貝構(gòu)造函數(shù)執(zhí)行的也是淺拷貝锦溪。大多情況下“淺拷貝”已經(jīng)能很好地工作了不脯,但是一旦對象存在了動態(tài)成員,那么淺拷貝就會出問題了刻诊,讓我們考慮如下一段代碼:
// 淺拷貝 class BB{ public: BB(){ // 構(gòu)造函數(shù)防楷,p指向堆中分配的一空間 p = new int(100); } ~BB(){ // 析構(gòu)函數(shù),釋放動態(tài)分配的空間 if(p!=NULL) delete p; } private: int width; int length; int *p; // 指針成員 }; BB rect1; BB rect2 = rect1; // 復制對象
- 在上面的代碼運行結(jié)束之前则涯,會出現(xiàn)一個運行錯誤复局。原因就在于在進行對象復制時,對于動態(tài)分配的內(nèi)容沒有進行正確的操作是整。我們來分析一下:
-
在運行定義rect1對象后肖揣,由于在構(gòu)造函數(shù)中有一個動態(tài)分配的語句,因此執(zhí)行后的內(nèi)存情況大致如下:
-
-
在使用rect1復制rect2時浮入,由于執(zhí)行的是淺拷貝龙优,只是將成員的值進行賦值,這時 rect1.p = rect2.p事秀,也即這兩個指針指向了堆里的同一個空間彤断,如下圖所示:
-
- 3.當然,這不是我們所期望的結(jié)果易迹,在銷毀對象時宰衙,兩個對象的析構(gòu)函數(shù)將對同一個內(nèi)存空間釋放兩次,這就是錯誤出現(xiàn)的原因睹欲。我們需要的不是兩個p有相同的值供炼,而是兩個p指向的空間有相同的值,解決辦法就是使用“深拷貝”窘疮。
4.3 深拷貝
- 在深拷貝的情況下袋哼,對于對象中動態(tài)成員,就不能僅僅簡單地賦值了闸衫,而應(yīng)該重新動態(tài)分配空間涛贯,如上面的例子就應(yīng)該按照如下的方式進行處理:
class DD{ public: DD(){ p = new int (100); cout << "這是DD的構(gòu)造函數(shù)\n"; } // 自定義默認拷貝構(gòu)造函數(shù) DD (const DD& dd){ width = dd.width; length = dd.length; p = new int; // 為新對象重新動態(tài)分配空間 *p = *(dd.p); cout << "這是DD的拷貝構(gòu)造函數(shù)\n"; } ~DD(){ if (p!=NULL) delete p; cout << "這是DD的析構(gòu)函數(shù)\n"; } private: int width; int length; int *p; }; DD rect1; DD rect2(rect1);
-
此時,在完成對象的復制后蔚出,此時rect1的p和rect2的p各自指向一段內(nèi)存空間弟翘,但它們指向的空間具有相同的內(nèi)容虫腋,這就是所謂的“深拷貝”,內(nèi)存的一個大致情況如下:
4.4 防止默認拷貝的發(fā)生
- 通過對對象復制的分析,我們發(fā)現(xiàn)對象的復制大多在進行“值傳遞”時發(fā)生稀余,這里有一個小技巧可以防止按值傳遞即聲明一個私有拷貝構(gòu)造函數(shù)悦冀。甚至不必去定義這個拷貝構(gòu)造函數(shù),這樣因為拷貝構(gòu)造函數(shù)是私有的滚躯,如果用戶試圖按值傳遞或函數(shù)返回該類對象雏门,將得到一個編譯錯誤,從而可以避免按值傳遞或返回對象掸掏。
// 防止默認拷貝的發(fā)生 class FF{ private: int a; public: FF(int b): a(b){ cout << "這是FF的構(gòu)造函數(shù)\n"; } private: FF(const FF& ff){ a = ff.a; cout << "這是FF的拷貝構(gòu)造函數(shù)\n"; } public: ~FF(){ cout << "這是FF的析構(gòu)函數(shù),刪除a = " << a << endl; } // 普通成員函數(shù) void show(){ cout << "a = " << a << endl; } }; // 全局函數(shù) void ff_fun(FF ff_param){ cout << "test\n"; } FF test(1); // ff_fun(test); 按值傳遞將會報錯!
5.拷貝構(gòu)造函數(shù)的幾個細節(jié)知識
- 拷貝構(gòu)造函數(shù)里能調(diào)用private成員變量嗎?
- 拷貝構(gòu)造函數(shù)其時就是一個特殊的構(gòu)造函數(shù)茁影,操作的還是自己類的成員變量,所以不受private的限制丧凤。
- 以下函數(shù)哪個是拷貝構(gòu)造函數(shù),為什么?
X::X(const X&); X::X(X); X::X(X&, int a=1); X::X(X&, int a=1, int b=2);
- 解釋:對于一個類X, 如果一個構(gòu)造函數(shù)的第一個參數(shù)是下列之一:
a) X& b) const X& c) volatile X& d) const volatile X&
X::X(const X&); //是拷貝構(gòu)造函數(shù) X::X(X&, int=1); //是拷貝構(gòu)造函數(shù) X::X(X&, int a=1, int b=2); //當然也是拷貝構(gòu)造函數(shù)
- 解釋:對于一個類X, 如果一個構(gòu)造函數(shù)的第一個參數(shù)是下列之一:
- 一個類中可以存在多于一個的拷貝構(gòu)造函數(shù)嗎?
- 類中可以存在超過一個拷貝構(gòu)造函數(shù)
class X { public: X(const X&); // const 的拷貝構(gòu)造 X(X&); // 非const的拷貝構(gòu)造 };
- 注意,如果一個類中只存在一個參數(shù)為X&的拷貝構(gòu)造函數(shù),那么就不能使用const X或volatile X的對象實行拷貝初始化.
class X{ public: X(); X(X&); }; const X cx; X x = cx; // error
- 如果一個類中沒有定義拷貝構(gòu)造函數(shù),那么編譯器會自動產(chǎn)生一個默認的拷貝構(gòu)造函數(shù);這個默認的參數(shù)可能為 X::X(const X&)或 X::X(X&),由編譯器根據(jù)上下文決定選擇哪一個募闲。
- 類中可以存在超過一個拷貝構(gòu)造函數(shù)