對於一個沒有實例化的空類,編譯器不會給它默認生成任何函數,當實例化一個空類後,編譯器會根據需要生成相應的函數。這類函數包括一下幾個:
構造函數 拷貝構造函數 析構函數 賦值運算符編譯器在需要的時候會合成一個空構造函數。本篇博文中就重點來介紹一下第二主角:拷貝構造函數。
正如Linus Torvalds說的一句話:“Talk is cheap,Show me the code”。在程序員的世界裡,講再多都不如直接給看代碼。就比如一道算法題,別人跟你講半天思路,你懂了,但真正要你碼出代碼來實現時,你可能花的時間比理解思路還要多,更別提後面的調試時間了。所以,一如本系列文章的風格,從代碼的角度來觀“對象模型”,再合適也不過呢。
拷貝構造函數,就是以一個對象作為另一個類對象初值的構造函數。在下面三種情況下會調用拷貝構造函數:
class Animal{}; //--------------------第一種情況-------------------------// //對一個對象對另一個對象進行顯示的初始化 Animal animal_one; Animal animal_two = animal_one; //--------------------第二種情況-------------------------// //一個對象作為函數參數,以值傳遞的方式傳進函數 void getName(Animal a){} getName(animal_one); //--------------------第三種情況-------------------------// //一個對象作為函數返回值,以值傳遞的方式從函數返回 Animal setName(){ Animal animal; //.... return animal; }
說了這麼多,拷貝構造函數到底該怎麼寫呢?請繼續閱讀下面的代碼。
//單參數拷貝構造函數 Animal::Animal(const Animal& _animal){ //.... } //多參數拷貝構造函數,其第二參數即後繼參數以一個默認值供應 Animal::Animal(const Animal& _animal, int =0){ //..... }
有了如上的理解之後,還是如默認構造函數那樣,接下來就來討論trivial和non-trivial構造函數以及什麼時候編譯器會產生non-trivial構造函數。
位逐次拷貝是由“Bitwise Copy Semantics”翻譯而來,就是按bit位來拷貝對象。如下面的代碼:
class Animal{ //沒有提供顯示的拷貝構造函數 int age; char* name; }; Animal animal_one; Animal animal_two = animal_one;
這種情況下會采用位逐次拷貝,只是簡簡單單按位把animal_one的內存中存的值賦給animal_two,這類拷貝也稱為淺拷貝。正如大家熟知,這類拷貝是不安全的。
<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPsnPzby+zcrHzrvW8LTOv72xtLrztcS21M/zyr7S4s28o6zP1tTaYW5pbWFsX3R3b7XEbmFtZda41evWuM/ywcthbmltYWxfb25lOjpuYW1l1rjP8rXE19a3+7Suo6zI57n7YW5pbWFsX29uZbG7zva5uaOsYW5pbWFsX3R3bzo6bmFtZb7Ns8m/1dD81rjV66OstbFhbmltYWxfdHdvzva5ubXEyrG68qOsvs274crNt8XSu7j20tG+rcrNt8W1xMTatOajrLvh1OyzybK7v8nUpNaqtcS07c7zoaM8L3A+DQo8cD7I57n7ztLDx7DRY2hhciogbmFtZbu7s8lzdHJpbmcgbmFtZaOs1NnWtNDQv72xtLm51Oy6r8r9uvOjrMbkttTP88q+0uLI58/Co7o8L3A+DQo8cD4mbmJzcDs8L3A+DQo8Y2VudGVyPg0KCTxpbWcgYWx0PQ=="深拷貝" src="/uploadfile/Collfiles/20160812/201608121004401340.png" title="\" />
因為string函數有顯式的拷貝構造函數,所以在執行拷貝構造函數的時候是為animal_two::name重新分配一塊內存,然後對其賦值,自然就不會存在兩個指針指向同一塊內存的情況了。
對於上述兩種情況,我們可以將拷貝構造函數劃分為trivial和non-trivial:
trivial:直接進行位逐次拷貝 non-trivial:不進行位逐次拷貝那麼,編譯器在什麼時候不會展現出位逐次拷貝的能力,即會合成一個non-trivial拷貝構造函數呢?下面就分四種情況來討論。
如果一個類中有帶有拷貝構造函數的類成員,或是編譯器會為其合成一個拷貝構造函數,那麼這個類就不會展現出位逐次拷貝的能力。
class Animal{ public: Animal(){} Animal(const Animal& animal){ cout<<"Animal's Copy Constructor"<<endl; class="" public:="" animal="" int="" dog="" dog2="dog1;" pre="">
如上述的代碼,執行之後會輸出:Animal’s Copy Constructor,編譯器為dog2合成的拷貝構造函數不是簡單的進行位逐次拷貝,而是調用了Animal的拷貝構造函數,重新構造一個dog2::animal。
如果一個類繼承自一個帶有拷貝構造函數的基類的話,那麼編譯器在為其合成拷貝構造函數的時候會調用基類的拷貝構造函數。簡單的以以下代碼來測試一下:
class Animal{ public: Animal(){} Animal(const Animal& animal){ cout<<"Animal's Copy Constructor"<<endl; class="" dog="" :="" public:="" int="" dog2="dog1;" pre="">
同樣的,上述代碼會輸出Animal’s Copy Constructor,顯示調用了基類的拷貝構造函數。
如果一個類帶有虛函數,想想在上一篇講默認構造函數的時候,編譯期間會執行下面兩個操作
增加一個虛函數表,內含每一個有作用的虛函數的地址 一個指向虛表的指針,安插在每一個類對象內涉及到虛函數的類在合成拷貝構造函數的時候,有點復雜,我們先看看如下測試代碼:
class Animal{ public: virtual void eat(){} }; class Dog : public Animal{ public: virtual void eat(){} }; int main(){ Dog dog1; Dog dog2 = dog1; cout<<"dog1::vptr"<<(long long *)*(long long*)&dog1<<endl; long="" pre="">
在上述測試代碼中,我提取出dog1和dog2對象中的虛表地址,觀察輸出如下:
dog1::vptr:0x400c60 dog2::vptr:0x400c60
由於虛表是在編譯的時候創建的,所以,將dog2的虛表指針指向dog1的虛表這樣是安全的,這裡使用位逐次拷貝是沒有問題的。
Tips:對於帶有虛函數的類,用同類型的對象初始化時,采用位逐次拷貝完全夠用,不會合成拷貝構造函數。
這裡可以對比,將兩個指針同時指向同一個字符常量的情況,這樣是安全的。
但是,如果執行如下代碼:
Dog dog; Animal animal = dog; cout<<"dog::vptr:"<<(long long *)*(long long*)&dog<此時,將一個父類用子類初始化,這時候輸出如下:
dog::vptr:0x400c30 animal::vptr:0x400c48可見,這時候就不能采用位逐次拷貝了,父類的拷貝構造函數需要重新設定自己的虛指針指向Animal類的虛表,而不是直接將dog::vptr直接賦給animal::vptr。
帶有虛基類的子類對象
同樣,對於帶有虛基類的子類,情況也比較復雜,我們先來看看如下繼承關系:
在上圖中,Canidae由Animal類虛擬派生出來,Dog由Canidae類派生出來,在Canidae和Dog類中都有一個虛基類的指針,指向每個類中的虛基類。因此,在執行以下操作時,位逐次拷貝也會失效,編譯器必須合成一個拷貝構造函數,來重新設定指向虛基類的指針。
Dog dog; Canidae canidae=dog; cout<<(long long *)*(long long*)&canidae<<endl; long="" pre="">
以上測試代碼輸出:
0x400c20 0x400be0
本篇博客討論了編譯器會合成一個拷貝構造函數的四種情況,現總結如下:
帶有拷貝構造函數的成員類對象 帶有拷貝構造函數的基類對象 帶有虛函數的類對象 帶有虛基類的子類對象其中,需要注意的是:對於帶有虛函數的類對象和帶有虛基類的子類對象這兩種情況中,如果是以同類型的對象作為初始對象的話,是不會合成拷貝構造函數的,僅僅使用位逐次拷貝就能完成。