static member functions不可能作到的兩點:(1)直接存取nonstatic數據,(2)被聲明爲const的。程序員
C++的設計準則之一就是:nonstatic member function至少必須和通常的nonmember function有相同的效率。好比,要在下面兩個函數之間做選擇:express
float magnitude3d(const Point3d *_this){ ... } float Point3d::magnitude3d() const { ... }
選擇member function不該該帶來什麼額外負擔。這是由於編譯器內部已將「member函數實例」轉換爲對等的「nonmember函數實例」安全
下面是magnitude()的一個nonmember定義:ide
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的形式。 轉化步驟以下:函數
通常而言,member的名稱前面會加上class的名稱,造成獨一無二的命名。例以下面的聲明:佈局
class Bar{ public: int ival; ... };
其中的ival
有可能變成這樣:this
//member通過name-mangling以後的可能結果之一 ival_3Bar
爲何編譯器要這麼作?請考慮以下派生操做:編碼
class Foo : public Bar{ public: int ival; ... }
記住,Foo對象內部結合了base class和derived class二者:lua
//Foo的內部描述 class Foo{ public: int ival_3Bar; int ival_3Foo; ... };
無論你要處理哪個ival
,經過"name mangling",均可以絕對清楚地指出來。因爲member function能夠被重載化(overload),因此須要更普遍的mangling手法,以提供絕對獨一無二的名稱。設計
把參數和函數名稱編碼在一塊兒,編譯器是在不一樣的編譯模塊之間達成了一種有限形式的類型檢驗。舉例以下,若是一個print函數被這樣定義:
void print(const Point3d& ){ ... }
但意外地被這樣聲明和調用:
//覺得是const Point3d& void print(const Point3d );
兩個實體若是擁有獨一無二的name mangling,那麼任何不正確的調用操做在連接時期就因沒法決議(resolved)而失敗。有時候咱們能夠樂觀地稱此爲「確保類型安全的連接行爲」(type-safe linkage)。我說「樂觀地」是由於它能夠捕捉函數標記(signature,亦即函數名稱+參數數目 + 參數類型)錯誤;若是「返回類型」聲明錯誤,就沒有辦法檢查出來。
若是normalize()是一個virtual member function,那麼如下調用:
ptr->normalize();
將會被內部轉化爲:
( *ptr->vptr[1])(ptr);
其中:
若是Point3d::normalize()是一個static member function,如下兩個調用操做:
obj.normalize(); ptr->normalize();
將被轉換爲通常的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++ 語言到當前爲止並不能識別這種狀況。
static member functions的主要特性是它沒有this指針。如下的次要特性通通根源於其主要特性:
若是取一個static member function的地址,得到的將是其在內存中的位置,也就是其地址。因爲static member function沒有this指針,因此其地址的類型並非一個「指向class member of function的指針」,而是一個「nonmember函數指針」。
virtual function的通常實現模型:每個class有一個virtual table,內含該class之中有做用的virtual function的地址,而後每一個object有一個vptr,指向virtual table的所在。
爲了支持virtual function機制,必須首先可以對於多態對象有某種形式的「執行期類型判斷法(runtime type resolution)」。也就是說,如下的調用操做將須要ptr在執行期的某些相關信息,
ptr->z();
如此一來纔可以找到並調用z()的適當實體。
在C++中,多態(polymorphism)表示「以一個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)了。以下:
//積極多態的常見例子 ptr->z();
在runtime type identification(RTTI)性質於1993年被引入C++ 語言以前,C++ 對「積極多態」的惟一支持,就是對virtual function call的決議(resolution)操做。有了RTTI,就可以在執行期查詢一個多態的指針或多態的reference了
//積極多態的第二個例子 if(Point3d *p3d = dynamic_cast<Point3d*>(ptr)) return p3d->_z;
在實現上,能夠在每個多態對象的class object身上增長兩個members:
表格中的virtual functions地址如何被構建起來?在C++ 中,virtual function(可經由其class object被調用)能夠在編譯時期獲知。此外,這一組地址是固定不變的。執行期不可能新增或替換之。因爲程序執行時,表格的大小和內容都不會改變,因此其建構和存取皆能夠由編譯器徹底掌控,不須要執行期的任何介入。
一個class只會有一個virtual table,每個table內含其對應的class object中全部active virtual functions函數實例的地址。這些active virtual functions包括:
每個virtual function都被指派一個固定的索引值,這個索引在整個繼承體系中保持與特定的virtual function的關係。以下的Point class體系中:
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; };
vitual destructo被賦值slot 1。而mult()被賦值slot 2。mult()並無函數定義(由於它是一個pure virtual function),因此pure_virtual_calssed()的函數地址會被放在slot 2中,若是該函數意外地被調用,一般的操做是結束掉這個程序。y()被賦值slot 3而z()被賦值slot 4。下圖爲Point的內存佈局和virtual table。
當一個class派生自Point時,會發生什麼事情?
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; };
一共有三種可能性:
相似的狀況以下,Point3d派生自Point2d:
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; };
其virtual table中的slot 1 放置Point3d destructor,slot 2放置Point3d::mult()。slot 3放置繼承自Point2d的y()函數地址,slot 4放置本身的z() 函數地址。Point2d,Point3d的對象佈局和virtual table以下:
在一個單一繼承體系中,virtual function機制的行爲十分良好,不但有效率並且很容易塑造出模型來。可是在多重繼承和虛擬繼承中,對virtual function的支持就沒有那麼美好了。
在多重繼承中支持virtual functions,其複雜讀圍繞在第二個及後繼的base classes身上,以及「必須在執行期調整this指針」這一點。如下面的class體系爲例:
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; };
Derived 支持virtual functions的困難度,通通落在Base2 subobject身上。有三個問題須要解決,以此而言分別是(1)virtual destructor,(2)被繼承下來的Base2::mumble(),(3)一組clone()函數實體。
首先,把一個從heap中配置而得的Derived對象的地址,指定給一個Base2指針:
Base2 *pbase2 = new Derived;
新的Derived對象的地址必須調整,以指向其Base2 subobject。編譯時期會產生如下的碼:
//轉移以支持第二個base class Derived *tmp = new Derived; Base2 *pbase2 = tmp ? tmp + 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。如:
Base2 *pbase2 = new Derived; ... delete pbase2; //invoke derived class's destructor(virtual)
該調用操做所連帶的「必要的this指針調整」操做,必須在執行期完成。也就是說,offset的大小,以及把offset加到this指針上頭的那一小段程序代碼,必須經由編譯器在某個地方插入。
比較有效率的解決辦法是利用所謂的thunk。所謂thunk是以小段assembly代碼,用來(1)以適當的offset值調整this指針,(2)調到virtual function去。例如,經由一個Base2指針用Derived destructor,其相關的thunk可能看起來是這個樣子的:
pbase2_dtor_thunk: this += sizeof(base1); Derived::~Derived(this);
Thunk技術容許virtual table slot繼續內含一個簡單的指針,所以多重繼承不須要任何空間上的額外負擔。Slots中的地址能夠直接指向virtual function,也能夠指向一個相關的thunk(若是須要調整this指針的話)。因而,對於那些不須要調整this指針的virtual function而言,也就不需承載效率上的額外負擔。
調整this指針的第二個額外負擔就是,因爲兩個不一樣的可能:(1)經由derived class(或第一個base class)調用,(2)經由第二個(或其後繼)base class調用,同一函數在virtual table中可能須要多筆對應的slots。如:
Base1 *pbase1 = new Derived; Base2 *pbase2 = new Derived; delete pbase1; delete pabse2;
雖然兩個delete操做致使相同的Derived destructor,但它們須要兩個不一樣的virtual table slots:
在多重繼承之下,一個derived class內含n-1個額外的virtual tables,n表示其上一層base classes的個數(所以,單一繼承將不會有額外的virtual tables)。
針對每個virtual tables,Derived對象中有對應的vptr。下圖說明了這點,vptrs將在constructor(s)中被設定初值(經由編譯器所產生的碼)
用以支持「一個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。
因爲執行期連接器(runtime linkers)的降臨(能夠支持動態共享函數庫),符號名稱的連接變得很是緩慢。爲了調節執行期連接器的效率,Sun編譯器將多個virtual tables連鎖爲一個;指向次要表格的指針,可由主要表格名稱加上一個offset得到。在這樣的策略下,每個class只有一個具名的virtual table。
有如下三種狀況,第二或後繼的base class會影響對virtual functions的支持。
Base2 *ptr = new Derived; //調用Derived::~Derived //ptr必須向後調整sizeof(Base1)個bytes delete ptr;
從上面那個圖能夠看到這個調用操做的重點:ptr指向Derived對象中的Base2 subobject;爲了可以正確執行,ptr必須調整指向Derived對象的起始處。
Derived *pder = new Derived; //調用Base2::mumble() //pder必須被向前調整sizeof(Base1)個bytes pder->mumble();
Base2 *pb1 = new Derived; //調用Derived * Derived::clone() //返回值必須被調整,指向Base2 subobject Base2 *pb2 = pb1->clone();
當進行pb1->clone()時,pb1會被調整指向Derived對象的起始地址,因而clone()的Derived版會被調用;它會傳回一個指針,指向一個新的Derived對象,該對象的地址在被指定給pb2以前,必須先通過調整,以指向Base2 subobject。
考慮下面的virtual base class派生體系,從Point2d派生出Point3d:
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; };
其內存佈局以下圖:
當一個virtual base class從另外一個virtual base class派生而來,而且二者都支持virtual functions和nonstatic data members時,編譯器對於virtual base class的支持簡直就像進了迷宮同樣。建議不要在一個virtual base class中聲明nonstatic data members。
取一個nonstatic data member的地址,獲得的結果是該member在class佈局中bytes位置(再加1)。能夠想象,它是一個不完整的值,它須要被綁定於某個class object的地址上,纔可以被存取
取一個nonstatic data member的地址,若是該函數是nonvirtual,獲得的結果是它在內存中真正的地址。然而這個值也是不徹底的。它也須要被綁定於某個class object的地址上,纔可以經過它調用該函數。全部的nonstatic member functions都須要對象的地址(以參數this指出)
一個指向member fucntion的指針,其聲明語法以下:
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->*corrd)();
這些操做會被編譯器轉化爲:
(coord)(&origin); 和 (coord)(ptr);
指向member function的指針的聲明語法,以及指向「member selection運算符」的指針,其做用是做爲this指針的空間保留着。這也就是爲何static member functions(沒有this指針)的類型是「函數指針」,而不是「指向member function的指針」之故。
使用一個「member function指針」,若是並不用於virtual function、多重繼承、virtual base class等狀況的話,並不會比使用一個「nonmember function指針」的成本高
注意下面的程序片斷:
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)();
也就是說,虛擬機制仍然可以在使用"指向memeber 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
獲得的結果是 1, 取x()或y()的地址:
&Point::x(); &Point::y();
獲得的則是函數在內存中的地址,由於它們不是virtual。取z()的地址:
&Point::z();
獲得的結果是2。經過pmf來調用z(),會被內部轉化爲一個編譯時期的式子,通常形式以下:
(*ptr->vptr[(int)pmf])(ptr);
對一個「指向member function的指針」評估求值,會由於改制有兩種意義而複雜化;其調用操做也將有別於常規的調用操做。集編譯器必須定義函數指針使它可以(1)含有兩種數值,(2)更重要的是其數值能夠被區別表明內存地址仍是virtual table中的索引值。
爲了讓指向member functions的指針也可以支持多重繼承和虛擬繼承,Stroustrup設計了下面一個結構體:
//通常結構,用以支持在多重繼承之下指向member functions的指針 struct _mptr{ int delta; int index; union{ protofunc 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。
delta字段表示this指針的offset值,而v_offset字段放的是一個virtual(或多重繼承中的第二或後繼的)base class的vptr位置。若是vptr被編譯器放在class對象的起頭處。這個字段就沒有必要了,代價則是C對象兼容性下降。這些字段只在多重繼承或虛擬繼承的狀況下才有其必要性,有許多編譯器在自身內部根據不一樣的classes特性提供多種指向member functions的指針形式,例如Microsoft就供應了三種風味:
通常而言,處理一個inline函數,有兩個階段:
若是函數因其複雜度,或因其建構問題,被判斷不可成爲inline,它會被轉爲一個static函數,並在「被編譯模塊」內產生對應的函數語義。
一樣在擴展點上,編譯器將決定這個調用是否「不可爲inline」。
通常而言,面對「會帶來反作用的實際參數」,一般都須要引入臨時性對象。換句話說,若是實際參數時一個常量表達式(constant expression),咱們能夠在替換以前先完成其求值操做(evaluations);後繼的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)的那一行會被擴展爲:
minval = val1 < val2 ? val1 : val2;
標識爲(2)的那一行直接擁抱常量:
minval = 1024;
標識爲(3)的那一行則引起參數的反作用,它須要導入一個臨時對象,以免重複求值(multiple evaluations)
int t1; int t2; minval = (t1 = foo()), (t2 = bar() + 1), t1 < t2 ? t1 : t2;
通常而言,inline函數中的每個局部變量都必須被放在函數調用的一個封閉區段中,擁有一個獨一無二的名稱。若是inline函數以單一表達式(expression)擴展屢次,則每次擴展都須要本身的一組局部變量。若是inline函數以分離的多個式子(discrete statements)被擴展屢次,那麼只需一組局部變量,就能夠重複使用(譯註:由於它們被放在一個封閉區段中,有本身的scope)