前奏
有關虛函數的問題層出不窮,有關虛函數的文章千篇一律,那為何還要寫這一篇有關虛函數的文章呢?看完本文後,相信能懂其意義之所在。同時,原狂想曲系列已經更名為程序員編程藝術系列,因為不再只專注於“面試”,而在“編程”之上了。ok,如果有不正之處,望不吝賜教。謝謝。
第一節、一道簡單的虛函數的面試題
題目要求:寫出下面程序的運行結果?
view plaincopy to clipboardprint?
//謝謝董天喆提供的這道百度的面試題
#include <iostream>
using namespace std;
class A
{
public:
virtual void p()
{
cout << "A" << endl;
}
};
class B : public A
{
public:
virtual void p()
{
cout << "B" << endl;
}
};
int main()
{
A * a = new A;
A * b = new B;
a->p();
b->p();
delete a;
delete b;
return 0;
}
//謝謝董天喆提供的這道百度的面試題
#include <iostream>
using namespace std;
class A
{
public:
virtual void p()
{
cout << "A" << endl;
}
};
class B : public A
{
public:
virtual void p()
{
cout << "B" << endl;
}
};
int main()
{
A * a = new A;
A * b = new B;
a->p();
b->p();
delete a;
delete b;
return 0;
}
我想,這道面試題應該是考察虛函數相關知識的相對簡單的一道題目了。然後,希望你碰到此類有關虛函數的面試題,不論其難度是難是易,都能夠舉一反三,那麼本章的目的也就達到了。ok,請跟著我的思路,咱們步步深入(上面程序的輸出結果為A B)。
第二節、有無虛函數的區別
1、當上述程序中的函數p()不是虛函數,那麼程序的運行結果是如何?即如下代碼所示:
class A
{
public:
void p()
{
cout << "A" << endl;
}
};
class B : public A
{
public:
void p()
{
cout << "B" << endl;
}
};
對的,程序此時將輸出兩個A,A。為什麼?
我們知道,在構造一個類的對象時,如果它有基類,那麼首先將構造基類的對象,然後才構造派生類自己的對象。如上,A* a=new A,調用默認構造函數構造基類A對象,然後調用函數p(),a->p();輸出A,這點沒有問題。
然後,A * b = new B;,構造了派生類對象B,B由於是基類A的派生類對象,所以會先構造基類A對象,然後再構造派生類對象,但由於當程序中函數是非虛函數調用時,B類對象對函數p()的調用時在編譯時就已靜態確定了,所以,不論基類指針b最終指向的是基類對象還是派生類對象,只要後面的對象調用的函數不是虛函數,那麼就直接無視,而調用基類A的p()函數。
2、那如果加上虛函數呢?即如最開始的那段程序那樣,程序的輸出結果,將是什麼?
在此之前,我們還得明確以下兩點:
a、通過基類引用或指針調用基類中定義的函數時,我們並不知道執行函數的對象的確切類型,執行函數的對象可能是基類類型的,也可能是派生類型的。
b、如果調用非虛函數,則無論實際對象是什麼類型,都執行基類類型所定義的函數(如上述第1點所述)。如果調用虛函數,則直到運行時才能確定調用哪個函數,運行的虛函數是引用所綁定的或指針所指向的對象所屬類型定義的版本。
根據上述b的觀點,我們知道,如果加上虛函數,如上面這道面試題,
class A
{
public:
virtual void p()
{
cout << "A" << endl;
}
};
class B : public A
{
public:
virtual void p()
{
cout << "B" << endl;
}
};
int main()
{
A * a = new A;
A * b = new B;
a->p();
b->p();
delete a;
delete b;
return 0;
}
那麼程序的輸出結果將是A B。
所以,至此,咱們的這道面試題已經解決。但虛函數的問題,還沒有解決。
第三節、虛函數的原理與本質
我們已經知道,虛(virtual)函數的一般實現模型是:每一個類(class)有一個虛表(virtual table),內含該class之中有作用的虛(virtual)函數的地址,然後每個對象有一個vptr,指向虛表(virtual table)的所在。
請允許我援引自深度探索c++對象模型一書上的一個例子:
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;
};
1、在Point的對象pt中,有兩個東西,一個是數據成員_x,一個是_vptr_Point。其中_vptr_Point指向著virtual table point,而virtual table(虛表)point中存儲著以下東西:
virtual ~Point()被賦值slot 1,
mult() 將被賦值slot 2.
y() is 將被賦值slot 3
z() 將被賦值slot 4.
class Point2d : public Point {
public:
Point2d( float x = 0.0, float y = 0.0 )
: Point( x ), _y( y ) {}
~Point2d(); //1
//改寫base class virtual functions
Point2d& mult( float ); //2
float y() const { return _y; } //3
protected:
float _y;
};
2、在Point2d的對象pt2d中,有三個東西,首先是繼承自基類pt對象的數據成員_x,然後是pt2d對象本身的數據成員_y,最後是_vptr_Point。其中_vptr_Point指向著virtual table point2d。由於Point2d繼承自Point,所以在virtual table point2d中存儲著:改寫了的其中的~Point2d()、Point2d& mult( float )、float y() const,以及未被改寫的Point::z()函數。
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;
};
3、在Point3d的對象pt3d中,則有四個東西,一個是_x,一個是_vptr_Point,一個是_y,一個是_z。其中_vptr_Point指向著virtual table point3d。由於point3d繼承自point2d,所以在virtual table point3d中存儲著:已經改寫了的point3d的~Point3d(),point3d::mult()的函數地址,和z()函數的地址,以及未被改寫的point2d的y()函數地址。
ok,上述1、2、3所有情況的詳情,請參考下圖。
(圖:virtual table(虛表)的布局:單一繼承情況)
本文,日後可能會酌情考慮增補有關內容。ok,更多,可參考深度探索c++對象模型一書第四章。
最近幾章難度都比較小,是考慮到狂想曲有深有淺的原則,後續章節會逐步恢復到相應難度。
第四節、虛函數的布局與匯編層面的考察
ivan、老夢的兩篇文章繼續