若是有一個Point3d的指針和對象:ios
Point3d obj; Point3d *ptr = &obj;
當這樣作:程序員
obj.normalize(); ptr->normalize();
時,會發生什麼事?其中的Point3d::normalize()定義以下:express
Point3d Point3d::normalize() const { register float mag = magnitude(); Point3d normal; normal._x = _x / mag; normal._y = _y / mag; normal._z = _z / mag; return normal; }
而其中的Point3d::magnitude()又定義以下:安全
float Point3d::magnitude() const { return sqrt( _x * _x + _y * _y + _z * _z ); }
答案是不明確的。C++支持三種類型的member functions:static、nonstatic和virtual,每一ide
種類型被調用的方式都不一樣。不過咱們雖不能肯定normalize()和magnitude()兩函數是否爲函數
virtual或nonvirtual,但能夠肯定它必定不是static,緣由有二:(1)它直接存取nonstatic數據,(2)工具
它被聲明爲const。而static member functions不可能作到這兩點。佈局
C++的設計準則之一就是:nonstatic member function至少必須和通常的nonmember測試
function有相同的效率。也就是說,若是咱們要在如下兩個函數之間作選擇:優化
float magnitude3d( const Point3d *_this ){ ... } float Point3d::magnitude3d() const { ... }
選擇member function不該該帶來什麼額外負擔。這是由於編譯器內部已將」member 函數實
例「轉換爲對等的」nonmember函數實例「。
好比下面是magnitude()的一個nonmember定義:
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被內化爲nonmember的
形式。下面是轉化步驟:
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 );
而normalize()函數會被轉化爲下面的形式,其中假設已經聲明有一個Point3d copy
constructor,而named returned value(NRV)的優化也已經實施:
// 如下描述」named return value函數「的內部轉化 // 使用C++代碼 void normalize_7Point3dFv( register const Point3d *const this, Point3d &_result ) { register float mag = this->magnitude(); // default constructor _result.Point3d::Point3d(); _result._x = this->_x / mag; _result._y = this->_y / mag; _result._z = this->_z / mag; return; }
一個有效率的作法是直接構建」normal「值,像這樣:
Point3d Point3d::normalize() const { register float mag = magnitude(); return Point3d( _x / mag, _y / mag, _z / mag ); }
它會轉化爲如下的代碼(再一次假設Point3d的copy constructor已經聲明好了,而NRV
的優化也已實施):
// 如下描述內部轉化 // 使用C++僞碼 void normalize_7Point3dFv( register const Point3d *const this, Point3d & _result ) { register float mag = this->magnitude(); // _result用以取代返回值(return value) _result.Point3d::Point3d( this->_x / mag, this->_y / mag, this->_z / mag ); return; }
這能夠節省default constructor初始化所引發的額外負擔。
通常而言,member的名稱前面會被加上class名稱,造成獨一無二的命令。例以下面的聲
明:
class Bar { public: int ival; ... };
其中的ival有可能變成這樣:
// member通過name-mangling以後的可能結果之一 ival_3Bar
爲何編譯器要這麼作?清考慮這樣的派生操做(derivation):
class Foo : public Bar { public: int val;... };
Foo對象內部結合了base class和derived class二者:
// C++僞碼 // Foo的內部描述 class Foo { public: int ival_3Bar; int ival_3Foo; };
無論處理哪一個ival,經過」name mangling「,均可以絕對清楚地指出來。因爲member
functions能夠被重載化(overload),因此須要更普遍的mangling手法,以提供對獨一無二的
名稱。若是把:
class Point { public: void x( float newX ); float x(); ... };
轉換爲:
class Point { public: void x_5Point( float newX ); float x_5Point(); ... };
會致使兩個被重載化(overloaded)的函數實例擁有相同的名稱。爲了讓它們獨一無二,惟
有再加上它們的參數鏈表(能夠從函數原型中參考獲得)。若是把參數類型也編碼進取,就一
定能夠製造出獨一無二的結果,使咱們的兩個x() 函數有良好的轉換(若是聲明extern 」C「,就會
壓抑nonmember functions的」mangling「效果):
class Point { public: void x_5PointFf( float newX ); float x_5PointFv(); ... };
把參數和函數名稱編碼在一塊兒,編譯器因而在不一樣的編譯模塊之間達成了一種優先形式的
類型檢驗。以下print函數被這樣定義:
void print( const Point3d& ) { ... }
但意外地被這樣聲明和調用:
// 覺得是const Point3d& void print( const Point3d );
兩個實例若是擁有獨一無二的name mangling,那麼任何不正確的調用操做在連接時期就
因沒法決議(resolved)而失敗。但若是是」返回類型「聲明錯誤就沒辦法檢查出來。
若是normalize()是一個virtual member function,那麼如下的調用:
ptr->normalize();
將會被內部轉化爲:
( *ptr->vptr[ 1 ] )( ptr );
其中:
1)vptr表示由編譯器產生的指針,指向virtual table。它被安插在每個「聲明有(或繼承
自),一個或多個virtual functions」的class object中。其名稱也會被「mangled」,由於在一個復
雜的class派生體系中,可能存在多個vptrs。
2)1是virtual table slot的索引值,關聯到normalize()函數。
3)第二個ptr表示this指針。
一樣,若是magnitude()也是一個virtual function,它在normalize()之中的調用操做將被轉
換以下:
// register float mag = magnitude(); register float mag = ( *this->vptr[ 2 ] )( this );
因爲Point3d::magnitude()是在Point3d::normalize()中被調用的,然後者已經由虛擬機制而
決議穩當,因此顯示地調用「Point3d實例」會比較有效率,並所以壓制因爲虛擬機制而產生的不
必要重複調用操做:
// 顯示的調用操做(explicitly invocation)會壓制虛擬機制 register float mag = Point3d::magnitude();
若是magnitude()聲明爲inline函數,會更有效率。使用class scope operator顯示調用一
個virtual function,其決議方式會和nonstatic member function同樣:
register float mag = magnitude_7Point3dFv( this );
對於如下調用:
// Point3d obj; obj.normalize();
若是編譯器把它轉換爲:
( *obj.vptr[ 1 ] )( &obj );
雖然語意正確,卻沒有必要。」經由一個class object 調用一個virtual function「,這種操做
應該老是被編譯器像對待通常nonstatic member function同樣地加以決議:
normalize_7Point3dFv( &obj );
這項優化的一利益是,virtual function的一個inline函數實例能夠被擴展(expanded)開
來,於是提供極大的效率利益。
若是Point3d::normalize()是一個static member function,如下兩個調用操做:
obj.normalize(); ptr->normalize();
將被轉換爲通常的nonmember函數調用,以下:
// obj.normalize(); normalize_7Point3dSFv(); // ptr->normalize(); normalize_7Point3dSFv();
在C++引入static member functions以前,不多會看到以下怪異寫法:
( ( Point3d* )0 )->object_count();
其中的object_count只是簡單傳回_object_count這個static data member。
在引入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。
這麼一來就存取static data members時產生了一些不規則性。若是class的設計者把static
data member聲明爲nonpublic(這一直被視爲一種好的習慣),那麼他就必須提供一個或多個
member functions來存取該member。所以,雖然你能夠不靠class object來存取一個static
member,但其存取函數卻得綁定於一個class object之上。
獨立於class object以外的存取操做,在某個時候特別重要:當class設計者但願支持」沒
有class object存在「的狀況時。程序方法上的解決之道是很奇特意把0強制轉換爲一個class指
針,於是提供出一個this指針實例:
// 函數調用的內部轉換 object_count( ( Point3d* )0 );
Static member functions的主要特性就是它沒有this指針。如下次要特性通通根源於其主
要特性:
1)它不能直接存取其class中的nonstatic members。
2)它不可以被聲明爲const、volatile或virtual。
3)它不須要經由class object才被調用——雖然大部分時候它是這樣被調用的!
「member selection」語法的使用是一種符號上的便利,它會被轉化爲一個直接調用操做:
if( Point3d::object_count() > 1 ) ...
若是class object是由於某個表達式而得到的,會如何?例如:
if( foo().object_count() > 1 ) ...
這個表達式仍然須要被評估求值:
// 轉化,以保存反作用 ( void ) foo(); if( Point3d::object_count() > 1 ) ...
一個static member function,固然會被提出於class聲明以外,並給予一個通過
「mangled」的適當名字。例如:
unsigned int Point3d::object_count() { return _object_count; }
會被cfront轉化爲:
// 在cfront之下的內部轉化結果 unsigned int object_count_5Point3dSFv() { return _object_count_5Point3d; }
其中SFv表示它是一個static member function,擁有一個空白(void)的參數鏈表
(argument list)。
因爲static member function沒有this指針,因此其地址的類型並非一個「指向class
member function的指針」,而是一個「nonmember函數指針」。也就是說:
&Point3d::object_count();
會獲得一個數值,類型是:
unsigned int (*)();
而不是:
unsigned int ( Point3d::* )( );
Static member function因爲缺少this指針,所以差很少等同於nonmember function。它
提供了一個意想不到的好處:成爲一個callback函數,使咱們得以將C++和C-base X Window系
統結合。它們也能夠成功地應用在線程(threads)函數身上。
virtual function的通常實現模型:,每個class 有一個virtual table,內含該
class之中有做用的virtual function的地址,而後每一個object有一個vptr,指向virtual
table的所在。
爲了支持virtual function機制,必須首先可以對於多態對象有某種形式的「執行期類型判斷
(runtime type resolution)」。也就是說如下的調用操做將須要ptr在執行期的某些相關信息:
ptr->z();
如此一來纔可以找到並調用z()的適當實例。
或許直截了當可是成本最高的解決方法就是把必要信息加載ptr身上。在這樣的策略之
下,一個指針(或是一個reference)持有兩項信息:
1)它所參考到的對象的地址(也就是目前它所持有的東西);
2)對象類型的某種編碼,或是某個結構(內含某些信息,用以正確決議出z()函數實例)
的地址。
這個方法帶來兩個問題:第一,它明顯增長了空間負擔,即便程序並不使用多態
(polymorphism);第二,它打斷了與C程序間的連接兼容性。
若是這份額外信息不可以和指針放在一塊兒,下一個能夠考慮的地方就是把它放在對象本
身。可是哪個對象真正須要這些信息呢?咱們應該把這些信息放進可能被繼承的每個集合
體身上呢?也許。但請考慮一下這樣的C struct聲明:
struct date { int m, d, y; };
這符合上述規則。然而事實上它並不須要那些信息。加上那些信息將使C struct膨脹而且
打破連接兼容性,卻沒有帶來任何明顯的補償利益。
而面對那些顯示使用了class關鍵詞的聲明,才應該加上額外的執行期信息。這樣作能夠
保持語言的兼容性,不過仍然不是一個夠聰明的政策。例如,下面這個class符合新規則:
class data { public: int m, d, y; };
但實際上它並不須要那份信息。下面的class聲明雖然不符合新規範,卻須要那份信息:
struct geom { public: virtual ~geom(); ... };
咱們須要一個以class的使用爲基礎,而不在意關鍵詞是class或struct的規範。若是class
真正須要那份信息,它就會存在;若是不須要,它就不存在。很明顯在必須支持某種形式之「執行
期多態(runtime polymorphism)」的時候須要這份信息。
在C++中,多態(ploymorphism)表示「一個public base class的指針(或reference),
尋找出一個derived class object」的意思。例以下面的聲明:
Point *ptr;
咱們能夠指定ptr以尋址出一個Point2d對象:
ptr = new Point2d;
或是一個Point3d對象:
ptr = new Point3d;
ptr的多態技能主要扮演一個輸送機制(transport mechanism)的角色,經由它,咱們
能夠在程序的任何地方採用一組public derived類型。這種多態形式被稱爲是消極的(passive)
,能夠在編譯時期完成——virtual base class的狀況除外。
當被指出的對象真正被使用時,多態也就變成積極的(active)了。下面對於virtual
function的調用,就是一例:
// "積極多態(active ploymorphism)"的常見例子 ptr->z();
在funtime type identification(RTTI)性質於1993年被引入C++語言以前,C++對「積極
多態(active polymorphism)」的惟一支持,就是對於virtual function call 的決議操做。有了
RTTI,就可以在執行期查詢一個多態的pointer或多態的reference了:
// "積極多態(active polymorphism)"的第二個例子 if( Point3d *p3d = dynamic_cast<Point3d*>( ptr ) ) return p3d->_Z;
因此欲鑑定哪些classes展示多態特性,咱們須要額外的執行期信息。關鍵詞class和
struct並不能幫助咱們。因爲沒有導入像是polymorphic之類的新關鍵詞,所以識別一個class是
否支持多態,惟一適當的方法就是看看它是否有任何virtual function。只要class擁有一個virtual
function,它就須要這份額外的執行期信息。
下一個明顯的問題是,什麼樣的額外信息是咱們須要存儲起來的?也就是說,若是有這
樣的調用:
ptr->z();
其中z()是一個virtual function,那麼什麼信息才能讓咱們在執行期調用正確的z()實例?
須要知道:
1)ptr所指對象的真實類型。這可以使咱們選擇正確的z()實例。
2)z()實例的位置,以便可以調用它。
在實際上,首先能夠在每個多態的class object身上增長兩個members:
1)一個字符串或數字,表示class的類型。
2)一個指針,指向某表格,表格中持有程序的virtual functions的執行期地址。
關於表格中的virtual functions地址如何被構建起來。在C++中,virtual functions(可經由
其class object被調用)能夠在編譯時期獲知。此外,這一組地址是固定不變的,執行期不可能
新增或替換之。因爲程序執行時,表格大小和內容不會改變,因此其建構和存取皆能夠由編譯
器徹底掌控,不須要執行期的任何介入。
然而,執行期備妥那些函數地址,只是解答的一半而已。另外一半解答是找到那些地址。
兩個步驟能夠完成這項任務:
1)爲了找到表格,每個class object被安插了一個由編譯器內部產生的指針,指向該
表格。
2)爲了找到函數地址,每個virtual function被指派一個表格索引值。
這些工做都是由編譯器完成。執行期要作的,只是在特定的virtual table slot中激活virtual
function。
一個class只會有一個virtual table。每個table內含其對應之class object中全部active
virtual function函數實例的地址。包括:
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的關係。例如咱們的Point class體系中:
#include <iostream> class Point { public: Point( float x = 0.0 ) : _x( x ) { } virtual ~Point() { } virtual int mult( float ) = 0; // ...其餘操做 float x() const { return _x; } virtual float y() const { return 0; } virtual float z() const { return 0; } // ... protected: 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 int mult( float y ) { return 1; } 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 int mult( float z ) { return 2; } float z() const { return _z; } // ...其餘操做 protected: float _z; }; int main() { Point2d point2d; Point3d point3d; std::cout << "sizeof( Point ) = " << sizeof( Point ) << std::endl; std::cout << "sizeof( point2d ) = " << sizeof( point2d ) << std::endl; std::cout << "sizeof( point3d ) = " << sizeof( point3d ) << std::endl; return 0; }
下面是class Point的虛表:
.section .rodata._ZTV5Point,"aG",@progbits,_ZTV5Point,comdat .align 8 .type _ZTV5Point, @object .size _ZTV5Point, 28 _ZTV5Point: # vtable for Point .long 0 .long _ZTI5Point # typeinfo for Point .long _ZN5PointD1Ev # Point::~Point() .long _ZN5PointD0Ev # Point::~Point() .long __cxa_pure_virtual .long _ZNK5Point1yEv # Point::y() const .long _ZNK5Point1zEv # Point::z() const
virtual destructor被指派slot 2,3,而mult()被指派slot 4。此例並無mult()的函數定義(因
爲它是一個pure virtual function),因此pure_virtual_called()的函數地址會被放在slot 4。若是
該函數意外地被調用,一般操做是結束這個程序。y()被指派slot 5,z被指派slot 6。x()不是
virtual function因此不存在虛表中。
下面是class Point2d的虛表:
.section .rodata._ZTV7Point2d,"aG",@progbits,_ZTV7Point2d,comdat .align 8 .type _ZTV7Point2d, @object .size _ZTV7Point2d, 28 _ZTV7Point2d: # vtable for Point2d .long 0 .long _ZTI7Point2d # typeinfo for Point2d .long _ZN7Point2dD1Ev # Point2d::~Point2d() .long _ZN7Point2dD0Ev # Point2d::~Point2d() .long _ZN7Point2d4multEf # Point2d::mult(float) .long _ZNK7Point2d1yEv # Point2d::y() const .long _ZNK5Point1zEv # Point::z() const
當一個class派生自Point時,會發生什麼事?
一共有三種可能性:
1)它能夠繼承base class所聲明的virtual function的函數實例。正確地說是,該函數實例
的地址會被拷貝到derived class的virtual table的相對應slot之中。
2)它可使用本身的函數實例。這表示它本身的函數實例地址必須放在對應的slot之中。
3)它能夠加入一個新的virtual function。這時候virtual table的尺寸會增大一個slot,而新
的函數實例地址會被放進該slot之中。
Point2d的virtual table在slot 2,3中指出destructor,而slot 4中指出mult()(取代pure
virtual function)。它本身的y()函數實例地址放在slot 5中,繼承自Point的z()函數實例地址則放
在slot 6中。
下面是class Point3d的虛表:
.section .rodata._ZTV7Point3d,"aG",@progbits,_ZTV7Point3d,comdat .align 8 .type _ZTV7Point3d, @object .size _ZTV7Point3d, 28 _ZTV7Point3d: # vtable for Point3d .long 0 .long _ZTI7Point3d # typeinfo for Point3d .long _ZN7Point3dD1Ev # Point3d::~Point3d() .long _ZN7Point3dD0Ev # Point3d::~Point3d() .long _ZN7Point3d4multEf # Point2d::mult(float) .long _ZNK7Point2d1yEv # Point2d::y() const .long _ZNK7Point3d1zEv # Point3d::z() const
一樣對於派生自Point2d的Point3d,其virtual table中的slot 2,3放置Point3d的
destructor,slot 4放置Point3d::mult()函數地址,slot 5放置繼承自Point2d的y()函數地址,slot 6
放置本身的z()函數地址。
如今,若是有這樣的式子:
ptr->z();
如何有足夠的知識在編譯時期設定virtual function的調用呢?
1)通常而言,在每次調用z()時,並不知道ptr所指對象的真正類型。然而知道經由ptr能夠
存取到該對象的virtual table。
2)雖然不知道哪個z()函數實例會被調用,但知道每個z()函數地址都被放在slot 6中。
這些信息使得編譯器能夠將該調用轉化爲:
( *ptr->vptr[ 6 ] )( ptr );
這一轉化中,vptr表示編譯器所安插的指針,指向virtual table;6表示z()被指派的slot編號
(關係到Point體系的virtual table)。惟一一個在執行期纔可以知道的東西是:slot 6所指的到
底是哪個z()函數實例。
在一個單一繼承體系中,virtual function 機制的行爲十分良好,不但有效率並且很容易塑
造出模型來。可是在多重繼承和虛擬繼承之中,對virtual functions的支持就沒那麼美好了。
在多重繼承中支持virtual functions,其複雜度圍繞在第二個及後繼的base classes身上,
以及」必須在執行期調整this指針「這一點。如下面的class體系爲例:
#include <iostream> class Base1 { public: Base1( float base1 ) : data_Base1( base1 ) { } virtual ~Base1() { } virtual void speakClearly() { data_Base1 += 1000.0; } virtual Base1 *clone() const { return new Base1( this->data_Base1 ); } protected: float data_Base1; }; class Base2 { public: Base2( float base2 ) : data_Base2( base2 ) { } virtual ~Base2() { } virtual void mumble() { data_Base2 -= 1.0; } virtual Base2 *clone() const { return new Base2( this->data_Base2 ); } protected: float data_Base2; }; class Derived : public Base1, public Base2 { public: Derived( float data1, float data2, float derived ) : Base1( data1 ), Base2( data2 ), data_Derived( derived ) { } virtual ~Derived() { } virtual Derived *clone() const { return new Derived( this->data_Base1, this->data_Base2, this->data_Derived ); } protected: float data_Derived; }; int main() { Base1 base1( 1.0 ); Base2 base2( 2.0 ); Derived derived( 1.0, 2.0, 3.0 ); std::cout << "sizeof( base1 ) = " << sizeof( base1 ) << std::endl; std::cout << "sizeof( base2 ) = " << sizeof( base2 ) << std::endl; std::cout << "sizeof( derived ) = " << sizeof( derived ) << std::endl; return 0; }
"Derived支持virtual functions"的困難度,通通落在Base2 subobject身上。有三個問題需
要解決,以此而言分別是(1)virtual destructor,(2)被繼承下來的Base2::mumble(), (3)一組
clone()函數實例。
首先,把一個從heap中配置而得的Derived對象的地址,指定給一個Base2指針:
Base2 *pbase2 = new Drived;
新的derived對象的地址必須調整以指向其Base2 subobject。編譯時期會產生如下的代
碼:
// 轉移以支持第二個base class Derived *temp = new Drived; Base2 *pbase2 = temp ? temp + sizeof( Base1 ) : 0;
若是沒有這樣的調整,指針的任何」非多態運用「都將失敗:
// 即便pbase2被指定一個Derived對象,這也應該沒有問題 pbase2->data_Base2;
當程序員要刪除pbase2所指的對象時:
// 必須首先調用正確的virtual destructor函數實例 // 而後施行delete運算符 // pbase2 可能須要調整,以指出完整對象的起始點 delete pbase2;
指針必須被再一次調整,以求再一次指向Drived對象的起始處。然而上述的offset加法
卻不可以在編譯時期直接設定,由於pbase2所指的真正對象只有在執行期才能肯定。
通常規則是,經由指向」第二或後繼之base class「的指針(或reference)來調用derived
class virtual function。
Base2 *pbase2 = new Derived; ... delete pbase2; // invoke derived class's destructor( virtual )
其所連帶的必要的」this指針調整」操做,必須在執行期完成。也就是說,offset的大小,
以及把offset加到this指針上頭的那一小段程序代碼,必須由編譯器在某個地方插入。問題是,
在哪插入?
Bjarne原先實施於cfront編譯器中的方法是將virtual table 加大,使它容納此處所須要的
this指針,調整相關事物。每個virtual table slot,再也不只是一個指針,而是一個集合體,內含
可能的offset以及地址。因而virtual function的調用操做由:
( *pbase2->vptr[ 1 ] )( pbase2 );
改變爲:
( *pbase2->vptr[ 1 ].faddr ) ( pbase2 + pbase2->vptr[ 1 ].offset );
其中faddr內含virtual function地址,offset內含this指針調整值。
這個作法的缺點是,它至關於連坐「處罰」了全部的virtual function調用操做,無論它們是
否須要offset的調整。
比較有效率的解決方法是利用所謂的thunk。所謂thunk是一小段assembly代碼,用來(1)
以適當的offset值調整this指針,(2)跳到virtual function去。例如,經由一個Base2指針調用
Derived destructor,其相關的thunk可能看起來是這個樣子:
// 虛擬C++代碼 pbase2_dtor_thunk: this += sizeof( base1 ); Derived::~Derived( this );
Thunk技術容許virtual table slot繼續內含一個簡單的指針,所以多重繼承不要任何空間上
的額外負擔。Slots中的地址能夠直接指向virtual function,也能夠指向一個相關的thunk(如需
調整this指針的話)。
調整this指針的第二個額外負擔就是,因爲兩種不一樣的可能:(1)經由derived class(或
第一個base class)調用,(2)經由第二個(或後繼)base class調用,同一函數在virtual table
中可能須要多筆對應的slots。例如:
Base1 *pbase1 = new Derived; Base2 *pbase2 = new Derived; delete pbase1; delete pbase2;
雖然兩個delete操做致使相同的Derived destructor,可是它們須要兩個不一樣的virtual
table slots:
1)pbase1不須要調整this指針(由於Base1是最左端base class之故,它已經指向
Derived對象的起始處)。其virtual table slot需放置真正的destructor地址。
2)pbase2須要調整this指針。其virtual table slot須要相關的thunk地址。
在多重繼承之下,一個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。
class Base1的虛表:
.weak _ZTV5Base1 .section .rodata._ZTV5Base1,"aG",@progbits,_ZTV5Base1,comdat .align 8 .type _ZTV5Base1, @object .size _ZTV5Base1, 24 _ZTV5Base1: # vtable for Base1 .long 0 .long _ZTI5Base1 # typeinfo for Base1 .long _ZN5Base1D1Ev # Base1::~Base1() .long _ZN5Base1D0Ev # Base1::~Base1() .long _ZN5Base112speakClearlyEv # Base1::speakClearly() .long _ZNK5Base15cloneEv # Base1::clone() const
class Base2的虛表:
.weak _ZTV5Base2 .section .rodata._ZTV5Base2,"aG",@progbits,_ZTV5Base2,comdat .align 8 .type _ZTV5Base2, @object .size _ZTV5Base2, 24 _ZTV5Base2: # vtable for Base2 .long 0 .long _ZTI5Base2 # typeinfo for Base2 .long _ZN5Base2D1Ev # Base2::~Base2() .long _ZN5Base2D0Ev # Base2::~Base2() .long _ZN5Base26mumbleEv # Base2::mumble() .long _ZNK5Base25cloneEv # Base2::clone() const
class Derived的虛表:
.weak _ZTV7Derived .section .rodata._ZTV7Derived,"aG",@progbits,_ZTV7Derived,comdat .align 32 .type _ZTV7Derived, @object .size _ZTV7Derived, 48 _ZTV7Derived: # vtable for Derived .long 0 .long _ZTI7Derived # typeinfo for Derived .long _ZN7DerivedD1Ev # Derived::~Derived() .long _ZN7DerivedD0Ev # Derived::~Derived() .long _ZN5Base112speakClearlyEv # Base1::speakClearly() .long _ZNK7Derived5cloneEv # Derived::clone() const .long -8 .long _ZTI7Derived # typeinfo for Derived .long _ZThn8_N7DerivedD1Ev # non-virtual thunk to Derived::~Derived() .long _ZThn8_N7DerivedD0Ev # non-virtual thunk to Derived::~Derived() .long _ZN5Base26mumbleEv # Base2::mumble() .long _ZTchn8_h8_NK7Derived5cloneEv # covariant return thunk to Derived::clone() const
因而當你將一個Derived對象地址指定給一個Base1指針或Derived指針時,被處理的
virtual table是主要表格。而當你將一個Derived對象地址指定給一個Base2指針時,被處理的
virtual table是次要表格。
因爲執行期連接器(runtime linkers)的降臨(能夠支持動態共享函數庫),符號名稱的
連接可能變得很是緩慢。爲了調節執行期鏈接器的效率,Sun編譯器將多個virtual tables連鎖爲
一個:指向次要表格的指針,可由主要表格名稱加上一個offset得到。這樣的策略下,每個
class只有一個具名的virtual table。
有三種狀況,第二或後繼的base class會影響對virtual functions的支持。第一種狀況
是,經過一個「指向第二個base class」的指針,調用derived class virtual function。例如:
Base2 *ptr = new Derived; // 調整Derived::~Derived // ptr必須被向後調整sizeof( Base1 )個bytes delete ptr;
這個操做的重點:ptr指向Derived對象中的Base2 subobject;爲了可以正確執行,ptr必須
調整指向Derived對象的起始處。
第二種狀況是第一種狀況的變化,經過一個「指向derived class」的指針,調用第二個
base class中一個繼承而來的virtual function。在此狀況下,derived class指針必須再次調整,
以指向第二個base subobject。例如:
Derived *pder = new Derived; // 調用Base2::mumble() // pder必須被向前調整sizeof( Base1 )個bytes pder->mumble();
第三種狀況發生於一個語言擴充性質之下:容許一個virtual function的返回值類型有所變
化,多是base type,也多是publicly derived type。這一點能夠經由Derived::clone()函數實
例來講明。clone函數的Derived版本回傳一個Derived class指針,默默地改寫了它的兩個base
class函數實例。當咱們經過「指向第二個base class」的指針來調用clone()時,this指針的offset問
題因而誕生了:
Base2 *pb1 = new Derived; // 調用Derived* Derived::clone() // 返回值必須被調整,以指向Base2 subobject Base2 *pb2 = pb1->clone();
當進行pb1->clone()時,pb1會被調整指向Derived對象的起始地址,因而clone()的
Derived版會被調用,它會傳回一個指針,指向一個新的Derived對象;該對象的地址在被指定給
pb2以前,必須先通過調整,以指向Base2 subobject。
Microsoft以所謂的「address points「來代替thunk策略。即將用來改寫別人的那個函數
(overriding function)期待得到的是」引入該virtual function之class「(而非derived class)的地
址。這就是該函數的「address point」。
考慮下面的virtual base class派生體系,從Point2d派生出Point3d:
#include <iostream> class Point2d { public: Point2d( float x = 0.0, float y = 0.0 ) : _x( x ), _y( y ) { } virtual ~Point2d() { } virtual void mumble( ) { _y += _x; } virtual float z() { return _x + _y; } protected: float _x, _y; }; class Point3d : public virtual Point2d { public: Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) : Point2d( x, y ), _z( z ) { } ~Point3d() { } float z() { return _z; } protected: float _z; }; int main() { Point2d point2d; Point3d point3d; std::cout << "sizeof( point2d ) = " << sizeof( point2d ) << std::endl; std::cout << "sizeof( point3d ) = " << sizeof( point3d ) << std::endl; return 0; }
class Point2d的虛表:
.weak _ZTV7Point2d .section .rodata._ZTV7Point2d,"aG",@progbits,_ZTV7Point2d,comdat .align 8 .type _ZTV7Point2d, @object .size _ZTV7Point2d, 24 _ZTV7Point2d: # vtable for Point2d .long 0 .long _ZTI7Point2d # typeinfo for Point2d .long _ZN7Point2dD1Ev # Point2d::~Point2d() .long _ZN7Point2dD0Ev # Point2d::~Point2d() .long _ZN7Point2d6mumbleEv # Point2d::mumble() .long _ZN7Point2d1zEv # Point2d::z()
class Point3d的虛表:
.weak _ZTV7Point3d .section .rodata._ZTV7Point3d,"aG",@progbits,_ZTV7Point3d,comdat .align 32 .type _ZTV7Point3d, @object .size _ZTV7Point3d, 60 _ZTV7Point3d: # vtable for Point3d .long 8 .long 0 .long _ZTI7Point3d # typeinfo for Point3d .long _ZN7Point3dD1Ev # Point3d::~Point3d() .long _ZN7Point3dD0Ev # Point3d::~Point3d() .long _ZN7Point3d1zEv # Point3d::z() .long -8 .long 0 .long -8 .long -8 .long _ZTI7Point3d # typeinfo for Point3d .long _ZTv0_n12_N7Point3dD1Ev # virtual thunk to Point3d::~Point3d() .long _ZTv0_n12_N7Point3dD0Ev # virtual thunk to Point3d::~Point3d() .long _ZN7Point2d6mumbleEv # Point2d::mumble() .long _ZTv0_n20_N7Point3d1zEv # virtual thunk to Point3d::z()
雖然Point3d有惟一一個(同時也是最左邊的)base class,也就是Point2d,單Point3d和
Point2d的起始部分並不像」非虛擬的單一繼承「狀況那樣一致。因爲Point2d和Point3d的對象不
再相符,二者之間的轉換也就須要調整this指針。至於在虛擬繼承的狀況下要消除thunks,通常
而言已經被證實是一項該難度技術。
建議是不要在一個virtual base class中聲明nonstatic data members。要否則會愈來愈復
雜。
在下面這組測試中,計算兩個3D點,其中用到一個nonmember friend function,
一個member function,以及一個virtual member function,而且Virtual member
function分別在單1、虛擬、多重繼承三種狀況下執行。
對於nonmember function:
未優化:
優化:
對於inline member:
未優化:
優化:
對於static Member:
未優化:
優化:
對於nonstatic Member:
未優化:
優化:
對於Virtual Member:
未優化:
優化:
對於Virtual Member(多重繼承):
未優化:
優化:
對於Virtual Member(虛擬繼承):
未優化:
優化:
nonmember 、static member或nonstatic member函數都被轉化爲徹底相同的形式。因此三
者效率徹底相同。
virtual member的效率相比前三項下降了4%到11%不等。
多重繼承中的virtual function的調用利用thunk技術用掉了較多成本。
而虛擬繼承花掉了最多的成本。
下面使用兩種方法優化:
1)在函數參數中加上一個對象,用以存放加法的結果:
void Point3d::cross_product( Point3d &pC, const Point3d &pA, const Point3d &pB ) { pC.x = pA.y * pB.z - pA.z * pB.y; pC.y = pA.z * pB.x - pA.x * pB.z; pC.z = pA.x * pB.y - pA.y * pB.x; }
能夠看到在未優化狀況下,效率優化了50%。
2)直接在this對象中計算結果:
void Point3d::cross_product( const Point3d &pB ) { x = y * pB.z - z * pB.y; y = z * pB.x - x * pB.z; z = x * pB.y - y * pB.x; }
取一個nonstatic data member的地址,獲得的結果是該member在class佈局中的
bytes位置(再加1)。能夠想象它是一個不完整的值,它須要被綁定於某個class
object的地址上,纔可以被存取。
取一個nonstatic member function的地址,若是該函數是nonvirtual,獲得的結果是它在內存
中真正的地址。然而這個值也是不徹底的。它也須要被綁定於某個class object的地址上,才能
夠經過它調用該函數。全部的nonstatic member functions都須要對象的地址(以this指出)。
一個指向member function的指針,其聲明語法以下:
double // return type { Point::* // class the function is member pmf } // name of pointer to member (); // argument list
而後咱們能夠這樣定義並初始化該指針:
double( Point::*coord )() = &Point::x;
也能夠這樣指定其值:
coord = &Point::y;
欲調用它,能夠這麼作:
( origin.*coord )();
或
( ptr->*coord )();
這些操做會被編譯器轉化爲:
// 虛擬C++碼 ( coord )( &origin );
和
// 虛擬C++碼 ( coord )( ptr );
指向member function的指針的聲明語法,以及指向」member selection運算符「的指針,其
做用是做爲this指針的空間保存者。這也就是爲何static member functions(沒有this指針)的
類型是」函數指針」,而不是「指向member function的指針」之故。
使用一個「member function指針」,若是並不用於virtual function、多重繼承、virtual base
class等狀況的話,並不會比使用一個「nonmember function指針」的成本高。上述三種狀況對於
「member function指針」的類型以及調用都太過複雜。事實上,對於那些沒有virtual functions、
virtual base class或multiple base classes的classes而言,編譯器能夠爲它們提供相同的效率。
考慮下面的程序片斷:
float ( Point::*pmf )() = &Point::z; Point *ptr = new Point3d;
pmf,一個指向member function的指針,被設置爲Point::z()(一個virtual function)的地
址。ptr則被指定以一個Point3d對象。若是咱們直接經由ptr調用z():
ptr->z();
被調用的是Point3d::z()。但若是咱們從pmf間接調用z()呢?
( ptr->*pmf )();
仍然是Point3d::z()被調用嗎,也就是說,虛擬機制仍然可以在使用「指向member
function之指針」的狀況運行。
對一個nonstatic member function取其地址,將得到該函數在內存中的地址。然而面對一
個virtual function,起地址在編譯時期是未知的,所能知道的僅是virtual function在其相關之
virtual table中的索引值。也就是是說,對一個virtual member function取其地址,所能得到的只
是一個索引值。
例如,假設咱們有如下的Point聲明:
class Point { public: virtual ~Point(); float x(); float y(); virtual float z(); // ... };
而後取destructor的地址:
&Point::~Point;
取x()或y()的地址:
&Point::x(); &Point::y();
獲得的則是函數在內存中的地址,由於它們不是virtual。取z()的地址:
&Point::z();
獲得的結果是2。經過pmf來調用z(),會被內部轉化爲一個編譯時期的式子,通常形式如
下:
( *ptr->vptr[ ( int )pmf ] )( ptr );
對一個「指向member function的指針」評估求值,會由於該值有兩種意義而複雜化:其調
用操做也將有別於常規調用操做。pmf的內部定義,也就是:
float ( Point::*pmf )();
必須容許此函數可以尋址出nonvirtual x()和virtual z()兩個member functions,而那兩個
函數有着相同的原型:
// 二者均可以被指定給pmf float Point::x() { return _x; } float Point::z() { return 0; }
只不過其中一個表明內存地址,另外一個表明virtual table中的索引值。所以,編譯器必
須定義pmf。使它可以(1)持有兩種數值,(2)更重要的是其數值能夠被區別表明內存地址還
是Virtual table中的索引值。
在cfront2.0非正式版中,這兩個值被內含在一個普通的指針內。cfront如何識別該值是
內存地址仍是virtual table索引呢?它使用瞭如下技巧:
( ( ( int )pmf ) & ~127 ) ? // non-virtual invocation ( *pmf )( ptr ) : // virtual invocation ( *ptr->vptr[ ( int )pmf ]( ptr ) );
爲了讓指向member functions的指針也能支持多重繼承和虛擬繼承,Stroustrup設計了下面
一個結構體:
// 通常結構,用以支持 // 在多重繼承之下指向member functions的指針 struct _mptr { int delta; int index; union { ptrtofunc faddr; int v_offset; }; };
index和faddr分別(不一樣時)持有virtual table索引和nonvirtual member function地址(爲
了方便,當index不指向virtual table時,會被設爲-1)。在此模型之下,像這樣的調用操做:
( ptr->*pmf )();
會變成:
( pmf.index < 0 ) ? // non-virtual invocation ( *pmf.faddr )( ptr ) : // virtual invocation ( *ptr->vptr[ pmf.index ]( ptr ) );
此法所受到的批評是,每個調用操做都得付出上述成本,檢查其是否爲virtual或
nonvirtual。Microsoft把這項檢查拿掉,導入一個它所謂的vcall thunk。在此策略執之下,faddr
被指定的要不就是真正的member function地址(若是函數是nonvirtual的話),要不就是vcall
thunk的地址。因而virtual或nonvirtual函數的調用操做透明化,vcall thunk會選出並調用相關
virtual table中的適當slot。
這個結構體的另外一個反作用就是,當傳遞一個不變值的指針給member function時,它需
要產生一個臨時性對象。以下:
extern Point3d foo( const Point3d&, Point3d ( Point3d::* )() ); void bar( const Point3d& p ) { Point3d pt = foo( p, &Point3d::normal ); // ... }
其中&Point3d::normal的值相似這樣:
{ 0, -1, 10727417 }
將須要產生一個臨時性對象,有明確的初值:
// 虛擬C++碼 _mpter temp = { 0, -1, 10727417 } foo( p, temp );
delta字段表示this指針的offset值,而v_offset字段放的是一個virtual(或多重繼承中的第
二或後繼的)base class的vptr位置。若是ptr被編譯器放在class對象的起頭處,這個字段就沒
有必要了,代價則是C對象兼容性下降。這些字段只在多重繼承或虛擬繼承的狀況下才有其必要
性,有許多編譯器在自身內部根據不一樣的classes特性提供多種指向member functions的指針形
式,例如Microsoft就提供了三種風味:
1)一個單一繼承實例(其中持有vcall thunk地址或是函數地址)
2)一個多重繼承實例(其中持有faddr和delta兩個members)
3)一個虛擬繼承實例(其中持有4個members)
下面一組測試中,cross_product()函數經由如下方式調用:
1)一個指向nonmember function的指針;
2)一個指向class member function的指針;
3)一個指向virtual member function的指針;
4)多重繼承下的nonvirtual及virtual member function call;
5)虛擬繼承下的nonvirtual及virtual member function call;
下面是一個加法運算符的可能實現內容:
class Point { friend Point operator+( const Point&, const Point& ); } Point operator+( const Point &lhs, const Point &rhs ) { Point new_pt; new_pt._x = lhs._x + rhs._x; new_pt._y = lhs._y + rhs._y; return new_pt; }
理論上,一個比較「乾淨」的作法是使用inline函數來完成set和get函數:
// void Point::x( float new_ ) { _x = new_x; } // float Point::x() { return _x; } new_pt.x( lhs.x() + rhs.x() );
因爲咱們受限只能在上述兩個函數中對_x直接存取,所以也就將稍後可能發生的data
members的改變所帶來的衝擊最小化了。若是把這些存取函數聲明爲inline,咱們就能夠繼續保
持直接存取members的那種高效率——雖然咱們亦兼顧了函數的封裝性。此外,加法運算符不
再須要被聲明爲Point的一個friend。
然而,實際上咱們並不可以強迫將任何函數都變爲inline。關鍵詞inline只是一項請求。如
果這項請求被接受,編譯器就必須認爲它能夠用一個表達式(expression)合理地將這個函數
擴展開來。
通常而言,處理一個inline函數,有兩個階段:
1)分析函數定義,以決定函數的「intrinsic inline ability」(本質的inline能力)。「instrinsic」
一詞在這裏指「與編譯器相關」。
若是函數因其複雜度,或因其建構問題,被判斷不可成爲inline,它會被轉爲一個static
函數,並在編譯模塊內產生對應的函數定義。
2)真正的inline函數擴展操做是在調用的那一點上。這會帶來參數的求值操做以及臨時對象
的管理。
在inline擴展期間,每個形式參數都會被對應的實際參數取代。通常而言,面對「會帶來副
做用的實際參數」,一般都須要引入臨時性對象。換句話說,若是實際參數是一個常量表達式
(constant expression),咱們能夠在替換以前先完成其求值操做;後繼的inline替換,就能夠把
常量直接「綁」上去。若是既不是常量表達式,也不是個帶有反作用的表達式,那麼就直接帶換
之。
假設有如下的簡單inline函數:
inline int min( int i, int j ) { return i < j ? i : j; }
下面是三個調用操做:
inline int bar() { int minval; int val1 = 1024; int val2 = 2048; /* (1) */ minval = min( val1, val2 ); /* (2) */ minval = min( 1024, 2048 ); /* (3) */ minval = min( foo(), bar()+1 ); return minval; }
標示爲(1)的那一行會被擴展爲:
// (1)參數直接替換 minval = val1 < val2 ? val1 : val2;
標示爲(2)的那一行直接擁抱常量:
// (2) 代換以後,直接擁抱常量 minval = 1024;
表示爲(3)的那一行則引起參數的反作用。它須要導入一個臨時性對象,以免重複求
值:
// (3) 有反作用,因此導入臨時性對象 int t1; int t2; minval = ( t1 = foo() ), ( t2 = bar() + 1 ), t1 < t2 ? t1 : t2;
若是咱們輕微地改變定義,在inline定義中加入一個局部變量,會怎樣?
inline int min( int i, int j ) { int minval = i < j ? i : j; return minval; }
這個局部變量須要什麼額外的支持或處理嗎?若是咱們有如下的調用操做:
{ int local_var; int minval; // ... minval = min( val1, val2 ); }
inline被擴展來後,爲了維護其局部變量,可能會成爲這個樣子
{ int local_var; int minval; // 將inline函數的局部變量處以「mangling」操做 int _min_lv_minval; minval = ( _min_lv_minval = val1 < val2 ? val1 : val2 ), _min_lv_minval; }
通常而言,inline函數中的每個局部變量都必須被放在函數調用的一個封閉區段中,擁有
一個獨一無二的名稱。若是inline函數以單一表達式擴展屢次,則每次擴展都須要本身的一組局
部變量。若是inline函數以分離的多個式子(duscrete statements)被擴展屢次,那麼只須要一
組局部變量,就能夠重複使用。
inline函數中的局部變量,再加上有反作用的參數,可能會致使大量臨時性對象的產生。特
別是若是它以單一表達式被擴展屢次的話。例如:
minval = min( val1, val2 ) + min( foo(), foo() + 1 );
可能被擴展爲:
// 爲局部變量產生臨時變量 int _min_lv_minval_00; int _min_lv_minval_01; // 爲放置反作用值而產生臨時變量 int t1; int t2; minval = ( ( _min_lv_minval_00 = val1 < val2 ? val1 : val2 ), _min_lv_minval_00 ) + ( ( _min_lv_minval_01 = ( t1 = foo() ), ( t2 = foo() + 1 ), t1 < t2 ? t1 : t2 ), _min_lv_minval_01 );
Inline函數對於封裝提供了一種必要的支持,能夠有效存取封裝於class中的nonpublic數
據。它同時也是C程序中大量使用的#define(前置處理宏)的一個安全代替品——特別是若是
宏中的參數有反作用的話。然而一個inline函數若是被調用太屢次的話,會產生大量的擴展碼,
使程序大小暴漲。
對於既要安全又要效率的程序碼,inline函數提供了一個強而有力的工具。然而,與non-
inline函數比起來,它們須要更加當心地處理。