Table of Contents
1.1 Nonstatic Member Functions
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函數指針”。