程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++對象模型——Copy Constructor 的建構操作(第二章)

C++對象模型——Copy Constructor 的建構操作(第二章)

編輯:關於C++

2.2 Copy Constructor 的建構操作

有三種情況,會以一個object的內容作為另一個 class object的初值,最明顯的一種情況就是對一個object做顯式的初始化操作,例如:
class X { ... };
X x;
// 明確地以一個object的內容作為另一個class object的初值
X xx = x;
另兩種情況是當object被當作參數交給某個函數時,例如
extern void foo(X x);
void bar() {
    X xx;
    // 以xx作為foo()第一個參數的初值(隱式的初始化操作)
    foo(xx);
}
以及當函數傳回一個 class object時。例如:
X foo_bar() {
    X xx;
    return xx;
}
假設 class 設計者明確定義了一個copy constructor(這是一個constructor,有一個參數的類型是其 class type),例如:
// user-defined copy constructor的實例
// 可以是多參數形式,其第二個參數及後繼參數以一個默認值供應之
X::X(const X &x);
Y::Y(const Y &y, int = 0);
那麼在大部分情況下,當一個 class object以另一個同類實體作為初值時,上述的constructor會被調用。這可能會導致一個暫時性 class object的產生或程序代碼的蛻變。

Default Memberwise Initialization

如果 class 沒有提供一個explicit copy constructor會怎樣?當 class object以相同class的另一個object作為初值時其內部是以所謂的default memberwise initialization完成的,也就是把每一個內建的或派生的data member(例如一個指針或數組)的值,從某個object拷貝一份到另一個object,不過它並不會拷貝其中的member class object,而是以遞歸的方式施行memberwise initialization。例如,考慮下面這個 class 聲明:
class String {
public:
    // ... 沒有 explicit copy constructor
private:
    char *str;
    int len;
};
一個String object的default memberwise initialization發生在這種情況下:
String noun(book);
String verb = noun;
    其完成方式就像個別設定每一個members一樣:
verb.str = noun.str;
verb.len = noun.len;
如果一個String object被聲明為另一個 class 的member,如下所示:
class Word {
public:
    // ... 沒有 explicit copy constructor
private:
    int _occurs;
    String _word;    // String object成為class Word的一個member
};
那麼一個Word object的default memberwise initialization會拷貝其內建的member _occurs,然後再從String member object _word遞歸實施memberwise initialization。
這樣的操作如何實際上是怎樣完成的?ARM指出:
從概念上而言,對於一個 class X,這個操作是被一個copy constructor實現出來。
關鍵的是概念上,這個注釋緊跟著一些解釋:
一個良好的編譯器可以為大部分 class objects產生bitwise copies,因為它們有bitwise copy semantics...
也就是說,如果一個class未定義出copy constructor,編譯器就自動為它產生出一個這句話是不對的,而是應該像ARM所說:
Default constructors和copy constructors在必要的時候采油編譯器產生出來。
這個句子的必要是指 class 不展現bitwise copy semantics時。C++ Standard仍然保留了ARM的意義,但將相關討論更形式化如下:
一個 class object可以從兩種方式復制得到,一種是被初始化,另一種是被指定(assignment)。從概念上而言,這兩個操作分別是以copy constructor和copy assignment operator 完成的。
就像default constructor一樣,C++ Standard指出,如果 class 沒有聲明一個copy constructor,就會有隱式的聲明出現。C++ Standard把copy constructor區分為trivial和nontrivial兩種,只有nontrivial的實體才會被合成於程序中,決定一個copy constructor是否為trivial的標准在於 class 是否展現出所謂的bitwise copy semantics。

Bitwise Copy Semantics (位逐次拷貝)

