1. 程式人生 > >淺談C++多型實現原理(虛繼承的奧祕)

淺談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)。

也正是這個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++物件模型》 侯捷 譯