拷貝構造函數

特殊的構造函數

拷貝構造函數,又稱複製構造函數,是一種特殊的構造函數,它由編譯器調用來完成一些基於同一類的其他對象的構建及初始化。其形參必須是引用,但並不限制為const,一般普遍的會加上const限制。此函數經常用在函數調用時用戶定義類型的值傳遞及返回。拷貝構造函數要調用基類的拷貝構造函數和成員函數。如果可以的話,它將用常量方式調用,另外,也可以用非常量方式調用。

概念


背景

++,需調拷貝構造函(稱“複製構造函”):
)函參,值傳遞式傳函;
)函返值,值傳遞式函返;
3)一個對象用於給另外一個對象進行初始化(常稱為賦值初始化);
如果在前兩種情況不使用拷貝構造函數的時候,就會導致一個指針指向已經被刪除的內存空間。對於第三種情況來說,初始化和賦值的不同含義是拷貝構造函數調用的原因。事實上,拷貝構造函數是由普通構造函數和賦值操作符共同實現的。描述拷貝構造函數和賦值運算符的異同的參考資料有很多。

使用原則

通常的原則是:①對於凡是包含動態分配成員或包含指針成員的類都應該提供拷貝構造函數;②在提供拷貝構造函數的同時,還應該考慮重載"="賦值操作符號。

傳遞形式

拷貝構造函數必須以引用的形式傳遞(參數為引用值)。其原因如下:當一個對象以傳遞值的方式傳一個函數的時候,拷貝構造函數自動的被調用來生成函數中的對象。如果一個對象是被傳入自己的拷貝構造函數,它的拷貝構造函數將會被調用來拷貝這個對象這樣複製才可以傳入它自己的拷貝構造函數,這會導致無限循環直至棧溢出(StackOverflow)。除了當對象傳入函數的時候被隱式調用以外,拷貝構造函數在對象被函數返回的時候也同樣的被調用。

隱式拷貝構造


如果在類中沒有顯式的聲明一個拷貝構造函數,那麼,編譯器會自動生成一個來進行對象之間非static成員的位拷貝(BitwiseCopy)。這個隱含的拷貝構造函數簡單的關聯了所有的類成員。注意到這個隱式的拷貝構造函數和顯式聲明的拷貝構造函數的不同在於對成員的關聯方式。顯式聲明的拷貝構造函數關聯的只是被實例化的類成員的預設構造函數,除非另外一個構造函數在類初始化或構造列表的時候被調用。拷貝構造函數使程序更有效率,因為它不用再構造一個對象的時候改變構造函數的參數列表。設計拷貝構造函數是一個良好的風格,即使是編譯系統會自動為你生成默認拷貝構造函數。事實上,默認拷貝構造函數可以應付許多情況。引申:在這裡,與C#是不同的。C#裡面用已知的對象去初始化另一個對象,傳遞的是該已知對象的指針,而並不是隱式地拷貝構造函數。例如:這裡的輸出會是“20,20”而不是C++裡面的10,20。所以一定要跟C++區分開看。

例述


複製初始化
對於下面例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class CExample
{
 
 
 
public:
 
 
 
CExample(){pBuffer=NULL;nSize=0;}
 
 
~CExample(){delete[] pBuffer;}
 
 
 
voidInit(intn){pBuffer= new char[n];nSize=n;}
 
 
 
 
 
char* pBuffer;//類的對象中包含指針,指向動態分配的內存資源
 
 
 
int nSize;
};
//這個類的主要特點是包含指向其他資源的指針,pBuffer指向堆中動態分配的一段內存空間。
 
 
 
