C/C++编程:拷贝构造函数的构建操作
有三種情況,會以一個對象的內容作為另一個類對象的初值
- 最明顯的一種情況是對一個對象做明確的初始化操作,比如:
class X{ ... };
X x;
X xx = x; // 明確的以一個對象的內容作為另一個類對象的初值
- 另一種情況是當對象被當作參數交給某個函數時,比如:
extern void foo(X x);void bar(){X xx;foo(xx); // 以xx作為foo()第一個參數的初值(不明顯的初始化操作)
}
- 當函數返回一個類對象是,比如:
X foo_bar(){X xx;return xx;
}
假設類設計者明確定義了一個拷貝構造函數(有一個參數的類型是類類型[class type]),比如:
// 用戶定義的拷貝構造函數的實例
// 可以是多參數形式,其第二參數以及后繼參數有默認值
X::X(const X& x);
Y::Y(const Y& y, int = 0);
那么在大部分情況下,當一個類對象以另一個同類實體作為初值時,就會調用上面的構造函數。這可能會導致一個臨時類對象的產生或者重新代碼的蛻變
Default Memberwise Initialization
如果類沒有提供一個顯式拷貝構造函數會是怎樣?
- 當類對象以相同類的另一個對象作為初值時,其內部是以所謂的默認成員初始化(Default Memberwise Initialization)手法完成的
- 也就是把每一個內建的或者派生的數據成員的值,從某個對象拷貝一份到另一個對象身上。
- 不過它不會拷貝其中的成員類對象(member class object),而是以遞歸的方式施行memberwise initalization。比如:
class String{
public://沒有顯示拷貝構造函數
private:char *str;int len;
};
一個String對象的默認成員初始化發生在這種情況下:
String noun("book");
String verb = noun;
其完成方式就好像個別設定每一個成員一樣:
// 語義相等
verb.str = noun.str;
verb.len = noun.len;
如果一個String對象被聲明為另一個類的成員,像這樣:
class Word{
public://沒有顯式拷貝構造
private:int _occurs;String _word; // String對象是word類的一個成員
};
那么Word對象的默認成員初始化會拷貝其內建的成員_occurs,然后再于String成員對象_word上遞歸實施memberwise initalization
這個操作實際上怎么完成呢?
- 從概念上講,對于一個類X,這個操作是被一個拷貝構造函數實現的
- 一個良好的編譯器可以為大部分類對象產生逐位拷貝(bitwise copies),因為它們有
bitwise copy semantics
也就是說,”如果一個類沒有定義拷貝構造函數,編譯器就自動產生一個“這句話不對。默認構造函數和拷貝構造函數都是在必要的時候才由編譯器產生出來的
這個必要指的是當類不展現bitwise copy semantics時。
一個類對象可以從兩種方式復制得到:
- 被初始化,通過拷貝構造函數完成
- 被指定,通過拷貝賦值運算符完成
如果類沒有聲明一個拷貝構造函數,就會有隱式聲明或者隱式定義一個。
- C++標準把拷貝構造函數分為trivial和nontrivial兩種。
- 只有nontrival的實體才會被合成于程序中
- 決定一個拷貝構造函數是否為trivial的標準在于類是否展示出所謂的bitwise copy semantics`
bitwise copy semantics(位逐次拷貝)
Word noun("book");
Word verb = noun;
verb是根據noun來初始化的。
- 如果類Word顯式定義了一個拷貝構造函數,verb的初始化操作就會調用它
- 如果類Word沒有顯式定義一個拷貝構造函數,那么是否有一個編譯器合成的實體被調用呢?這就需要看該類是否展現
bitwise copy semantics。 看個例子:
// 以下聲明展現了bitwise copy semantics
class Word{
public:Word(const char *);~Word(){delete [] str};
private:int cnt;char *str;
};
這種情況下不需要合成出一個默認拷貝函數,因為上面聲明展現了default copy semantics。 而如果Word聲明如下:
// 以下聲明沒有展現了bitwise copy semantics
class Word{
public:Word(const string &);~Word();
private:int cnt;String str;
};
其中String聲明了一個顯式拷貝函數
class String{
public:String(const char *);String(const String &);~String();
};
這時,編譯器必須合成一個拷貝構造函數以調用成員類對象String的拷貝構造函數:
// 一個被合成出來的拷貝構造函數
inline Word::Word(const Word &wd){str.String::String(wd.str);cnt = wd.cnt;
}
注意:這里的拷貝構造函數中,比如整數、指針、數組等nonclass members也會被賦值
沒有bitwise copy semantics
當類不再保持bitwise copy semantics時,而且沒有聲明默認拷貝函數時,這個類會被視為nontrival。如果沒有聲明拷貝函數,編譯器為了正確處理以一個類對象作為另一個類對象的初值,必須合成一個拷貝對象。
什么時候一個類不展示出bitwise copy semantics呢?有四種情況:
- 當類含有一個成員對象而后者的類聲明有一個拷貝構造函數時(不管這個拷貝構造函數是被顯式聲明還是被編譯器合成的)
- 當類繼承自一個基類而后者存在有一個拷貝構造函數時(不管這個拷貝構造函數是被顯式聲明還是被編譯器合成的)
- 當類聲明了一個或者多個虛函數時
- 當類派生自一個繼承串鏈,其中有一個或者多個虛基類時
前兩種情況中,編譯器必須將成員對象或者基類的拷貝構造函數調用操作插入到被合成的拷貝構造函數中
后兩種請看下面討論:
重新設定虛函數表的指針
當有一個類聲明了一個或者多個虛函數時,編譯期間就會做如下擴張工作:
- 增加一個虛函數表vtbl,內含每一個有作用的虛函數的地址
- 將一個指向虛函數表的指針vptr,安插在每一個類對象內
顯然,如果編譯器對于每一個新產生的類對象的vptr不能成功正確的設定好初值,將導致可怕的后果。因此,當編譯器導入一個vptr到類之后,該類就不再展現bitwise semantics了。現在,編譯器需要合成出一個拷貝構造函數,以求將vptr適當的初始化。舉個例子:
class ZooAnimal{public:ZooAnimal();virtual ~ZooAnimal();virtual void animate();virtual void draw();private://...
};class Bear : public ZooAnimal{public:Bear();void animate(); // 雖然沒有明寫virtual,但是是一個virtualvoid draw(); // 雖然沒有明寫virtual,但是是一個virtualvirtual void dance();private:// ...
};
ZooAnimal類對象以另一個ZooAnimal類對象作為初值,或者Bear類對象以另一個Bear類對象作為初值,都可以直接靠"bitwise copy semantics"完成,舉個例子:
Bear yogi;
Bear winnie = yopi;
yogi會被默認構造函數初始化,在這個默認構造函數中,yogi的vptr被設定指向Bear類的虛函數表(靠編譯器安插的碼完成)。因此,把yogi的vptr值拷貝給winnie的vptr是安全的。
當一個基類對象以其派生類對象內容做初始化操作時,其vptr復制操作也必須保證安全。比如:
ZooAnimal franny = yogi; //這會發生切割行為
franny的vptr不可以被設定為指向Bear類的虛函數表,但是如果yogi的vptr被直接"bitwise copy",就會導致此結果。后果是下面程序片段就會被“炸毀”
void draw(const ZooAnimal& zoey) { zoey.draw(); }
void foo(){// franny的vptr指向ZooAnimal的虛函數表而不是Bear的虛函數表ZooAnimal franny = yogi;draw(yogi); // 調用Bear::draw;draw(franny); // 調用ZooAnimal::draw;
}
通過franny調用虛函數draw,調用的是ZooAnimal實體而不是Bear實體(雖然franny是以Bear類yogi作為初值),因為franny是一個ZooAnimal對象。實際上,yogi的Bear部分已經在franny初始化時被切割掉了。如果franny被聲明為一個引用(或者指針,其值為yogi的地址),那么經由franny所調用的draw()才會是Bear的函數實體
也就是說,合成出來的ZooAnimal拷貝構造函數會明確設定對象的vptr指向ZooAnimal類的虛函數表,而不是直接從右手邊的類對象中將其vptr現值拷貝出來。
處理虛基類子對象
虛基類的存在需要特別處理。一個類對象如果以另一個對象作為初值,而后者有虛基類子對象(virtual base class subobject),那么也會使bitwise copy semantics失效
每一個編譯器對于虛擬繼承的支持承諾,都表示必須讓派生類對象中的虛基類子對象位置在執行期就準備妥當。維護位置的完整性是編譯器的責任。bitwise copy semantics肯能會破壞這個位置,所以編譯器必須在(編譯器)合成出來拷貝構造函數做出仲裁。舉個例子:
class Raccon : public virtual ZooAnimal{public:Raccon(){}Raccon(int val){}private:
};
編譯器所產生的代碼(用以調用ZooAnimal的默認構造函數、將Raccon的vptr初始化,并定位出Raccon的ZooAnimal subobject)被安插在兩個Raccon構造函數之內,成為其先頭部隊
那么所有成員初始化呢?
- 首先,一個虛基類的存在會使得
bitwise copy semantics失效 - 其次,問題并不發生于一個類對象以另一個
同類對象作為初值(這時可以bitwise copy semantics)之時,而是發生與一個類對象以其派生類對象作為初值(bitwise copy semantics失效)時。比如:
class RedRanda : public Raccon{public:RedRanda(){};RedRanda(int val){};private:
}
如果同類對象作為初值,那么bitwise copy就綽綽有余了:
// 簡單的bitwise copy就足夠了
Raccon rocky;
Raccon little_critter = rocky;
如果以子對象作為父對象的初值,編譯器必須判斷"后繼當程序員視圖存取其ZooAnimal子對象時是否能夠正確的執行":
// 簡單的bitwise copy不夠,編譯器必須能夠明確little_critter的虛基類pointer/offset初始化
RedRanda little_red;
Raccon little_critter = little_red;
在這種情況下,為了完成正確的little_critter初值設定,編譯器必須合成一個拷貝構造函數,安插一些碼以設定虛基類pointer/offset的初值,對每一個成員指向必要的初始化操作,以及執行它們的內存相關操作
再看一種情況:
// 簡單的bitwise copy可能夠用,也可能不夠用
Raccon *ptr;
Raccon lillte_critter = *ptr;
上面編譯器無法知道是否bitwise copy semantics還保持著,因為它無法知道Raccon指針是否指向一個真正的Raccon對象,還是指向一個派生類對象。
這里有一個有趣的問題:當一個初始化操作存在并保持著bitwise copy semantics的狀態時,如果編譯器能夠保證對象有正確而相等的初始化操作,是否它應該壓抑拷貝構造函數的調用,以使其所產生的程序代碼優化?
- 如果是合成的拷貝構造函數,程序副作用為〇,會優化
- 如果這個拷貝構造是由類設計者提供的呢?
總結
以上是生活随笔為你收集整理的C/C++编程:拷贝构造函数的构建操作的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: E2. Square-free divi
- 下一篇: Day08_vant实现_网易云音乐案例