淺談C++多型實現原理(虛繼承的奧祕)
大夥都知道,如果要實現C++的多型,那麼,基類中相應的函式必須被宣告為虛擬函式(或純虛擬函式)。舉個例子:
class Point {
public:
Point(float x = 0.0, float y = 0.0) : _x(x), _y(y) {
}
virtual float z(); //virtual function
protected:
float _x, _y;
};
我推測編譯器會這樣處理這個類:處理Point類時,它會安插this指標
、vptr指標
和增加一張虛表vtbl
。
比如,建構函式可能被編譯器改造成如下模樣(很驚訝吧!建構函式也有返回值的喲~):
Point* Point(Point* this, float x = 0.0, float y = 0.0) : _x(x), _y(y) {
this->__vptr_Point = __vtbl_Point; //pointer to virtual table
//以下為使用者自定義部分
this->_x = x;
this->_y = y;
return this;
}
從上述虛擬碼中可以看到:其一,類成員函式被編譯器在第一個位置強制安插了一個this
指標。其二,編譯器給類增加了一個虛表指標__vptr_Point
,它指向了Point類的虛表__vtbl_Point
也正是這個vptr與vtbl的動態關聯…奠定了C++多型的基礎。
目前,編譯器維護的虛表可能是這個樣子:
[0] type_info for Point
[1] Point::z()
vtbl[0]
處存放的是Point類的資訊(我下面會介紹),vtbl[1]
處就是Point::z()
函數了。那麼,可以這麼呼叫它。
//Point* ptr = new Point();
ptr->__vptr_Point[1](ptr);
為什麼是這個樣子呢?其一,__vptr_Point[1]
可以找到這個函式的入口。但是,呼叫時Point::z()
this
——這裡就是ptr
。
好了,那vtbl[0] type_info for Point
又是什麼?——它可能被解釋為Point類在記憶體中的基準(或者位置)。不同的基準,就代表vptr
關聯著不同的vtbl
,導致的最直接結果肯定是會呼叫不同的函式,這樣產生的現象就是多型。
我們考慮一下多重繼承,舉個例子(假設有以下繼承關係且有虛函數出沒):
class Base1 {
};
class Base2 {
};
class Derived : public Base1, public Base2 {
};
在記憶體中,根據C++的多繼承規則——Derived
應該是這樣的(低地址)[Base1, Base2, Derived](高地址),可以圖示一下:
[ [Base 1 //Derived begin
__vptr_base1 ]
[Base 2
__vptr_base2 ] //此處一定要指向Base2的虛表,否則Base2* ptr2 = new Derived;會出問題
[Derived] ] //Derived end
很顯然,Base1的首地址和Derived的首地址是一致的。那麼,對於Base2來說,以下這個過程中…會發生什麼?
Base2* ptr2 = new Derived;
如果盲目的將Derived type_info一股腦給Base2,是不是ptr2指向的地址就不對了?所以編譯器會有一個這樣的操作:
//將Base2* ptr2 = new Derived;拆分為以下兩句
Derived* temp = new Derived;
Base2* ptr2 = temp ? temp + sizeof(Base1) : 0;
new Derived
返回的地址會是Base1
的首地址(我再次強調,Derived和Base1首地址是一致的)。而Base2在記憶體中緊接著Base1,所以必須做個偏移,使ptr2指向Derived中的Base2首地址,並將__vptr_base2指派給它,也就是關聯好Base2的虛表vtbl。
這裡,做了個判斷是考慮到有temp == nullptr
(也就是Base2* ptr2 = nullptr;)這種情況的存在。
看到這裡應該明白了 vtbl[0] type_info for Point 它可能是virtual base class offsets ——也就是說,它是對應類的基址(或記憶體中的地址)。而編譯器在vtbl[1-n]
中存放相對於vtbl[0]
的offset
,這是C++之父讚賞的一種做法。
綜上,呼叫某個虛擬函式時,ptr->__vptr_Point[1](ptr);
== ptr->__vptr_Point[0]+1(ptr);
此時offset=1。
以上都是基礎知識,那麼複雜的虛繼承物件是如何構造起來的呢?考慮以下程式碼,Derived是如何避免多次構造Base的?
class Base {
public:
Base() {
}
};
class Mid1 : virtual public Base {
public:
Mid1() : Base() {
}
};
class Mid2 : virtual public Base {
public:
Mid2() : Base() {
}
};
class Derived : public Mid1, public Mid2 {
public:
Derived() : Mid1(), Mid2() {
}
};
有一種的做法是編譯器對建構函式進行擴充,添加了一個bool __most_derived
的開關,正是由於這個開關的新增,使得底層子類Derived可以壓制它的直接基類Mid1、Mid2對頂部虛基類Base的構造。為了突出主次,沒有新增類似於vptr設定的相關語句(編譯器擴充後的虛擬碼如下):
class Base {
public:
Base() {
}
};
class Mid1 : virtual public Base {
public:
Mid1* Mid1(Mid1* this, bool __most_derived) {
if (__most_derived != false) {
this->Base::Base();
}
}
};
class Mid2 : virtual public Base {
public:
Mid2* Mid2(Mid2* this, bool __most_derived) {
if (__most_derived != false) {
this->Base::Base();
}
}
};
class Derived : public Mid1, public Mid2 {
public:
Derived* Derived(Derived* this, bool __most_derived) {
if (__most_derived != false) {
this->Base::Base();
}
this->Mid1(false); //壓制Mid1呼叫Base建構函式
this->Mid2(false);
}
};
讀到這裡,我想給大夥推薦一篇文章《C++中定義一個不能被繼承的類(友元類+類模板)》該問題正是利用了虛繼承的相關性質。
2018-12-24 北京 海淀
Reference: 《深度探索C++物件模型》 侯捷 譯