1. 程式人生 > >《深度探索C++物件模型》:member functions

《深度探索C++物件模型》:member functions

        在第一篇文章中我們談到member function有三種:nonstaticstaticvirtual。前面兩種並不是我們今天的主角,我們要學習的是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()的時候,那我們呼叫的是classPointobjecty()還是classPoint2dobjecty()呢?也就是說,y()是一個virtual member function,我們知道RTTI的特性就是某些資訊只有在執行期才能確定,而在編譯期是不能確定的。

顯然,不管編譯期還是執行期,若我們要正確的執行y()的例項,那麼整個過程中需要知道:

1、ptr所指物件的真實型別;

2、y()例項的位置。

根據前面幾節物件模型學習,我們知道在C++物件模型中,在編譯期編譯期會為我們構建兩個東西:vptrvtbl,在程式執行時表格的大小和內容是不會發生改變的。由於要執行y()函式,可以由兩個步驟來完成這項任務(即vptrvtbl的構建):

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著,侯捷譯;