複製建構函式,賦值操作符過載之間的區別
阿新 • • 發佈:2019-02-20
-
何時呼叫拷貝(複製)建構函式:
StringBad ditto (motto); StringBad metoo = motto; StringBad also = StringBad(motto); StringBad * pStringBad = new StringBad (motto);
以上4中方式都將呼叫:StringBad(const StringBad &)
- 其中中間兩種宣告可能會使用複製建構函式直接建立metoo和also物件,也可能使用複製建構函式生成一個臨時物件,然後將臨時物件的內容賦給metoo和also,這取決於具體的實現。最後一種宣告使用motto初始化一個匿名物件,並將新物件的地址賦給pStringBad指標。
- 每當程式生成了物件副本時,編譯器都將使用複製建構函式。具體的說,當函式按值傳遞物件或函式返回物件時,都將使用複製建構函式。記住,按值傳遞意味著建立原始變數的一個副本。
- 編譯器生成臨時物件時,也將使用複製建構函式。例如,將3個Vector物件相加時,編譯器可能生成臨時的Vector物件來儲存中間的結果。
- 另外,String sailor = sports;等價於String sailor = (String)sports;因此呼叫的是拷貝建構函式
-
何時呼叫賦值運算子:
- 將已有的物件賦給另一個物件時,將呼叫過載的賦值運算子。
- 初始化物件時,並不一定會使用賦值操作符:
這裡,metoo是一個新建立的物件,被初始化為knot的值,因此使用賦值建構函式。不過,正如前面指出的,實現時也可能分兩步來處理這條語句:使用複製建構函式建立一個臨時物件,然後通過賦值操作符將臨時物件的值複製到新物件中。這就是說,初始化總是會呼叫複製建構函式,而使用=操作符時也可能呼叫賦值建構函式。StringBad metoo=knot; // use copy constructor, possibly assignment, too
與複製建構函式相似,賦值運算子的隱式實現也對成員進行逐個複製。如果成員本身就是類物件,則程式將使用為這個類定義的賦值運算子來複制該成員,但靜態資料成員不受影響。
-
賦值運算子和拷貝建構函式在實現上的區別:
- 由於目標物件可能引用了以前分配的資料,所以函式應使用delete[]來釋放這些資料。
- 函式應當避免將物件賦給自身;否則給物件重新賦值前,釋放記憶體操作可能刪除物件的內容。
- 函式返回一個指向呼叫物件的引用(方便串聯使用),而拷貝建構函式沒有返回值。
下面的程式碼說明了如何為StringBad類編寫賦值操作符:
StringBad & StringBad::operator=(const StringBad & st) { if(this == & st) return * this; delete [] str; len = st.len; str = new char [len + 1]; strcpy(str,st.str); return *this; }
程式碼首先檢查自我複製,這是通過檢視賦值操作符右邊的地址(&s)是否與接收物件(this)的地址相同來完成的,如果相同,程式將返回*this,然後結束。
如果不同,釋放str指向的記憶體,這是因為稍後將把一個新字串的地址賦給str。如果不首先使用delete操作符,則上述字串將保留在記憶體中。由於程式程式不再包含指向字串的指標,一次這些記憶體被浪費掉。
接下來的操作與複製建構函式相似,即為新字串分配足夠的記憶體空間,然後複製字串。
賦值操作並不建立新的物件,因此不需要調整靜態資料成員num_strings的值。 -
class TestChild { public: TestChild() { x=0; y=0; printf("TestChild: Constructor be called!\n"); } ~TestChild(){} TestChild(const TestChild& tc) { x=tc.x; y=tc.y; printf("TestChild: Copy Constructor called!//因為寫在了Test(拷貝)建構函式的初始化列表裡\n"); } const TestChild& operator=(const TestChild& right) { x=right.x; y=right.y; printf("TestChild: Operator = be called! //因為寫在了Test(拷貝)建構函式的函式體裡\n"); return *this; } int x,y; }; class Test { public: Test(){printf("Test: Constructor be called!\n");} explicit Test(const TestChild& tcc) { tc=tcc; } ~Test(){} Test(const Test& test):tc(test.tc) { tc=test.tc; printf("Test: Copy Constructor be called!\n"); } const Test & operator=(const Test& right) { tc=right.tc; printf("Test: Operator= be called!\n"); return *this; } TestChild tc; }; int main() { printf("1、Test中包含一個TestChild,這兩個類分別具有建構函式、\n 拷貝建構函式、過載operator=。\n\n"); printf("2、在呼叫Test的建構函式和拷貝建構函式之前,會根據跟在\n 這些函式後的初始化列表去初始化其\n TestChild變數(呼叫TestChild的拷貝建構函式去初始化)\n\n"); printf("3、一旦進入Test的建構函式體或拷貝建構函式體,則說明其成員變數TestChild已\n 經通過TestChild的建構函式或TestChild的拷貝建構函式構造出了物件\n"); printf(" 所以,在Test的建構函式體或拷貝建構函式體中,再去使用=號\n 給TestChild的時候,呼叫的就是TestChild的operator=,\n 而不是TestChild的拷貝構造函數了\n"); printf(" 這就是Test建構函式後面 “:” 初始化列表的存在意義!(\n 為了呼叫成員變數的建構函式或者拷貝建構函式)\n\n"); printf("4、最後!揪出讓人困惑的終極原因!!!!!\n Test test2=test1和Test test2(test1)這兩種是TM一模一樣的\n (都呼叫拷貝建構函式)!!!!除了這點兒之外,其他地方都是該是什麼是什麼(\"()\"呼叫建構函式,\"=\"呼叫賦值操作符)!!!\n\n"); printf("5、一個物件初始化完畢後,所有對這個物件的賦值都呼叫operator=\n\n輸出如下:"); printf("Test test1; DO:\n"); Test test1; printf("\n"); printf("Test test2=test1; DO:\n"); Test test2=test1; printf("\n"); printf("Test test3(test2); DO:\n"); Test test3(test2); printf("\n"); printf("test3=test1; DO:\n"); test3=test1; return 0; }