linux c/c++ 學習總結(8)-- 在某些情況下編譯器爲什麼可以不合成拷貝構造函數

關於編譯器爲什麼需要合成拷貝構造函數以及在什麼時候合成拷貝構造函數的文章很多,但是反過來想想:爲什麼在某些情況下編譯器可以不合成拷貝構造函數?


通常需要合成的情況有:
1)如果一個類沒有拷貝構造函數,但是含有一個類類型的成員變量,該類型含有拷貝構造函數,此時編譯器會爲該類合成一個拷貝構造函數;

2)如果一個類沒有拷貝構造函數,但是該類繼承自含有拷貝構造函數的基類,此時編譯器會爲該類合成一個拷貝構造函數;

3)如果一個類沒有拷貝構造函數,但是該類聲明或繼承了虛函數,此時編譯器會爲該類合成一個拷貝構造函數;

4)如果一個類沒有拷貝構造函數,但是該類含有虛基類,此時編譯器會爲該類合成一個拷貝構造函數;


其實我們在拷貝或賦值一個對象或變量時,其本質是在內存上做了一個二進制序列的拷貝。如:

int i = 10;
int j = i;  //把i對應的內存上的二進制數據,複製一份到j對應的內存區域上

而c++中對象的本質就是一段“內存區域”,如

class A{
public:
    char c;
    int num;
};
A a;

對象a在內存上可以理解爲一個unsigned char類型的數組,這個數組的大小爲8(sizeof(class A)爲8,有內存對齊),其中數組的[0]上存放的二進制數據用來表示成員變量c,數組的[1][2][3]是內存對齊的“空隙”,數組的[4][5][6][7]共同表示成員變量num。當運行A aa = a;這樣的代碼時,本質把這個unsigned char數組複製到aa對應的內存區域上了。請看下面的代碼:

class A{
public:
    char c;
    int num;
};
int main() {

    A a;
    a.c = 'A';
    a.num = 100;  

    cout<<sizeof(class A)<<endl;    //大小爲8

    reinterpret_cast<unsigned char*>(&a)[1] = 'a' ;  //在內存對齊的“空隙”中放入一些字符
    reinterpret_cast<unsigned char*>(&a)[2] = 'b' ; 
    reinterpret_cast<unsigned char*>(&a)[3] = 'c' ;


    A b = a;    //沒有自定義拷貝構造函數,編譯器也不會合成,

    cout<<b.c<<endl;      //輸出 A
    cout<<b.num<<endl;    //輸出 100

    cout<<reinterpret_cast<unsigned char*>(&b)[1]<<endl; //輸出 a
    cout<<reinterpret_cast<unsigned char*>(&b)[2]<<endl; //輸出 b
    cout<<reinterpret_cast<unsigned char*>(&b)[3]<<endl; //輸出 c

    return 0;
}

有趣的是在對象b的內存對齊“空隙”上,竟然也能輸出在對象a的對齊“空隙”上插入的字符,這說明在運行A aa = a;時只是簡單的複製二進制數據罷了。作爲對比,請看下面的代碼:

class A{
public:

    char c;
    int num;
    A(){}
    A(const A &a){     //自己定義一個拷貝構造函數
        c = a.c;
        num = a.num;
    }
};
int main() {

    A a;
    a.c = 'A';
    a.num = 100;

    cout<<sizeof(class A)<<endl;

    reinterpret_cast<unsigned char*>(&a)[1] = 'a' ;
    reinterpret_cast<unsigned char*>(&a)[2] = 'b' ;
    reinterpret_cast<unsigned char*>(&a)[3] = 'c' ;


    A b = a;  //調用自己定義的拷貝構造函數

    cout<<b.c<<endl;
    cout<<b.num<<endl;

    cout<<reinterpret_cast<unsigned char*>(&b)[1]<<endl; //輸出亂碼
    cout<<reinterpret_cast<unsigned char*>(&b)[2]<<endl; //輸出亂碼
    cout<<reinterpret_cast<unsigned char*>(&b)[3]<<endl; //輸出亂碼

    return 0;
}

不同於之前代碼,這裏在輸出對象b中內存對齊“空隙”時亂碼了,說明對象a中內存對齊“空隙”上字符並沒有拷貝過來,原因我們自己定義了拷貝構造函數。

綜上,除非出現編譯器必須要合成拷貝構造函數的四種情況之外,在拷貝對象只需要簡單的複製下內存上的二進制數據就行了,因此編譯器可以不合成拷貝構造函數。

2018-7-6更新:關於對象在內存上的佈局,是個很複雜的問題,類中的成員的類型有很多種可能性,如虛的,非虛的,靜態的,非靜態的,引用的,非引用的,const的,非const的,枚舉的,這都會影響到對象的佈局。本人水平有限,如果沒有理解錯的話,只有滿足標準佈局類型的類才能使用reinterpret_cast轉化成數組來理解,至於什麼是標準佈局類型可以參考cppreference.com中相關條目

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章