在下面的程序片段中:
#include Word.h
Word noun(book);
void foo() {
    Word verb = noun;
}
很明顯verb是根據noun來初始化,但在尚未看到 class Word聲明之前,不可能預測這個初始化操作的程序行為,如果 class Word的設計者定義了一個copy constructor,verb的初始化操作會調用它,但如果該 class 沒有定義explicit copy constructor,那麼是否會有一個編譯器合成的實體被調用呢?這就視該 class 是否展現bitwise copy semantics而定。如下所示:
// 以下聲明展現了bit copy semantics
class Word {
public:
    Word(const char *);
    ~Word() {
        delete []str;
    }
private:
    int cnt;
    char *str;
};
這種情況下並不需要合成出一個default copy constructor,因為上述聲明展現了default copy semantics,因此verb的初始化操作就不需要一個函數調用,然而,如果 class Word是這樣聲明的:
// 以下聲明並未展現出bitwise copy semantics
class Word {
public:
    Word( const String &);
    ~Word();
private:
    int cnt;
    String str;
};
    其中String聲明了一個explicit copy constructor:
class String {
public:
    String(const char *);
    String(const String &);
    ~String();
};
在這種情況下,編譯器必須合成出一個copy constructor以便調用member class String object的copy constructor:
// 一個被合成出來的copy constructor
// C++偽代碼
inline Word::Word(const Word &wd) {
    str.String::String(wd.str);
    cnt = wd.cnt;
}
有一點值得注意:在這被合成出來的copy constructor中,如整數、指針、數組等等的nonclass members也都會被復制。

不要Bitwise copy Semantics

什麼時候一個 class 不展現出bitwise copy semantics呢?有四種情況:
1. 當 class 內含一個member object而後者的 class 聲明有一個copy constructor時(不論是被 class 設計者顯式聲明,或是被編譯器合成)
2. 當 class 繼承自一個base class 而後者存在有一個copy constructor時
3. 當 class 聲明了一個或者多個 virtual functions時
4. 當 class 派生自一個繼承串鏈,其中有一個或者多個 virtual base classe
前兩種情況中,編譯器必須將members或base class 的copy constructors調用操作插入到被合成的copy constructor中。後兩種情況有點復雜,如接下來的小節所述。

重新設定 Virtual Table的指針

編譯期間的兩個程序擴張操作(只要有一個 class 聲明了一個或多個 virtual functions就會如此)
增加一個 virtual function table(vtbl),內含每一個有作用的 virtual function的地址
將一個指向 virtual funtcion table的指針(vptr),插入到每一個 class object中
很顯然,如果編譯器對於每一個新產生的 class object的vptr不能成功而正確地設好其初值,將導致可怕的後果。因此,當編譯器導入一個vptr到 class 中時,該 class 就不再展現bitwise semantics。現在,編譯器需要合成出一個copy constructor,以求vptr適當初始化,如下所示:
首先,定義兩個classes,ZooAnimal和Bear
class ZooAnimal {
public:
    ZooAnimal();
    virtual ~ZooAnimal();
    virtual void animate();
    virtual void draw();
private:
    // ZooAnimal的animate()和draw()
    // 所需要的數據
};
class Bear : public ZooAnimal {
public:
    Bear();
    void animate();
    void draw();
    virtual void dance();
private:
    // Bear的animate()和draw()和dance()
    // 所需要的數據
};
ZooAnimal class object以另一個ZooAnimal class object作為初值,或Bear class object以另一個Bear class object作為初值,都可以直接靠bitwise copy semantics完成。例如:
Bear yogi;
Bear winnie = yogi;
yogi會被 default Bear constructor初始化,而在constructor中,yogi的vtpr被設定指向Bear class 的 virtual table。因此,把yogi的vptr值拷貝給winnie的vptr是完全的。
當一個base class object以其derived class 內容做初始化操作時,其vptr復制操作也必須保證安全,例如:
ZooAnimal franny = yogi;    // 這會發生切割(sliced)
franny的vptr不可以被設定指向Bear class 的virtual table,否則當下面程序片段中的draw()被調用而franny被傳進去時,就會炸毀(blow up)
void draw (const ZooAnimal &zoey) {
    zoey.draw();
}
void foo() {
    // franny的vptr指向ZooAnimal的virtual table
    // 而非Bear的virtual table
    ZooAniaml franny = yogi;
    draw(yogi);        //調用Bear::draw()
    draw(franny);    //調用ZooAnimal::draw()
}
通過franny調用virtual function draw(),調用的是ZooAnimal實體而非Bear實體(雖然franny是以Bear object yogi作為初始值)。因為franny是一個ZooAnimal object。事實上,yogi中的Bear部分已經在franny初始化時被切割(sliced)。如果franny被聲明為一個reference(或者如果它是一個指針,而其值為yogi的地址),那麼經由franny所調用的draw()才會是Bear的函數實體。
合成出來的ZooAnimal copy constructor會顯式設定object的vptr指向ZooAnimal class 的 virtual table,而不是直接從 class object中將其vptr的值拷貝過來。