int main(intargc,char*argv[])
{
 
 
 
CExample theObjone;
 
 
theObjone.Init(40);
//需要另一個對象,並將它初始化為theObjone
 
 
CExample
theObjtwo=theObjone;
...
}
語句"CExample theObjtwo=theObjone;"用theObjone初始化theObjtwo。
回顧一下此語句的具體過程:首先建立對象theObjtwo,並調用其構造函數,然後成員被複制初始化。
其完成方式是內存拷貝,複製所有成員的值。完成後,theObjtwo.pBuffer==theObjone.pBuffer。
即它們將指向同樣的地方,指針雖然複製了,但所指向的空間並沒有複製,而是由兩個對象共用了。這樣不符合要求,對象之間不獨立了,並為空間的刪除帶來隱患。所以需要採用必要的手段來避免此類情況:可以在構造函數中添加操作來解決指針成員的這種問題。
所以C++語法中除了提供預設形式的構造函數外,還規範了另一種特殊的構造函數:拷貝構造函數,一種特殊的構造函數重載。上面的語句中,如果類中定義了拷貝構造函數,在對象複製初始化時,調用的將是拷貝構造函數,而不是預設構造函數。在拷貝構造函數中,可以根據傳入的變數,複製指針所指向的資源。
拷貝構造函數的格式為:類名(const 類名& 對象名);//拷貝構造函數的原型,參數是常量對象的引用。由於拷貝構造函數的目的是成員複製,不應修改原對象,所以建議使用const關鍵字。
提供了拷貝構造函數后的CExample類定義為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class CExample
{
 
 
public:
 
 
CExample(){pBuffer=NULL;nSize=0;}
 
 
~CExample(){delete[] pBuffer;}
 
 
CExample(const CExample&);//拷貝構造函數
 
 
voidInit(int n){pBuffer= new char[n];nSize=n;}
 
 
private:
 
 
char* pBuffer;//類的對象中包含指針,指向動態分配的內存資源
 
 
int nSize;
};
 
 
CExample::CExample(const CExample& RightSides)//拷貝構造函數的定義
{
 
 
nSize=RightSides.nSize;//複製常規成員
 
 
pBuffer=newchar[nSize];//分配內存
 
memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof(char));
}
這樣,定義新對象,並用已有對象初始化新對象時,CExample(const CExample& RightSides)將被調用,而已有對象用別名RightSides傳給構造函數,以用來作複製。 
對象按值傳遞
下面介紹拷貝構造函數的另一種調用:當對象直接作為參數傳給函數時,函數將建立對象的臨時拷貝,這個拷貝過程也將調用拷貝構造函數。例如:
BOOL testfunc(CExample obj);
testfunc(theObjone); //對象直接作為參數。
BOOL testfunc(CExample obj)
{
//針對obj的操作實際上是針對複製后的臨時拷貝進行的
}
還有一種情況,也是與臨時對象有關:當函數中的局部對象作為返回值被返回給函數調者時,也將建立此局部對象的一個臨時拷貝,拷貝構造函數也將被調用。
CTest func()
{
CTest theTest;
return theTest;
}
總結:當某對象是按值傳遞時(無論是作為函數參數,還是作為函數返回值),編譯器都會先建立一個此對象的臨時拷貝,而在建立該臨時拷貝時就會調用類的拷貝構造函數。 

賦值重載


重載"="
下面的代碼與上例相似
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argc,char* argv[])
{
 
CExample theObjone;
 
theObjone.Init(40);
 
CExample theObjthree;
 
 
theObjthree.init(60);
//需要一個對象賦值操作,被賦值對象的原內容被清除,並用右邊對象的內容填充。
 
 
theObjthree=theObjone;
 
return0;
}
這裡也用到了"="號,但與"複製初始化"中的例子並不同。"複製初始化"的例子中,"="在對象聲明語句中,表示初始化。更多時候,這種初始化也可用圓括弧表示。例如:CExample theObjthree(theObjone);。
而本例中,"="表示賦值操作。將對象theObjone的內容複製到對象theObjthree,這其中涉及到對象theObjthree原有內容的丟棄,新內容的複製。
但"="的預設操作只是將成員變數的值相應複製。由於對象內包含指針,將造成不良後果:指針的值被丟棄了,但指針指向的內容並未釋放。指針的值被複制了,但指針所指內容並未被複制。
因此,包含動態分配成員的類除提供拷貝構造函數外,還應該考慮重載"="賦值操作符號。 
重載注意事宜
拷貝構造函數和賦值函數的功能是相同的,為了不造成重複代碼,拷貝構造函數實現如下:
CExample::CExample(const CExample& RightSides)
{
*this=RightSides; //調用重載后的"="

格式示例


格式
拷貝構造函數的聲明:
class 類名
{
public:
類名(形參參數)//構造函數的聲明/原型
類名(類名& 對象名)//拷貝構造函數的聲明/原型
...
};
拷貝構造函數的實現:
類名::類名(類名& 對象名)//拷貝構造函數的實現/定義
{函數體} 
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Point
{
 
 
public:
 
Point(int xx,int yy){X=xx;Y=yy;}
 
Point(const Point& p);
 
 
int getX(){returnX;}
 
int getY(){returnY;}
 
private:
 
intX,Y;
};
 
 
Point::Point(const Point& p)
{
X=p.X;
Y=p.Y;
std::cout<<"拷貝構造函數調用"<
}