轉自:http://blog.chinaunix.net/uid-28662931-id-3496326.html
一、拷貝構造函數
int main(int argc, char * argv[]) { CExample A; A.Init40); CExample B=A; //把B初始化為A的副本 ... }
B = A ; 此語句的具體過程:首先建立對象theObjtwo,並調用其構造函數,然后成員被拷貝。
語句"CExample B=A;" 用 A 初始化 B。 其完成方式是內存拷貝,復制所有成員的值。 完成后,A.pBuffer = B.pBuffer, 即它們將指向同樣的地方,指針雖然復制了,但所指向的空間並沒有復制,而是由兩個對象共用了。這樣不符合要求,對象之間不獨立了,並為空間的刪除帶來隱患。 所以需要采用必要的手段(拷貝構造函數)來避免此類情況。
拷貝構造函數的格式為 : 構造函數名(對象的引用) 提供了拷貝構造函數后的CExample類定義為:
class CExample { public : CExample(){pBuffer=NULL; nSize=0;} //構造函數 ~CExample(){delete pBuffer;} // 析構函數 CExample(const CExample&); //拷貝構造函數 void Init(int n){ pBuffer=new char [n]; nSize=n;} private : char *pBuffer; //類的對象中包含指針,指向動態分配的內存資源 int nSize; }; //拷貝構造函數的定義 CExample::CExample(const CExample& RightSides) { nSize=RightSides.nSize; //復制常規成員 pBuffer=new char [nSize]; //復制指針指向的內容 memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof (char )); }
這樣,定義新對象,並用已有對象初始化新對象時,即執行語句“CExample B=A; ” 時,CExample(const CExample& RightSides)將被調用,而已有對象用別名RightSides傳給構造函數,以用來作復制。原則上,應該為所有包含動態分配成員的類都提供拷貝構造函數。
拷貝函數被調用的情況有:
1,定義新對象,並用已有對象初始化新對象時; 即執行語句“CExample B=A; ” 時(定義對象時使用賦值初始化);
2,當對象直接作為參數傳給函數時,函數將建立對象的臨時拷貝,這個拷貝過程也將調同拷貝構造函數。
BOOL testfunc(CExample obj) { //針對obj的操作實際上是針對復制后的臨時拷貝進行的 } testfunc(theObjone); //對象直接作為參數,拷貝函數將被調用;
3,當函數中的局部對象被返回給函數調者時,也將建立此局部對象的一個臨時拷貝,拷貝構造函數也將被調用 ;
CTest func() { CTest theTest; return theTest }
二、賦值符的重載
iint main(int argc, char * argv[]) { CExample A; A.Init(40); CExample C; C.Init(60); //現在需要一個對象賦值操作,被賦值對象的原內容被清除,並用右邊對象的內容填充。 C = A; return 0; }
用到了"="號,但與上面的例子中語句“ CExample B=A; ” 不同“ CExample B=A; ”語句中的 "=" 在對象聲明語句中,表示初始化。更多時候,這種初始化也可用括號表示。 例CExample B(A);
而本例子中,"=" 表示賦值操作。將對象 A 的內容復制到對象C;,這其中涉及到對象C 原有內容的丟棄,新內容的復制。 但"="的缺省操作只是將成員變量的值相應復制。舊的值被自然丟棄。 由於對象內包含指針,將造成不良后果:指針的值被丟棄了,但指針指向的內容並未釋放。指針的值被復制了,但指針所指內容並未復制。 因此,包含動態分配成員的類除提供拷貝構造函數外,還應該考慮重載"="賦值操作符號。
類定義變為:
class CExample { public : CExample(){pBuffer=NULL; nSize=0;} //構造函數 ~CExample(){delete pBuffer;} // 析構函數 CExample(const CExample&); //拷貝構造函數 CExample& operator = (const CExample&); //賦值符重載 void Init(int n){ pBuffer=new char [n]; nSize=n;} private : char *pBuffer; //類的對象中包含指針,指向動態分配的內存資源 int nSize; }; //賦值操作符重載 CExample & CExample::operator = (const CExample& RightSides) { nSize=RightSides.nSize; //復制常規成員 char *temp=new char [nSize]; //復制指針指向的內容 memcpy(temp,RightSides.pBuffer,nSize*sizeof (char )); delete []pBuffer; //刪除原指針指向內容 (將刪除操作放在后面,避免X=X特殊情況下,內容的丟失) pBuffer=temp; //建立新指向 return *this }
三、拷貝構造函數使用賦值運算符重載的代碼
1、為什么要有拷貝構造函數,它跟構造函數有什么區別?
答:拷貝構造函數其實也是構造函數,只不過它的參數是const 的類自身的對象的引用。如果類里面沒有指針成員(該指針成員指向動態申請的空間),是沒有必要編寫拷貝構造函數的 。 我們知道,如果有一個類CObj,它已經產生了一個對象ObjA,現在又用CObj去創建ObjB,如果程序中使用語句ObjB = ObjA; 也就是說直接使用ObjA的數據給ObjB賦值。這對於一般的類,沒有任何問題,但是如果CObj里面有個char * pStr的成員,用來存放動態申請的字符串的地址,在ObjA中使用new 方法動態申請了內存並讓ObjA.pStr指向該申請的空間,在OjbB = OjbA之后,ObjA.pStr和ObjB.pStr將同時指向那片空間,這樣到導致了誰也不知道到底該由誰來負責釋放那塊空間,很有可能導致同一塊內存被釋放兩次。 使用拷貝構造函數,先申請ObjA.pStr所指向的空間大小的空間,然后將空間內容拷貝過來,這樣就不會同時指向同一塊內存,各自有各自申請的內存,各自負責釋放各自申請的內存,從而解決了剛才的問題。所以這里的“拷貝”拷貝的是動態申請的空間的內容,而不是類本身的數據。另外注意到,拷貝構造函數的參數是對象的引用,而不是對象的指針。至於為什么要用引用,不能夠用指針暫時還沒有搞明白,等搞明白了再說。
2、為什么要對=賦值操作符進行重載?
答:接上面的例子,用戶在使用語句ObjB = ObjA的時候,或許ObjB的pStr已經指向了動態申請的空間,如果直接簡單將其指向的地址覆蓋,就會導致內存泄露,所以需要對=賦值操作符進行重載,在重載函數中判斷pStr如果已經指向了動態申請的空間,就先將其釋放。
3、拷貝構造函數和=賦值操作符重載的關系。
答:從原文的例子中可以看出,=賦值操作符重載比拷貝構造函數做得要多,它除了完成拷貝構造函數所完成的拷貝動態申請的內存的數據之外,還釋放了原本自己申請的內存空間。所以原文最后給出的拷貝構造函數的實現可以使用=賦值操作符的重載來完成。
4、拷貝構造函數何時被調用?
a.對象的直接賦值也會調用拷貝構造函數 ;
b.函數參數傳遞只要是按值傳遞也調用拷貝構造函數;
c.函數返回只要是按值返回也調用拷貝構造函數。
四、拷貝構造函數 和 賦值運算符重載 為什么要使用引用?
首先先說下基類 和 派生類的關系:
例如:
class Derived:public Base { public: ..... private: ....... };
首先,派生類對象的引用初始化基類引用。多態性的動態綁定中存在兩個條件:1,必須是virtual 函數(虛函數);2, 必須是通過基類的引用或基類的指針進行成員函數的調用。
Base(const Derived &);
Base &operator=(const Derived &);
我們用一個基類引用綁定一個派生對象,然后采用基類引用對基類成員進行訪問,完成了一個基類對象基本要素的填充操作,相當於完成了基類對象的創建,也就是構造問題。這樣也就能完成由派生類對象到基類對象的構造過程。
繼承方式 | 基類特性 | 派生類特性 |
公有繼承 | public | public |
protected private |
protected 不可訪問 |
|
私有繼承 | public | private |
protected private |
private 不可訪問 |
|
保護繼承 | public | protected |
protected private |
protected 不可訪問 |