/*
*晚上花了幾個小時翻譯了下,第一次翻譯這么長的文字;挺累呀,翻譯的很多地方也不算通順,權當自娛自樂了。
*版權所有 xt2120#gmail 謝絕轉載
*/
c++ 虛函數 原理 機制 c 虛函數表 表指針
上個月,我介紹了虛擬函數。我概述了如何使用虛擬函數來實現一個設備無關的文件系統,并詳細描述了如何創建一個具有多態行為的幾何圖形類。這個月我將繼續解釋虛擬函數的工作機制。首先,扼要重復一下其中的關鍵概念。
在c++中在基礎類和基類之間的公共繼承定義了一個is-a的關系。這就是說,給出了定義:
class D : public B {...}
D從B公共派生而來,所以任何D對象同時也是一個B對象。一個接收B對象指針或引用作為形式參數的函數也會允許一個指向D對象的指針(或引用,相應的)作為參數。更一般的情況,將一個指向D對象的指針轉換為一個指向B對象的指針是一個標準轉換而不需要cast。例如,如果d是一個類D的對象,B是D的公共繼承類,你可以寫下如下代碼:
B *pb = &d;
這會將&d(一個 typde D*表達式)轉換為B*.將一個D對象綁定到B&上也是一個標準轉換(B & rb = d;)
which converts &d (an expression of type D * ) to B * . Binding a D object to a B & is also a standard conversion.當討論指向基類和繼承類對象的指針行為時,它將能夠幫助你區分一個對象的動態類型和靜態類型。一個對象的靜態類型是用于引用那個對象的表達式。一個對象的動態類型是其“實際的”類型--當指針被創建時對象的類型。
Listing 3 Class circle derived form shape
比如,使用上面定義和初始化的pb,*pb的靜態類型為B,但是其動態類型為D(B* pb = &d;)。或者考慮:
B &rb = d;
rb的靜態類型為B同時其動態類型為D。*pb的靜態類型總是B,但是其動態類型在程序執行的時候可能會發生變化。例如,如果b是一個B對象,那么
pb = &b;
會改變*pb的動態類型為B。
一個派生類繼承了其基類的所有成員。一個派生類不能丟棄任何其所繼承的成員,但是它能夠使用覆寫(override)一個繼承的成員函數。
在c++中,一個非靜態成員函數默認是非虛擬函數。c++通過靜態綁定來解析一個非虛函數調用。也就是說,如果pb被聲明為B*,并且B有一個非虛函數,那么pb->f總是調用B的函數f。即使在調用時pb實際指向的是一個D對象(此中D從B派生而來并且覆寫了函數),那么調用pb->f仍舊調用的是隸屬于B的函數f,而不是D的f。
從另一方面來說,虛擬成員函數調用是動態綁定的。如果pb是B*指針并且f在中被聲明為一個虛擬成員函數,那么pb->f所實際調用的函數取決于*pb的動態類型。這樣的話,如果pb實際指向的是B對象,那么pb->f調用的隸屬于B的f。而如果pb實際是想一個D對象(D由B派生而來并且覆寫了函數f),那么pb->f調用的是D的f。
一個至少有一個虛擬成員函數的類被稱作多態類型,這樣類型的對象們展示了多態性。多態能讓你為邏輯上相似但實體行為不同的子類型繼承體系定義統一的接口。通過使用多態,你能夠將一個派生類對象指針或引用傳遞給只知其基類型對象的函數。對象仍將保持其動態類型以使得成員函數調用能夠施行派生類的行為。
listing.1
class shape
{
public:
enum palette { BLUE, GREEN, RED };
shape(palette c);
virtual double area() const;
virtual const char *name() const;
virtual ostream &put(ostream &os) const;
palette color() const;
private:
palette_color;
static const char *color_image[RED - BLUE + 1];
};
inline ostream &operator<<(ostream &os, const shape &s)
{
return s.put(os);
}
// End of File
Listing 1顯示了shape類定義,一個多態的幾何類。
Listing 2 Member function and static member data definitions for class shape
shape::shape(palette c) : _color(c) { }
shape::palette shape::color( ) const
{
return _color;
}
double shape::area() const
{
return 0;
}
const char *shape::name() const
{
return "point";
}
ostream &shape::put(ostream &os) const
{
return os << color_image[_color] << '' << name();
}
const char *shape::color_image[shape::RED - shape::BLUE + 1] =
{ "blue", "green", "red" };
// End of File
Listing 2顯示了相應的成員函數和靜態成員函數定義。Class shape有三個虛擬函數,area,name 和put,兩個非虛成員函數,color和shape(一個ctor)。
class circle : public shape
{
public:
circle(palette c, double r);
double area() const;
const char *name() const;
ostream &put(ostream &os) const;
private:
double radius;
};
circle::circle(palette c, double r) : shape(c), radius(r) { }
double circle::area() const
{
const double pi = 3.1415926;
return pi * radius * radius;
}
const char *circle::name() const
{
return "circle";
}
ostream &circle::put(ostream &os) const
{
return shape::put(os) << "with radius = " << radius;
}
// End of File
Listing 4 Class rectangle derived from shape
class rectangle : public shape
{
public:
rectangle(palette c, double h, double w);
double area() const;
const char *name() const;
ostream &put(ostream &os) const;
private:
double height, width;
};
rectangle::rectangle(palette c, double h, double w)
: shape(c), height(h), width(w) { }
double rectangle::area() const
{
return height * width;
}
const char *rectangle::name() const
{
return "rectangle";
}
ostream &rectangle::put(ostream &os) const
{
return shape::put(os) << " with height = " << height
<< " and width = " << width;
}
// End of File
Listing 3 和Listing 4相應的展示了類circle 和 rectangle完整定義,這兩個類都從shape派生而來。每一個派生類定義了自己的構造函數,并且使用合適的定義將各自繼承而來的虛函數覆寫了。
Listing 5 A function that returns the shape with the largest area in a collection of shapes
const shape *largest(const shape *sa[], size_t n)
{
const shape *s = 0;
double m = 0;
double a;
for (size_t i = 0; i < n; ++i)
if ((a = sa[i]->area()) > m)
{
m = a;
s = sa[i];
}
return s;
}
// End of File
Listing 5包含了一個函數展示了多態的威力。函數largest從一個shapes集合中找到具有最大面積的shape。既然shape是多態類型,那么調用sa[i]->area不用調用者準確知曉*sa[i]實際的shape類型就能返回其面積。
vptrs and vtbls
ARM(Ellis)和新興的c++標準都在竭力描述虛函數的行為特征,就如同其所描述的C++語言的其他部分,而沒有沒有建議具體的實現策略。但是,ARM里面在第十章的結尾派生類的評注中確實描述了實現技術。我覺得仰仗一個模型來實現簡化了虛擬函數習性的細節描述。下面就是這樣的一個模型。
典型的c++實現里為每一個多態類的對象增加了一個指針。這個指針被稱作vptr。不論何時一個多態類的構造函數初始化一個對象時,它都會將對象的vptr設置為一個叫做vtbl的函數指針表的地址。vtbl中的每一個條目都一個虛函數的地址。一個既定類的所有對象都共享同樣的vtbl;這個vtbl包含了包含了該類中的每一個虛函數的入口地址。
Figure 1 Layout of class shape
![]()
例如,上圖顯示了類shape的一個對象的布局(在listing 1中定義的)和其相應的vtbl。每一個shape對象都擁有同樣順序的兩個值域:vptr和一個_colo 成員數據。vptr指向了shape的vtbl,包含了shape虛函數的地址(shape::area,shape::name和shape::put)。非虛函數shape::color和shape::shape(構造函數)不會在vtbl中占用任何空間,也不會占用對象本身的任何空間。Figure 2 Layout of class circle
![]()
Figure 3 Layout of class rectangle
![]()
圖2和圖3相應顯示了circle和rectangle對象及其相關的vtbl的布局。注意到circle和reatangle對象起始一部分是shape對象,所以一個指向circle或者rectangle的指針同時也是一個指向shape的指針,同時從一個circle *或者ractangle*轉換成shape* 不需要指針計算。兩個派生類的vtbls和基類的vtbl擁有同樣的函數指針序,盡管指針值是不同的。例如,area函數的vtbl入口在每一個從shape派生而來的類中都排在第一位。name的vtbl入口總在第二,put的入口序總為3。
然而一個非虛函數調用所產生的調用指令直接指向了在轉換中(編譯和鏈接)就已確定的地址,一個虛函數調用產生的額外的代碼以定位vtbl中的函數地址。
ARM一書中建議將vtbl看作函數地址的一個隊列,以使得每一次對被調用函數的定位能夠vtbl的下標來定位。比如,如果ps只一個指向shape的指針,那么
a = ps->area();
會被轉換成如下這個樣子:
a = (*(ps->vtbl[0]))(ps);
同樣
ps->put(cout);
會被轉換成
(*(ps->vtbl[2]))(ps,cout);
形如ps->vtbl[n]的表達式就表示了*ps對象vtbl的入口,所以 (*(ps->vtbl [n])) 就是第nth虛擬函數自身了。實際上,如同在c語言里一樣,你不需要在調用表述里顯式提領一個函數指針,你可以將
(*(ps->vtbl[2])) (ps, cout);
簡化成
(ps->vtbl[2])(ps, cout);
每一個虛函數可能會有不同的簽名式(形式參數類型次序)和返回類型返回類型。所以嚴格來講,你不能將vtbls實現成函數數組,因為數組需要其所有類型都具有同樣的類型。比如, shape::area 類型為 double (*)() , shape::put 類型為 void (*)(ostream &) .我寧愿將vtbl模塑為一個結構,其內所有的成員都是指向函數的指針。具體來講,你可以為shape的vtbl定義如下的結構類型
struct shape_virtual_table
{
double (*area)();
const char *(*name)();
ostream &(*put)(ostream &os);
};
并且定義實際的shape vtbl如下:
shape_virtual_table shape_vtbl =
{
&shape::area,
&shape::name,
&shape::put
};
與此類似,你能夠如下定義circle vtbl:
shape_virtual_table circle_vtbl =
{
&circle::area,
&circle::name,
&circle::put
};
(我說“類似這樣”,因為這樣的代碼實際通不過編譯,這樣的代碼只是演示下vtbl的通常的布局)使用這樣的轉換模式,
派生類或多或少或不覆寫基類中的虛函數。一個派生類繼承了其所未覆寫的虛函數的定義。Listing 6 和 圖4共同例示了選擇性地覆蓋基類中的虛函數的效果。
a = ps->area();
轉換成
a = (*ps->vtbl->area)(ps);
或簡化版
a = ps->vtbl->area(ps);
同樣
ps->put(cout);
轉換成
(*ps->vtbl->put)(ps, cout);
就是
ps->vtbl->put(ps, cout);
一次具有n個參數的虛擬函數調用將被轉換成具有n+1個參數的調用(通過vtbl入口)。增加的那個參數就是被應用函數的對象的地址;在上例中,其值總是ps。在被調用函數中,這個額外的參數就成了this的值。虛函數不能為靜態成員,所以他們總是隱式的有一個this參數。
記住我所描述的只是一種典型的實現策略。c++ translators 可能在實現虛函數時并不一致,但是效果是一樣的。vptr并不需要在每個多態對象的開頭。但是,對于任何從多態類型B派生而來的類D而言,D的vptr在D中的偏移量和B的vptr在B中的偏移量一致。類似地,vtbl中的函數指針的順序也并不一定和類中聲明的順序一致,但對于任何任何從多態類B派生而來的D,D的vtbl的初始部分必須和B的vtbl的布局一致,即便因D已經覆寫了某些所繼承的虛函數而造成D的實際的指針值與B的不同。
簡而言之,一個C++ TRANSLATOR必須確保派生對象的基子對象與任何基類型對象的布局一致,并且派生類型vtbl基portion部分和基類對象的vtbl一致。因此,當translator轉換一個虛函數調用時不需要預見任何派生類的聲明。不考慮p的動態類型,一個如p->f這樣的虛函數調用總是轉成這樣的代碼
構造f的實際實際參數列表
循p的vtpr到一個vtbl
將控制權移交給vtbl中相應指向f的入口地址。
所有既定多態類型的多態對象能夠共享共同的vtbl實體。一些c++成功剔除了vtbls的重復。另一些產生多酚vtbls的拷貝,由于開發環境所限或者為提供更好的系統性能。許多實現提供了編譯和鏈接選項讓你自行作出決定。
這個實現模型展示了虛函數導入了小小的時間和空間上的代價:
為一個已有虛函數的類增加了一個或多個虛函數并沒有為該類的每一個對象增加一個vptr。
每一個多態類增加了至少多增加了一個vtbl到程序數據區。
多態類的每一個構造函數必須初始化vptr
每一個虛函數調用必須定位通過查詢vtbl以定位函數地址(通常需要2-4條額外的機器指令)
在c++中,成員函數默認為非虛的,因為c++堅持原則“不為不使用的部分付出代價”。如果你樂意承擔虛函數調用的代價,你得明確的說出來。
選擇性的覆寫
Listing 6 Selective virtual overriding
#include <iostream.h>
class B
{
public:
virtual void f();
virtual void g();
virtual void h();
};
class C : public B
{
public:
void f(); // virtual
};
class D : public C
{
public:
void h(); // virtual
};
void B::f()
{
cout << "B::f()/n";
}
void B::g()
{
cout << "B::g()/n";
}
void B::h()
{
cout << "B::h()/n";
}
void C::f()
{
cout << "C::f()/n";
}
void D::h()
{
cout << "D::h()/n";
}
int main()
{
C c;
D d;
B *pb = &c; // ok, &c is a C * which is a B *
pb->f(); // calls C::f()
pb->g(); // calls B::g()
pb->h(); // calls B::h()
C *pc = &d; // ok, &d is a D * which is a C *
pc->f(); // calls C::f()
pc->g(); // calls B::g()
pc->h(); // calls D::h()
B &rb = *pc; // ok, *pc is a C which is a B
rb.f(); // calls C::f()
rb.g(); // calls B::g()
rb.h(); // calls D::h()
return 0;
}
// End of File
Figure 4 Selectively overriding only some virtual functions
![]()
listing 6 顯示了一個簡單的class 繼承體系,圖4顯示了相應的vtbls。類B定義了三個虛函數f,g和h。由B派生而來的C只覆寫了函數f,所以C的vtbl中的g和h的入口仍舊是B的g和h。由C派生的類D只覆寫了函數h,所以D的vtbl中的f和g的入口和C的vtbl中的一致。既然C和D都沒有覆寫g,所有三個;類的vtbl對于g的入口都具有同樣的值,也就是B'g。
在Listing 6中,pc有一個靜態類型C*.但是當程序執行到這句前
B &rb = *pc;
pc的動態類型為D*。所以所有應用到rb上的調用都使用D的vtbl。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