處理Virtual Base Class Subobject

Virtual base class 的存在需要特別處理,一個 class object如果以另一個object作為初值,而後者有一個 virtual base class subobject,那麼也會使bitwise copy semantics失效。
每一個編輯器對於虛擬繼承的支持承諾,都表示必須讓derived class object中的virtual base class subobject位置在執行期就准備妥當。維護位置的完整性是編輯器的責任。Bitwise copy semantics可能會破壞這個位置,所以編輯器必須在它自己合成出來的 copy constructor中做出仲裁。例如,在下面的聲明中,ZooAnimal成為Raccon的一個virtual base class :
class Raccon : public virtual ZooAnimal {
public:
    Raccon(){ /* 設定private data初值 */ }
    Racccon(int val) { /* 設定private data初值 */ }
    // ...
private:
    // 所需要的數據
};
編譯器所產生的代碼(用以調用ZooAnimal的default constructor,將Racccon的vptr初始化,並定位出Raccon中的ZooAnimal subject)被插入在兩個Raccon constructors之間。
那麼memberwise初始化呢?一個 virtual base class 的存在會使bitwise copy semantics無效。其次,問題並不發生於一個class object以另一個同類object作為初值,而是發生於一個class object以其derived classes的某個object作為初值.例如讓Racccon object以一個RedPanda object作為初值,而RedPanda聲明如下:
class RedPanda : public Raccon {
public:
    RedPanda() { /* 設定private data初值 */ }
    RedPanda(int val) { /*設定private data初值 */ }
private:
    // ...
};
如果以一個Reccon object作為另一個Raccon object的初值,那麼bitwise copy就戳戳有余了
// 簡單的bitwise copy就足夠
Raccon rocky;
Raccon little_critter = rocky;
然而如果企圖以一個RedPanda object作為little_critter的初值,編譯器必須判斷後續當程序員企圖存取其ZooAnimal subobject時是否能夠正確地執行
// 簡單的bitwise copy還不夠
// 編譯器必須明確地將litte_critter的virtual base class pointer/offset初始化
RedPanda little_red;
Raccon little_critter = little_red;
在這種情況下,為了完成正確的little_critter初值設定,編譯器必須合成一個copy constructor,插入一些碼以設定 virtual base class pointer/offset的初值,對每一個members執行必要的memberwise初值化操作,以及執行其它的內存相關操作(3.4對於 virtual base classes有更詳細的討論)
在下面的情況中,編譯器無法知道是否bitwise copy semantics還保持著,因為它無法知道Raccon指針是否指向一個真正的Raccon object,還是指向一個derived class object:
// 簡單的bitwise copy可能夠用,可能不夠用
Raccon *ptr;
Raccon little_critter = *ptr;
當一個初始化操作存在並保持著bitwise copy semantics的狀態時,如果編譯器能夠保證object有正確而相等的初始化操作,是否它應該抑制copy constructor的調用,以使其所產生的程序代碼優化?
至少在合成的copy constructor之下,程序副作用的可能性是零,所以優化似乎是合理的。如果copy constructor是由 class 設計者所提供的呢?這是一個頗有爭議的問題。
上面介紹的四種情況下 class 不再保持bitwise copy semantics,而且 default copy constructor如果未被聲明的話,會被視為nontrivial,在這四種情況下,如果缺乏一個已聲明的copy constructor,編譯器為了正確處理一個class object作為另一個class object的初值,必須合成出一個copy constructor。下一節介紹編譯器調用ocpy constructor的策略,以及這些策略如何影響程序。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved