C++對象靜態綁定與動態綁定

 

先看一個簡單的例子,該段測試代碼的輸出結果是:
hello
段錯誤

爲什麼呢?

 

 

 

 

 

上面這段代碼,輸出爲2,暴露了宏函數的一個弊端。所以引入inline.

 

 

1.  編譯器在初始化及指定操作之間做出了仲裁。編譯器必須確保如果某個object含有一個或一個以上的vptrs,那些vptrs的內容不會被base class object初始化或改變。

 

2. 它們每個都指向Bear Object的第一個byte。其間的差別是,pb所涵蓋的地址包含整個Bear object,而pz所涵蓋的地址只包含Bear object的 ZooAnimal subobject。 除了 ZooAnimal subobject中出現的members,你不能夠使用pz來直接處理Bear的任何members。唯一列外是通過virtual機制。

 

3. 嗯,那麼,一個指向地址1000而類型爲void*的指針,將涵蓋怎樣的地址空間呢?是的,我們不知道!這就是爲什麼一個類型爲void*的指針只能夠含有一個地址,而不能通過它操作所指之object的緣故。  所以,轉型(cast)其實是一種編譯器指令。大部分情況下它並不改變一個指針所含的真正地址,它隻影響“被指出之內存的大小和其內容”的解釋方式。

 

4.  Default Constructor 的建構操作

class AK {
public:
    AK() { cout << "AK" << endl; }
};

class AC {
public:
    AC() { cout << "AC" << endl; }
};


class BB {
public:
    BB() {
        cout << "BB" << endl;
    }
private:
    AK ak;
};

class BBB : public BB{
    AC ac;
};

int main()
{
    BBB b;
    return 0;
}

輸出結果爲:
BB
AK
AC

如果設計者提供多個constructors,但其中都沒有default constructor呢?編譯器會擴張現有的每一個constructors,將“用以調用所有必要之default constructors”的程序代碼加進去。它不會合成一個新的default consturctor,這是因爲其它“由user所提供的constructors”存在的緣故。如果同時亦存在着“帶有default constructors”的member class objects,那些default constructor也會被調用——在所有base class constructor都被調用之後。

5. 有四種情況,會導致“編譯器必須爲未聲明constructor之classes合成一個default constructor”。C++ Stardand把那些合成物稱爲implicit nontrivial default constructors。 被合成出來的constructor只能滿足編譯器(而非程序) 的需要。它之所以能夠完成任務,是藉着“調用member object或base class的 default constructor” 或是 “爲每一個 object初始化其 virtual function機制 或 virtual base class機制”而完成。 至於沒有存在那四種情況而有沒有聲明任何constructor 的classes, 我們說它們擁有的是 implicit trivial default constructors, 它們實際上並不會被合成出來。
    在合成的default constructor 中,只有 base classes subobjects 和 member class objects 會被初始化。 所有其它的 nonstatic data member, 如整數、 整數指針、 整數數組等等都不會被初始化。 這些初始化操作對程序而言或許有需要,但對編譯器則並非必要。 如果程序需要一個“把某指針設爲0” 的default constructor, 那麼提供它的人應該是程序員。
    C++新手一般有兩個常見的誤解:
    1. 任何 class 如果沒有定義 default constructor, 就會被合成出一個類。
    2. 編譯器合成出來的 default constructor 會明確設定 “class 內每一個 data member的默認值”。
    如你所見,沒有一個是真的!

 

6.  有四種情況,一個 class 不展現出 “bitwise copy semantics”。
     1) 當 class 內含一個 member object 而後者的 class 聲明有一個copy constructor時(不論是被 class設計者明確地聲明,就像前面的 String那樣;或是被編譯器合成, 像 class Word 那樣)。
     2) 當 class 繼承自一個 base class 而後者存在有一個 copy constructor 時(再次強調,不論是被明確聲明或是被合成而得)。
     3) 當 class 聲明瞭一個或多個  virtual function 時。
     4) 當 class 派生自一個繼承串鏈, 其中有一個 或多個 virtual base classes時。
  前兩種情況中,編譯器必須將 member 或 base class 的“copy constructors 調用操作” 安插到被合成的 copy constructor中。
  第三種情況,回憶編譯期間的兩個程序擴張操作(只要有一個class 聲明瞭一個或多個 virtual functions 就會如此):
     增加一個 virtual function table(vtbl), 內含每一個有作用的 virtual function 的地址。
     將一個指向 virtual function table 的指針(vptr),安插在每一個 class object內。
     很顯然,如果編譯器對於每一個新產生的 class object 的vptr不能成功而正確地設好其初值,將導致可怕的後果。 因此,當編譯器導入一個 vptr 到 class之中時, 該 class 就不再展現 bitwise semantics了。現在,編譯器需要合成出一個 copy constructor,以求將vptr 適當地初始化。

 

 

7. 處理 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中做出仲裁。

 

8.  Member Initialization List
 

class X{
public:
  int i;
  int j;
public:
  X(int val) : j(val), i(j){}
};

int main()
{
    X x(4);
    cout << x.i << endl;
    return 0;
}

運行結果:
-132453224

list中的項目次序是有class中的members聲明次序決定,不是由 initialization list 中的排列次序決定。
initialization list代碼優先於 constructor中的代碼執行。

 

9. Data語意學 (The Semantics of Data)

class A {};
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};


int main()
{
    cout << sizeof A << endl;
    cout << sizeof B << endl;
    cout << sizeof C << endl;
    cout << sizeof D << endl;
    return 0;
}

輸出結果:

1
4
4
8

class A中沒有任何成員變量,編譯器會安插進去一個char,這使得這個class 的 objects得以在內存中配置獨一無二的地址。
class B 和 class C的大小都是4,這個大小和機器有關,也和編譯器有關。事實上 B 和 C的大小受到三個因素的影響:
1. 語言本身所造成的額外負擔(overhead), 當語言支持 virtual base classes時, 就會導致一些額外負擔。在 derived class中,這個額外負擔就反映在某種形式的指針身上,它或者指向 virtual base class subobject, 或者指向一個相關表格; 表格中存放的若不是 virtual base class subobject的地址,就是其偏移量(offset)。
2. 編譯器對於特殊情況所提供的優化處理。 Virtual base class A subobject 的 1 bytes 大小也出現在 class B 和 C身上。 傳統上它被放在derived class 的固定(不變動)部分的尾端。 某些編譯器會對 empty virtual base class 提供特殊支持(我的機器上沒有)。
3. Alignment的限制, class B 和 C的大小截至目前爲 2 bytes。 在大部分機器上, 羣聚的結構體大小會受到 alignment 的限制,使它們能夠更有效率地在內存中被存取。 在我的機器上, alignment 是 4bytes,所以 class B 和 C必須填補 2 bytes。 最終得到的結果就是 4 bytes。

 

10.  Data Member 的綁定(The Binding of a Data Member)

typedef char length;
class Test {
public:
    void print() { cout << sizeof data << endl; }
private:
    typedef int length;
    length data;
};

int main()
{
    Test t;
    t.print();
    return 0;
}

輸出結果:
4

原因: 對 member functions 本身的分析,會直到整個class 的聲明都出現了纔開始。因此,在一個inline member funciton 軀體之內的一個 data member 綁定操作,會在整個class 聲明完成之後才發生。

然而, 這對於 member function 的 argument list 並不爲真。 Argument list 中的名稱還是會在它們第一次遭遇時被適當地決議完成。因此在 extern 和 nested type names 之間的非直覺綁定操作還是會發生。 例如在下面的程序片段中, length的類型在兩個 member function signatures 中都決議爲 global typedef, 也就是 int。 當後續再有length 的 nested typedef聲明出現時, C++ Standard 就把稍早的綁定標示爲非法:

typedef char length;
class Test {
public:
    void print() { cout << sizeof data << endl; }
    void setData(length l) { data = l; }
    length Data() { return data; }
private:
    typedef int length;
    length data;
};

int main()
{
    Test t;
    t.print();
    cout << sizeof t.Data() << endl;
    return 0;
}

輸出結果:
4
1

  上述這種語言狀況,仍然需要某種防禦性程序風格: 請始終把 “nested type 聲明” 放在class 的起始處。 在上述例子中, 如果length 的 nested typedef 定義於 “在class中被參考” 之前, 就可以確保非直覺綁定的正確性。

 

11. Data Member 的佈局(Data Member Layout)

Nonstatic data members 在 class object 中的排列順序將和其被聲明的順序一樣, 任何中間介入的 static data members 如 freeList 和 chunkSize 都不會被放進對象佈局之中。 static data members 存放在程序的 data segment 中, 和個別的 class objects無關。

