[讀書筆記] - 《深度探索C++對象模型》第4章 Function語意學

Table of Contents

1. Member的各種調用方式

1.1 Nonstatic Member Functions

1.2 名稱的特殊處理(Name Mangling)

1.3 Virtual Member Functions

1.4 Static Member Functions

2. Virtual Member Functions

2.1 單繼承下的Virtual Functions

2.2 多重繼承下的Virtual Functions

2.3 虛擬繼承下的Virtual Functions

3. 其他


1. Member的各種調用方式

C++支持三種類型的member functions: static、nonstatic和virtual,每一種類型被調用的方式都不相同。

1.1 Nonstatic Member Functions

C++的設計準則之一就是:nonstatic member function至少必須和一般的nonmember function有相同的效率。下面是一個nonmember function的定義:

float magnitude3d(const Point3d* _this)
{
    return sqrt( _this->_x * _this->_x +
                 _this->_y * _this->_y +
                 _this->_z * _this->_z );
}

乍見之下似乎nonmember function比較沒有效率,它間接地經由參數取用座標成員,而member function確是直接取用座標成員。然而實際上member function被內化爲nonmmeber的形式。下面就是轉化步驟:

1>改寫函數的signature以安插一個額外的參數到member function中,用以提供一個存取管道,使class object得以調用該函數。該額外參數被稱爲this指針:

// non-const nonstatic member的增長過程
Point3d Point3d::magnitude(Point3d* const this)

如果member function是const,則變成:

// const nonstatic member的增長過程
Point3d Point3d::magnitude(const Point3d* const this)

2>將每一個“對nonstatic data member的存取操作”改爲經由this指針來存取:

{
    return sqrt( _this->_x * _this->_x +
                 _this->_y * _this->_y +
                 _this->_z * _this->_z );
}

3>將member function重新寫成一個外部函數。對函數名稱進行“mangling”處理,使它在程序中稱爲獨一無二的語彙:

extern magnitude__7Point3dFv(register Point3d* const this);

現在這個函數已經被轉換好了,而其每一個調用操作也都必須替換:

obj.magnitude();
==> magnitude__7Point3dFv( &obj );

ptr->magnitude();
==> magnitude__7Point3dFv( ptr );

在函數內直接return創建的對象比先創建,再return有效率:

Point3d Point3d::normalize() const
{
    Point3d normal:

    normal._x = _x/2;
    normal._y = _y/2;
    normal._z = _z/2;

    return normal;
}

// 直接建構“normal”值比較有效率
Point3d Point3d::normalize() const
{
    return Point3d( _x/2, _y/2, _z/2 );
}

1.2 名稱的特殊處理(Name Mangling)

class member的名稱前面會被加上class名稱,形成獨一無二的命名。

class Bar { public: int ival; ... }
class Foo : public { public: int ival; ... }

// Foo 的內部描述
class Foo
{
public:
    int ival_3Bar;
    int ival_3Foo;
    ...
};

不管你要處理哪一個ival,通過“name mangling”,都可以絕對清楚地指出來。由於member functions可以被重載,所以需要更廣泛的mangling手法,以提供絕對獨一無二的名稱。

class Point
{
public:
    void x(float newX);
    float x();
    ...
};

//內部描述
class Point
{
public:
    void x__5PointFf(float newX);
    float x__5PointFv();
};

兩個實體如果擁有獨一無二的name mangling,那麼任何不正確的調用操作在鏈接時期就因無法決議(resolved)而失敗。但是它只可以捕捉函數signature(函數名稱+參數數目+參數類型)錯誤;如果“返回類型”聲明錯誤,就沒辦法檢查出來。

1.3 Virtual Member Functions

如果normalize()是一個virtual member function,那麼以下的調用:

ptr->normalize();

將會被內部轉化爲:

(*ptr->vptr[1])(ptr);

其中:

1>vptr表示由編譯器產生的指針,指向virtual table。事實上,其名稱也會被“mangled”,因爲在一個複雜的class派生體系中,可能存在有多個vptrs。

2>1是virtual table slot的索引值,關聯到normalize()函數。

3>第二個ptr表示this指針。

使用class scope operator明確調用一個virtual funciton,其決議(resolved)方式會和nonstatic member function一樣:

//明確地調用操作會壓制虛擬機制
register float mag = Point3d::magnitude();
<==> register float mag = magnitude__7Point3dFv(this);

1.4 Static Member Functions

如果Point3d::normalize()是一個static member function,以下兩個調用操作將被轉換爲一般的nonmember函數調用:

obj.normalize();
==> normalize__7Point3dSFv();

ptr->normalize();
==> normalize__7Point3dSFv();

