單繼承虛函數例子

class Point
{
public:
virtual ~Point();

virtual Point& mult( float ) = 0;
//
other operations 


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();

// overridden base class virtual functions
Point2d& mult( float );

float y() const
{ return _y; }

//
other operations 

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();

// overridden base class virtual functions
Point3d& mult( float );

float z() const
{ return _z; }

//
other operations 
protected:
float _z;
};
-
Vtable和VPTR結構
虛函數的實現是通過VTable和vptr。每一個帶有虛函數的類都有一個VTable,在編譯器生成,每一個帶有虛函數的類實例都有一個vptr,該類實例vptr指向該類的VTable,在運行期生成。
如圖左部的類實例內存結構,編譯器為之生成__vptr__Point的指針,指向該類的VTable。

VTable的結構是一個函數指針數組,數組的每個元素是一個函數指針,指向該類虛函數的地址。因為基類Point的Point::mult()為純虛函數,因此Point對應的mult函數指針指向一個pure_virtual_called(),拋出調用純虛函數錯誤。
如圖VTable所示,Point類和其子類的析構函數均在VTable[1],mult在VTable[2],y在VTable[3],z在VTable[4]。如果Point2d增加Point2d自己的虛函數,同時Point3d繼承Point2d的虛函數,他們相同的虛函數接口同樣對應于相同的VTable數組下標,如VTable[5],此由編譯器保證,因而編譯器對于虛函數接口能將其轉換為函數指針數組的下標。
故,當調用
ptr->z();
編譯器實際調用的是:
( *ptr->vptr[ 4 ] )( ptr );
從而可以找到ptr實際指向的VTable中的虛函數調用地址。
-
虛函數系統開銷
為了實現虛函數,編譯器產生的操作包括:
-
編譯期,為每一個類增加一個VTable函數指針數組,并使其指向正確的虛函數實現。
-
運行期,在類的構造函數中,為每一個類實例增加一個vptr,指向該類的VTable。
-
編譯器,將虛函數調用編譯為函數指針的調用。
-
運行期,在虛函數調用時,通過指向VTable和調用函數的index,查找函數指針(查找效率為數組隨機訪問,常數時間),調用虛函數。
由分析得,虛函數開銷主要在編譯期的VTable函數指針數組的構造,而運行期的函數指針查找不是性能瓶頸。同時,一個帶虛函數的基類無論有多少個孩子類,并不會降低虛函數性能,而如果類的繼承層次太深,底層類實例的構造函數則需要為類繼承層次的每一層父類初始化vptr,效率降低。
-
虛函數系統性能測試
void
cross_product( const pt3d &pA, const pt3d &pB )


{
pt3d pC;

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;
}

main()
{
pt3d pA( 1.725, 0.875, 0.478 );
pt3d pB( 0.315, 0.317, 0.838 );

for ( int iters = 0; iters < 10000000; iters++ )
cross_product( pA, pB );

return 0;
}
Optimized Non-optimized
Inline Member 0.08 4.70
Nonstatic Member 4.43 6.13
Virtual Member
CC 4.76 6.90
NCC 4.63 7.72
CC和NCC是比較的兩個編譯器版本,對于上述計算函數的測試,虛函數的調用開銷主要是3.4虛表查詢,虛函數調用損失了4% 到11%的運行時間。相對于IO操作,可以忽略。