淺拷貝、深拷貝 Bitwise Copy和Memberwise Copy

轉載:
http://blog.csdn.net/arcsinsin/article/details/9815937
這裏寫圖片描述
C++中類的默認的拷貝構造函數是按位拷貝的,也就是直接將一個對象的成員值拷貝過來;
比如一個類A,我們不顯示提供拷貝構造函數的話:
如下:

class{
 int a;
 char arr[10];
char *p;
};
A a1;
A a2=a1;

這個時候,a2和a1的成員 int a和arr[ ]是相同的值,成員是獨立的,修改某一個的成員不會影響另外一個,但是這個時候要注意的是指針p;
雖然我們輸出的結果一樣,但是不能草率下決定。因爲這個時候a2和a1的p值是一樣的,也就是說兩個指針指向同一個地方;
所以這樣的拷貝會經常出現問題,比如兩次釋放指針的內存等。解決辦法大家都知道是自己構造一個按內容拷貝的拷貝構造函數,這裏我也就不多說了。
我想討論的問題是如下的問題:
C++編譯器在什麼情況下不會按照默認的Bitwise copy來做呢?
首先有兩種情況我不想說,1:就是含有的成員對象本身提供了拷貝構造函數(不管是默認的還是自己提供的),這個時候當拷貝這個對象的時候調用的是對象的類提供的拷貝構造函數。2:繼承的基類有拷貝構造函數,這個時候編譯器會插入基類的拷貝構造函數,而不是編譯器自己來提供。這兩種都不會按照默認的拷貝語意來做。
我想討論下面兩種:
3:含有虛函數的類;
考慮下面的代碼:

class Foo
{
public:
 int a;
 Foo *next;
 virtual void Func(void){cout<<"call Foo::func()"<<endl;}
};
class D:public Foo
{
public:
 int c;
 virtual void Func(void){cout<<"call D::func()"<<endl;}
};
int main(void)
{
  D d;
 Foo f=d;
 d.Func();
 f.Func();
 system("pause");
 return 0;
}

輸出的結果是什麼呢?運行看看:結果完全按照我們想要的要求運行:call D::func() call Foo::func()
所以這個就是說,當含有虛函數的時候,我們的類會有虛函數表,每個對象會有vptr。如果我們的基類只是簡單的將子類的vptr值拷貝過來,顯然是不符合要求的,於是這個時候編譯器會給我們自動的停用bitwise拷貝,而用reset將基類的vptr指向基類的虛函數表。所以這個時候是達到了我們的要求的。我想說的是,其他值會按照默認的按位拷貝,只是vptr不會,還有我們下一個將討論的也不會。

4:虛繼承
與第三種情況類似,但是有區別,這裏有vbtl,然而vbtl是採用的重新初始化,而不是reset;
讓我們看看例子:

class Raccoon : public virtual ZooAnimal
{
 public: Raccoon() { /* private data initialization */ }
Raccoon( int val ) { /* private data initialization */ }
// ... private: // all necessary data
 };
class RedPanda : public Raccoon
{
public: RedPanda() { /* private data initialization */ }
RedPanda( int val ) { /* private data initialization */ }
// ... private: // all necessary data
};

這裏如果是兩個一樣的類的對象間進行拷貝,簡單的按位拷貝就會解決問題,而我們的問題在於父類與子類之間的拷貝;
如:RedPanda little_red;
Raccoon little_critter = little_red;
這個時候,編譯器會在默認的拷貝構造函數中插入初始化指向虛基類的指針,而不是reset。這個與類的內部存儲結構有關,建議看看之後便一目瞭然了。


memberwise copy和bitwise copy
首先說一下深拷貝(memberwise copy)和淺拷貝(bitwise copy)的問題。一般來說,自己定義的copy ctor對於對象的拷貝會有嚴格的、符合語義的定義(人爲錯誤、破壞因素除外)。然而,無論是自定義的還是默認的ctor,編譯器都會插入對虛擬機制的處理代碼,這就保證對象切片和拷貝正確的發生——可能會出乎你的意料,但符合C++的語法語義。

虛擬機制與拷貝方式
當類中沒有虛擬機制、沒有其他類對象的成員時(只包含built-in類型、指針或者數組),默認copy ctor進行的是bitwise copy,這會導致對象切片的發生。然而,當類中有虛擬機制,或者有其他類對象成員時,默認copy ctor採用的是memberwise copy,並且會對虛擬機制進行正確的拷貝。

因爲包含虛擬機制的類在定義一個對象時,編譯器會向ctor中添加初始化vtable和vbaseclasstable(依賴於具體編譯器)的代碼,這樣可以保證vtable中的內容與類型完全匹配。也就是說MyBase和DerivedMyBase有這相似的VTABLE,但不是完全相同——例如DerivedMyBase中還可以定義自己的virtual函數,這樣它的VTABLE就會有更多表項。

而多態的實現是通過將函數調用解析爲VTABLE中的偏移量來實現。pMB->Get()可能會被編譯器解析成:
(*pMB->__vtable[Offset_of_Get])();

而當MyBase作爲虛基類時,訪問其中的數據成員可能就是:
pMB->__vBaseClassMyBase->b;

那麼,當“aMB = aDMB;”,copy ctor會執行memberwise copy,正確的初始化aMB的VTABLE,而不是僅僅將aDMB的VTABLE拷貝過來。如果是bitwise copy,aMB對象中的VTABLE將是aDMB的,aMB.Get()調用的將是DervieMyBase定義的Get(),這顯然是不符合語義和邏輯的。

總而言之
對象切片和copy ctor是一個很複雜的東西,在有虛擬機制的情況下兩者是緊密結合在一起的。因爲對象切片和拷貝構造函數的問題,不通過指針或者引用無法達到多態的目的。

還有一個問題是賦值拷貝的問題,這個機制更復雜,因此Lippman建議不要再虛基類中使用數據成員。C#和java禁止了多重繼承,並將interface作爲一個單獨的東西,消除了賦值拷貝帶來的複雜性。關於賦值拷貝的問題,有機會再討論。

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