在引入static member functions之前,C++語言要求所有的member functions都必須經由該class的object來調用。而實際上,只有當一個或多個nonstatic data members在member function中被直接存取時,才需要class object。Class object提供了this指針給這種形式的函數調用使用。這個this指針把“在member function中存取的nonstatic class members”綁定於“object內對應的members”之上。如果滅有任何一個members被直接存取,事實上就不需要this指針,因此也就沒有必要通過一個class object來調用一個member function。不過C++語言到當前爲止並不能夠識別這種情況。

如果class的設計者把static data member聲明爲nonpublic(這一直被視爲是一種好的習慣),那麼他就必須提供一個或多個member functions來存取該member。一次,雖然你可以不靠class object來存取一個static member,但其存取函數卻得綁定於一個class object之上。

static member functions的主要特性就是沒有this指針。以下的次要特性統統根源於其主要特性:

1>它不能直接存取其class中的nonstatic members;

2>它不能被聲明爲const、volatile或virtual;

3>它不需要經由class object才被調用——雖然大部分時候它是這樣被調用的。

static member function由於缺乏this指針,因此差不多等同於nonmember function。

2. Virtual Member Functions

在C++中,多態(polymorphism)表示“以一個public base class的指針(或reference),尋址出一個derived class object”的意思。

在C++中,virtual functions可以在編譯時期獲知,此外,這一組地址是固定不變的,執行期不可能新增或替換它。由於程序執行時,表格的大小和內容都不會改變,所以其建構和存取皆可以由編譯器完全掌握,不需要執行期的任何介入。

爲了找到表格,每一個class object被安插上一個由編譯器內部產生的指針,指向該表格。爲了找到函數地址,每一個virtual function被指派一個表格索引值。

這些工作都由編譯器完成。執行期要做的,只是在特定的virtual table slot中激活virtual function。

2.1 單繼承下的Virtual Functions

單繼承下,一個class只會有一個virtual table。每一個table內含其對應的class object中所有active virutal functions函數實體的地址。這些active virtual functions包括:

1>這個class所定義的函數實體。它會改寫(overriding)一個可能存在的base class virtual function函數實體;

2>繼承自base class的函數實體。這是在derived class決定不改寫virtual function時纔會出現的情況;

3>一個pure_virtual_called()函數實體,它既可以扮演pure virtual function的空間保衛者角色,也可以當做執行期異常處理函數(有時候會用到)。

每一個virtual function都被指派一個固定的索引值,這個索引在整個繼承體系中保持與特定的virtual function的關聯。

class Point
{
public:
    virtual ~Point();
    virtual Point& mult(float) = 0;
    
    float x() const { return _x; }
    virtual float y() const { return 0; }
    virtual float z() const { return 0; }

protected:
    Point(float x = 0.0);
    float _x;
};

class Point2d : public Point
{
public:
    Point2d(float x = 0.0, float y = 0.0)
        : Point(x), _y(y) { }
    ~Point2d();

    //改寫base class virtual functions
    Point2d& mult(float);
    float y() const { return _y; }
protected:
    float _y;
};
    
class Point3d : public Point2d
{
public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
        : Point2d(x, y), _z(z) { }
    ~Point3d();

    //改寫base class virtual functions
    Point3d& mult(float);
    float z() const { return _z; }

protected:
    float _z;
};

對於class Point,virtual destructor被賦值slot 1,而mult()被賦值slot2。此例並沒有mult()的函數定義,所以pure_virtual_called()的函數地址會被放在slot2中。如果該函數被意外地被調用,通常的操作是結束掉這個程序。

class Point2d繼承自class Point,Point2d的virtual table在slot1中指出destructor,而在slot2中指出mult()(取代pure virtual function)。它自己的y()函數實體地址放在slot3,繼承自Point的z()函數實體地址則放在slot4。

現在,如果我有這樣的式子:

ptr->z();

那麼如何有足夠的知識在編譯時期設定virtual function的調用呢?

1>一般而言,我並不知道ptr所指對象的真正類型。然而我知道,經由ptr可以存取到該對象的virtual table;

2>雖然我不知道哪一個z()函數實體會被調用,但我知道每一個z()函數地址都被放在slot4。

這些信息是的編譯器可以將該調用轉化爲:

(*ptr->vptr[4])(ptr);

在這個轉化中,vptr表示編譯器所安插的指針,指向virtual table;4表示z()被賦值的slot編號(關聯到Point體系的virtual table)。唯一一個在執行期才能知道的東西是:slot4所指的到底是哪一個z()函數實體?

2.2 多重繼承下的Virtual Functions

在多重繼承中支持virtual functions,其複雜度圍繞在第二個及後繼的base classes身上,以及“必須在執行期調整this指針”這一點。

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 Base2* clone() const;
protected:
    float data_Base2;
};

class Derived : public Base1, public Base2
{
public:
    Derived();
    virtual ~Derived();
    virtual Derived* clone() const;
protected:
    float data_Derived;
};