C++ Standard要求,在同一個 access section (也就是 private、public、protected等區段) 中, members 的排列只需符合“較晚出現的 members 在 class objects 中有較高的地址” 這一條件即可。 也就是說,各個members 並不一定得連續排列。 什麼東西可能會介於被聲明的 members 之間呢? members 的邊界調整(alignment) 可能就需要填補一些 bytes。 對於 C 和 C++而言,這的確是真的, 對當前的 C++編譯器實現情況而言, 這也是真的。
編譯器還可能會合成一些內部使用的 data members, 以支持整個對象模型, vptr 就是這樣的東西, 當前所有的編譯器都把它安插在每一個“內含 virtual function 之 class” 的 object 內。 vptr 會被放在什麼位置呢?傳統上它被放在所有明確聲明的 members 的最後。 不過如今也有一些編譯器把 vptr 放在一個 class object 的最前端。 C++ Standard 秉持先前所說的 “對於佈局所持的放任態度”, 允許編譯器把那些內部產生出來的 members自由放在任何位置上, 甚至放在那些被程序員聲明出來的 members 之間。
C++ Standard 也允許編譯器將多個 access sections 之中的 data members 自由排列, 不必在乎它們出現在 class 聲明中的次序。
當前各家編譯器都是把一個以上的 access sections 連鎖在一起,依照聲明的次序,成爲一個連續區塊。 Access sections 的多寡並不會招來額外負擔。 例如在一個 section中聲明8個members,或是在8個 sections 中總共聲明8 個members,得到的object 大小是一樣的。

 

 

12.  Data Member 的存取

    Static data members, 按其字面意義,被編譯器提出於 class 之外, 一如我在1.1 節所說, 並被視爲一個 global 變量(但只在 class 生命範圍之內可見)。 每一個member 的存取許可(private或protected或public), 以及與 class的關聯, 並不會導致任何空間上或執行時間上的額外負擔——不論是在個別的class objects 或是在 static data member 本身。 每一個 static data member 只有一個實體, 存放在程序的 data segment 之中。 每次程序參閱(取用) static member, 就會被內部轉化爲對該唯一的 extern 實體的直接參考操作。

Point3D origin, *pt = &origin;
origin.x = 0.0;
pt->x = 0.0;

   “從origin 存取” 和 “從pt 存取” 有什麼重大的差異? 答案是“ 當 Point3d是一個 derived class, 而在其繼承結構中有一個 virtual base class, 並且被存取的 member(如本例的x) 是一個從該 virtual base class 繼承而來的 member 時,就會有重大的差異”。 這時候我們不能夠說 pt 必然指向哪一種 class type(因此我們也就不知道編譯時這個 member 真正的offset 位置),所以這個存取操作必須延遲至執行期,經由一個額外的間接導引,才能夠解決。 但如果使用origin, 就不會有這些問題, 其類型無疑是 Point3d class, 而即使它繼承自 virtual base class, members 的offset 位置也在編譯時期就固定了。 一個積極進取的編譯器甚至可以靜態地經由 origin 就解決掉對 x 的存取。

 

#include <iostream>
using namespace std;

class A {
public:
    A(int v):a(v) {}
    virtual void print_v() {
        cout << "class A , a= " << a << endl;
    }
    int a = 0;
};

class B : public A {
public:
    B(int a, int b) :A(a), b(b) {}
    void print_v() {
        cout << "class B , a= " << A::a << ", b=" << b<< endl;
    }
    int b = 0;
};

class C :public B {
public:
    C(int a, int b, int c):B(a, b), c(c) {}
    void print_v() {
        cout << "class C , a= " << A::a << ", b=" << b << ",c = " << c << endl;
    }
    int c = 0;
};

int main()
{
    C c1(1,1,1), c2(2,2,2);
    A *a1 = &c1, *a2 = &c2;
    B *b1 = &c1, *b2 = &c2;

    c1.print_v();
    *a1 = *a2;
    c1.print_v();

    *b1 = *b2;
    c1.print_v();

    c1 = c2;
    c1.print_v();

    return 0;
}

輸出結果:
1,1,1
2,1,1
2,2,1
2,2,2

基類指針A* a,不管指向的是基類對象,還是派生類對象,對其進行 解引用 操作,得到的結果都是 基類 對象。

 

 

——————本文引用於《深度探索C++對象模型》

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