拷貝構造函數
拷貝構造函數,顧名思義,在拷貝的過程中進行構造類對象。首先看一個例子進行理解。
一個例子
8 #include<iostream>
10 using namespace std;
11
12 class copyconstructor
13 {
14 static int objectCount;
15 public:
16 copyconstructor():bufsize(0){objectCount++;}
41 static void printOjectNum(const string& msg = "")
42 {
43 if(msg.size()!=0)
44 {
45 cout<< msg<< ":";
46 cout<< "objectCount = "<< objectCount<< endl;
47 }
48 }
49 ~copyconstructor()
50 {
52 objectCount--;
53 cout<< "~copyconstructor():"<< objectCount<< endl;
54 }
55 private:
57 int bufsize;
58 };
59 int copyconstructor::objectCount = 0;
main.cpp:
8 #include<iostream>
9 #include"copyconstructor.h"
11 using namespace std;
12
13 copyconstructor f(copyconstructor c)
14 {
15 c.printOjectNum("after f()");
16 return c;
17 }
18
19 int main()
20 {
21 copyconstructor c1;
23 copyconstructor::printOjectNum("the objects num");
25 copyconstructor c2 = f(c1);
43 //c2 = c1;
44 copyconstructor::printOjectNum("after call f(), the objects num");
45 return 0;
46 }
在copyconstructor.h文件中定義了靜態成員變量來記錄外部申請對象實例的個數,執行main函數得到以下結果:
可以發現在退出程序的時候,對象實例並未變爲,而是-2。這是由於程序在調用f(c1)的時候調用了默認拷貝構造函數,進行了淺拷貝操作(c中的位操作),並未調用構造函數,當f函數返回值拷貝到c2後,f函數參數c1的拷貝會調用析構函數結束自己的生命週期。此時對象變爲0,這個時候main函數內還存在c1和c2沒有釋放,當main函數結束時,自動調用析構,objectCount的值自然變成了-2。
自定義拷貝構造函數
在上面提到了默認拷貝構造函數,在我們定義一個類的時候,如果沒有定義默認拷貝構造函數,c++會有一個默認拷貝構造函數,在函數參數爲值傳遞或者對象初始化使用另一個對象賦值的時候會調用拷貝構造函數拷貝一份對象的實例。如果我們的類很簡單,內部沒有指針成員變量和其他類對象等,使用默認拷貝構造函數沒有問題。但是當一個類很複雜的時候我們必須定義自己的拷貝構造函數或者禁用拷貝構造函數。因爲默認拷貝構造函數使用的是c的淺拷貝方式對對象實例進行拷貝的,也就是有指針成員變量的時候,如果調用了默認拷貝構造函數,就會出現指針共享的情況。這樣很容易出現使用野指針或者多次釋放野指針的情況。所以我們最好定義一個類的時候總是定義它的拷貝構造函數或者禁用。
爲上面的類添加一個指針成員變量char* buffer和自定義拷貝構造函數:
18 copyconstructor(const copyconstructor& cop)
19 {
20 objectCount++;
21 bufsize = cop.bufsize;
22 buffer = new char[bufsize];
23 memcpy(buffer, cop.buffer, bufsize*sizeof(char));
24 }
36 void setBuf(const int size)
37 {
38 bufsize = size;
39 buffer = new char[bufsize];
40 memset(buffer, 0, bufsize*sizeof(char));
41 }
main.cpp
19 int main()
20 {
21 copyconstructor c1;
23 copyconstructor::printOjectNum("the objects num");
25 copyconstructor c2 = f(c1); //調用了拷貝構造函數
43 copyconstructor c3 = c2; //調用了拷貝構造函數
44 copyconstructor::printOjectNum("after call f(), the objects num");
45 return 0;
46 }
輸出結果:
這個時候類實例的個數是正確的。
當一個類比較複雜的時候(類成員含有其他類的對象,其他類又包含指針等類對象),這時爲了深拷貝定義拷貝構造函數就很麻煩了,此時我們可以將拷貝構造函數聲明爲私有的,並且不去定義它。或者在聲明的拷貝構造函數後添加=delete關鍵字即可。
重載賦值函數
對上面的c3做如下的改動:
copyconstructor c3 = f(c1);
c3 = c2; //此時調用了賦值函數
如果我們沒有在類中重載賦值函數,會出現什麼結果呢。程序同樣會做淺拷貝操作,即拷貝buffer的時候會出現:拷貝buffer指針而非指針所指向的內容。此時會出現一個非常嚴重的問題。c3原指針成員變量所指向的內存沒有釋放!c3.buffer直接指向了c2.buffer所指向的內存。這裏不但出現了共享指針的危險,更要命的是出現了內存泄露。如果這段代碼在一個死循環內,且該對象佔用內存很大的話,後果顯而易見,宕機宕機!
所以定義一個類的時候,對它的重載賦值函數定義或者禁用同樣是很有必要的。
下面是對該類的重載賦值函數的定義:
26 copyconstructor& operator = (const copyconstructor& rightObj)
27 {
28 bufsize = rightObj.bufsize;
29 if(NULL != buffer) delete []buffer;
30 buffer = new char[bufsize];
31 memcpy(buffer, rightObj.buffer, bufsize*sizeof(char));
32 return *this;
33
34 }
同樣我們可以將其聲明爲一個私有成員函數或者添加delete關鍵字。
至於拷貝構造函數和賦值函數爲什麼要使用對象的引用呢,這是因爲如果這個類是一個派生類的話,爲了在構造派生類的時候可以將該派生類中的基類部分也可以完整的拷貝過來。即爲了保證一個完整的對象被拷貝。