有三種情況,第二或後繼的base class會影響對virtual functions的支持:

1>通過一個“指向第二個base class”的指針,調用derived class virtual function。

Base2* pbase2 = new Derived;

新的Derived對象的地址必須調整,以指向其Base2 subobject。編譯時期會產生以下的代碼:

// 轉移以支持第二個base class
Derived* temp = new Derived;
Base2* pbase2 = temp ? temp + sizeof(Base1) : 0;

如果沒有這樣的調整,指針的任何“非多態運用”都將失敗:

// 即使pbase2被指定一個Derived對象,這也應該沒有問題
pbase2->data_Base2;

當要刪除pbase2所指的對象時:

// 必須首先調用正確的virtual destructor函數實體
// 然後施行delete運算符
// pbase2 可能需要調整,以指出完整對象的起始點
delete pbase2;

指針必須被再一次調整,以求再一次指向Derived對象的起始處(推測它還指向Derived對象)。然而上述的offset加法卻不能夠在編譯時期直接設定,因爲pbase2所指的真正對象只有在執行期才能確定。

一般規則是,經由指向“第二或後繼的base class”的指針(或reference)來調用derived class virtual function,該調用操作所連帶的“必要的this指針調整”操作,必須在執行期完成。

在多重繼承之下,一個derived class內含n-1個額外的virtual tables,n表示其上一層base classes的數目(因此,單一繼承將不會有額外的virtual tables)。對於本例的Derived而言,會有兩個virtual tables被編譯器產生出來:

(1)一個主要實體,與Base1(最左端base class)共享;

(2)一個次要實體,與Base2(第二個base class)有關。

針對每一個virtual tables,Derived對象中有對應的vptr。vptrs將在constructors中被設立初值。

用以支持“一個class擁有多個virtual tables”的傳統方法是,將每一個tables以外部對象的形式產生出來,並給與獨一無二的名稱。例如,Derived所關聯的兩個tables可能有這樣的名稱:

vtbl__Derived;     //主要表格
vtbl__Base2__Derived; //次要表格

於是當你將一個Derived對象地址指定給一個Base1指針或Derived指針時,被處理的virtual table是主要表格vtbl__Derived。而當你將一個Derived對象地址指定給一個Base2指針時,被處理的virtual table是次要表格vtbl__Base2__Derived。

2>通過一個“指向derived class”的指針,調用第二個base class中一個繼承而來的virtual function。在此情況下,derived class指針必須再次調整,以指向第二個base subobject。

Derived* pder = new Derived;

// 調用Base2::mumble()
// pder 必須被向前調整sizeof(Base1)個bytes
pder->mumber();

3>允許一個virtual function的返回值類型有所變化,可能是base type,也可能是publicly derived type。

本例的Derived::clone()傳回一個Derived class指針,默默地改寫了它的兩個base class函數實體。

Base* pb1 = new Derived;

//調用Derived* Derived::clone()
//返回值必須被調整,以指向Base2 subobject
Base2* pb2 = pb1->clone();

當進行pb1->clone()時,pb1會被調整指向Derived對象的起始地址,於是clone()的Derived版會被調用;它會傳回一個指針,指向一個新的Derived對象;該對象的地址在被指定給pb2之前,必須先經過調整,以指向Base2 subobject。

2.3 虛擬繼承下的Virtual Functions

class Point2d
{
public:
    Point2d(float = 0.0, float = 0.0);
    virtual ~Point2d();
    virtual void mumble();
    virtual float z();
protected:
    float _x, _y;
};

class Point3d : public virtual Point2d
{
public:
    Point3d(float = 0.0, float = 0.0, float = 0.0);
    ~Point3d();
    float z();
protected:
    float _z;
};

雖然Point3d有唯一一個(同時也是最左邊的)base class,也就是Point2d,但Point3d和Point2d的起始部分並不像“非虛擬的單一繼承”情況那樣一致。由於Point2d和Point3d的對象不再相符,兩者之間的轉換也就需要調整this指針。

當一個virtual base class從另一個virtual base class派生而來,並且兩者都支持virtual functions和nonstatic data members時,編譯器對於virtual base class的支持簡直就像進了迷宮一樣。因此,最好不要在一個virtual base class中聲明nonstatic data members。

3. 其他

取一個nonstatic member function的地址,如果該函數是nonvirtual,則得到的結果是它在內存中真正的地址。然而這個值也是不完全的,它也需要被綁定於某個class object的地址上,才能夠通過它調用該函數。所有的nonstatic member functions都需要對象的地址(以參數this指出)。

如果取一個static member function的地址,獲得的將是其在內存中的位置,也就是其地址。由於static member function沒有this指針,所以其地址的類型並不是一個“指向class member function的指針”,而是一個“nonmember函數指針”。

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