《深度探索C++物件模型》:member functions
在第一篇文章中我們談到member function有三種:nonstatic、static和virtual。前面兩種並不是我們今天的主角,我們要學習的是virtual member functions。
nonstatic member functions
C++的設計準則之一就是:nonstatic member function至少必須和一般的nonmember function有相同的效率,實際上member function被編譯器內化為nonmember的形式。member function的呼叫必須經由一個class object才能操作,這點與static mmber function不同。
static member functions
一個static member function的主要特性是它沒有this指標,並且由此衍生出來的其他特性有:
1、它不能夠直接存取class中的nonstatic member;
2、它不能夠被宣告為const、volatile或virtual;
3、它的呼叫不需要經由class object(雖然大部分時候它是這麼被呼叫的)。
// 如果Point3d::normalize()是一個static member function obj.normalize(); ptr->normalize(); // 那麼以上兩個呼叫操作,將會被轉換為一般的nonmember函式的呼叫。
單一繼承下的virtual functions
在之前的文章中,我們或多或少的瞭解了virtual的一般實現模:每一個class有一個vtbl,內含class的virtual functions的地址,每個class object有一個vptr,指向vtbl。
我們來看一個例子:
class Point { public: virtual ~Point(); virtual Point& mult(float) = 0; // ... float x() { return _x; } virtual float y() const { return 0.0F; } virtual float z() const { return 0.0F; } // ... protected: Point(float x = 0.0F); float _x; }; class Point2d : public Point { public: Point2d(float x = 0.0F, float y = 0.0F) : Point(x), _y(y) {} ~Point2d(); Point2d& mult(float); float y() const { return _y; } protected: float _y; };
顯然這是一個單一繼承的例子,很簡單。那麼就有一個問題來了,當我們呼叫某個virtual member function譬如說ptr->y()的時候,那我們呼叫的是classPointobject的y()還是classPoint2dobject的y()呢?也就是說,y()是一個virtual member function,我們知道RTTI的特性就是某些資訊只有在執行期才能確定,而在編譯期是不能確定的。
顯然,不管編譯期還是執行期,若我們要正確的執行y()的例項,那麼整個過程中需要知道:
1、ptr所指物件的真實型別;
2、y()例項的位置。
根據前面幾節物件模型學習,我們知道在C++物件模型中,在編譯期編譯期會為我們構建兩個東西:vptr和vtbl,在程式執行時表格的大小和內容是不會發生改變的。由於要執行y()函式,可以由兩個步驟來完成這項任務(即vptr和vtbl的構建):
1、為了找到表格,每一個class object被安插了一個由編譯器內部產生的指標,指向該表格;
2、為了找到該函式地址,每一個virtual function被指派一個表格索引值。
當然,以上的工作都是有編譯器在編譯期完成的,那麼在執行期所要做的就是,只在特性的virtual table slot中啟用virtual function。
根據上面的物件模型,我們在編譯期就構建了vptr和vtbl,而且已經知道了某個函式的索引值,因此唯一一個在執行期才能知道的東西就是:slot所指的到底是哪一個函式例項,當然執行期ptr的具體指向會為我們解決這個問題。
class Point3d : public Point2d {
public:
Point3d(float x = 0.0F, float y = 0.0F, float z = 0.0F) : Point2d(x, y), _z(z) {}
~Point3d();
Point3d& mult(float);
float z() const { return _z; }
// ...
protected:
float _z;
};
在單一繼承中,virtual function機制的行為非常良好的,不但有效率而且很容易被塑模出來。
多重繼承下的virtual functions
對於多重繼承,我們還是直接來看一個例子。
class Base1 {
public:
Base1();
virtual ~Base1();
virtual void speakClearly();
virtual Base1* clone() const;
protected:
float data_Base1;
};
class Base2 {
public:
Base2();
virtual ~Base2();
virtual void mumble();
virtual Base1* clone() const;
protected:
float data_Base2;
};
class Derived : public Base1, public Base2 {
public:
Derived();
virtual ~Derived();
virtual Derived* clone() const;
protected:
float data_Derived;
};
在多重繼承之下,物件模型的機制是:一個derived table slot內含n-1個額外的virtual tables,n表示其上一層base classes的個數(因此,單一繼承將不會有額外的virtual tables)。對於本例的Derived而言,會有兩個virtual tables被編譯器產生出來:
1、一個主要例項,與Base1(最左端base class)共享;
2、一個次要例項,與Base2(第二個base class)有關。
針對每一個virtual tables,Derived物件中有對應的vptr,具體物件模型如下:
虛擬繼承下的virtual functions
在之前的文章中,我們大概也見識到了虛擬繼承的特殊性,class subobject作為共享部分放在最後面,而在vtpr所指向的vtbl中的開頭添加了表示offset的項。下面還是來看一個現例項子。
class Point2d {
public:
Point2d(float = 0.0F, float = 0.0F);
virtual ~Point2d();
virtual void mumble();
virtual float z();
// ...
protected:
float _x;
float _y;
};
class Point3d : public virtual Point2d {
public:
Point3d(float = 0.0F, float = 0.0F, float = 0.0F);
virtual ~Point3d();
virtual float z();
// ...
protected:
float _z;
};
其物件模型如下:
這樣一來我們就清楚了虛擬繼承下的物件模型。
有一點建議就是:不要再一個virtual base class中宣告nonstatic data members。
參考資料:
[1] 深度探索C++物件模型,[美]Stanley B. Lippman著,侯捷譯;