虛函數(shù)的講解

1、前言

簡(jiǎn)單地說(shuō),每一個(gè)含有虛函數(shù)(無(wú)論是其本身的,還是繼承而來(lái)的)的類都至少有一個(gè)與之對(duì)應(yīng)的虛函數(shù)表,其中存放著該類所有的虛函數(shù)對(duì)應(yīng)的函數(shù)指針。例:



其中:

B的虛函數(shù)表中存放著B::foo和B::bar兩個(gè)函數(shù)指針。
D的虛函數(shù)表中存放的既有繼承自B的虛函數(shù)B::foo,又有重寫(override)了基類虛函數(shù)B::bar的D::bar,還有新增的虛函數(shù)D::quz。

2、虛函數(shù)表構(gòu)造過(guò)程

從編譯器的角度來(lái)說(shuō),B的虛函數(shù)表很好構(gòu)造,D的虛函數(shù)表構(gòu)造過(guò)程相對(duì)復(fù)雜。下面給出了構(gòu)造D的虛函數(shù)表的一種方式(僅供參考):


3. 虛函數(shù)調(diào)用過(guò)程

以下面的程序?yàn)槔?/p>


編譯器只知道pb是B*類型的指針,并不知道它指向的具體對(duì)象類型 :pb可能指向的是B的對(duì)象,也可能指向的是D的對(duì)象。

但對(duì)于“pb->bar()”,編譯時(shí)能夠確定的是:此處operator->的另一個(gè)參數(shù)是B::bar(因?yàn)閜b是B*類型的,編譯器認(rèn)為bar是B::bar),而B::bar和D::bar在各自虛函數(shù)表中的偏移位置是相等的。

無(wú)論pb指向哪種類型的對(duì)象,只要能夠確定被調(diào)函數(shù)在虛函數(shù)中的偏移值,待運(yùn)行時(shí),能夠確定具體類型,并能找到相應(yīng)vptr了,就能找出真正應(yīng)該調(diào)用的函數(shù)。

3、虛函數(shù)的使用

人們提出這樣的設(shè)想,能否用同一個(gè)調(diào)用形式,既能調(diào)用派生類又能調(diào)用基類的同名函數(shù)。在程序中不是通過(guò)不同的對(duì)象名去調(diào)用不同派生層次中的同名函數(shù),而是通過(guò)指針調(diào)用它們。例如,用同一個(gè)語(yǔ)句“pt->display( );”可以調(diào)用不同派生層次中的display函數(shù),只需在調(diào)用前給指針變量 pt 賦以不同的值(使之指向不同的類對(duì)象)即可。

打個(gè)比方,你要去某一地方辦事,如果乘坐公交車,必須事先確定目的地,然后乘坐能夠到達(dá)目的地的公交車線路。如果改為乘出租車,就簡(jiǎn)單多了,不必查行車路線,因?yàn)槌鲎廛囀裁吹胤蕉寄苋ィ灰谏宪嚭笈R時(shí)告訴司機(jī)要到哪里即可。如果想訪問多個(gè)目的地,只要在到達(dá)一個(gè)目的地后再告訴司機(jī)下一個(gè)目的地即可,顯然,“打的”要比乘公交車 方便。無(wú)論到什么地方去都可以乘同—輛出租車。這就是通過(guò)同一種形式能達(dá)到不同目的的例子。

C++中的虛函數(shù)就是用來(lái)解決這個(gè)問題的。虛函數(shù)的作用是允許在派生類中重新定義與基類同名的函數(shù),并且可以通過(guò)基類指針或引用來(lái)訪問基類和派生類中的同名函數(shù)。

#include <iostream>
#include <string>
using namespace std;
//聲明基類Student
class Student
{
public:
   Student(int, string,float);  //聲明構(gòu)造函數(shù)
   void display( );//聲明輸出函數(shù)
protected:  //受保護(hù)成員,派生類可以訪問
   int num;
   string name;
   float score;
};
//Student類成員函數(shù)的實(shí)現(xiàn)
Student::Student(int n, string nam,float s)//定義構(gòu)造函數(shù)
{
   num=n;
   name=nam;
   score=s;
}
void Student::display( )//定義輸出函數(shù)
{
   cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\n\n";
}
//聲明公用派生類Graduate
class Graduate:public Student
{
public:
   Graduate(int, string, float, float);//聲明構(gòu)造函數(shù)
   void display( );//聲明輸出函數(shù)
private:float pay;
};
// Graduate類成員函數(shù)的實(shí)現(xiàn)
void Graduate::display( )//定義輸出函數(shù)
{
  // 該輸出相比于父類中的輸出多了一個(gè)pay
   cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\npay="<<pay<<endl;
}
Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){}
//主函數(shù)
int main()
{
   Student stud1(1001,"Li",87.5);//定義Student類對(duì)象stud1
   Graduate grad1(2001,"Wang",98.5,563.5);//定義Graduate類對(duì)象grad1
   Student *pt=&stud1;//定義指向基類對(duì)象的指針變量pt
   pt->display( );
   pt=&grad1;
   pt->display( );
   return 0;
}

運(yùn)行結(jié)果如下:

num:1001(stud1的數(shù)據(jù))
name:Li
score:87.5

num:2001 (grad1中基類部分的數(shù)據(jù))
name:wang
score:98.5

假如想輸出grad1的全部數(shù)據(jù)成員,當(dāng)然也可以采用這樣的方法:通過(guò)對(duì)象名調(diào)用display函數(shù),如grad1.display(),或者定義一個(gè)指向Graduate類對(duì)象的指針變量ptr,然后使ptr指向gradl,再用ptr->display()調(diào)用。這當(dāng)然是可以的,但是如果該基類有多個(gè)派生類,每個(gè)派生類又產(chǎn)生新的派生類,形成了同一基類的類族。每個(gè)派生類都有同名函數(shù)display,在程序中要調(diào)用同一類族中不同類的同名函數(shù),就要定義多個(gè)指向各派生類的指針變量。這兩種辦法都不方便,它要求在調(diào)用不同派生類的同名函數(shù)時(shí)采用不同的調(diào)用方式,正如同前面所說(shuō)的那樣,到不同的目的地要乘坐指定的不同的公交車,一一 對(duì)應(yīng),不能搞錯(cuò)。如果能夠用同一種方式去調(diào)用同一類族中不同類的所有的同名函數(shù),那就好了。

用虛函數(shù)就能順利地解決這個(gè)問題。下面對(duì)程序作一點(diǎn)修改,在Student類中聲明display函數(shù)時(shí),在最左面加一個(gè)關(guān)鍵字virtual,即
virtual void display( );
這樣就把Student類的display函數(shù)聲明為虛函數(shù)。程序其他部分都不改動(dòng)。再編譯和運(yùn)行程序,請(qǐng)注意分析運(yùn)行結(jié)果:

num:1001(stud1的數(shù)據(jù))
name:Li
score:87.5

num:2001 (grad1中基類部分的數(shù)據(jù))
name:wang
score:98.5
pay=1200 (這一項(xiàng)以前是沒有的)

看!這就是虛函數(shù)的奇妙作用?,F(xiàn)在用同一個(gè)指針變量(指向基類對(duì)象的指針變量),不但輸出了學(xué)生stud1的全部數(shù)據(jù),而且還輸出了研究生grad1的全部數(shù)據(jù),說(shuō)明已調(diào)用了grad1的display函數(shù)。用同一種調(diào)用形式“pt->display()”,而且pt是同一個(gè)基類指針,可以調(diào)用同一類族中不同類的虛函數(shù)。這就是多態(tài)性,對(duì)同一消息,不同對(duì)象有 不同的響應(yīng)方式。

1、在基類用virtual聲明成員函數(shù)為虛函數(shù)。

這樣就可以在派生類中重新定義此函數(shù),為它賦予新的功能,并能方便地被調(diào)用。在類外定義虛函數(shù)時(shí),不必再加virtual。

2、在派生類中重新定義此函數(shù),要求函數(shù)名、函數(shù)類型、函數(shù)參數(shù)個(gè)數(shù)和類型全部與基類的虛函數(shù)相同,并根據(jù)派生類的需要重新定義函數(shù)體。

C++規(guī)定,當(dāng)一個(gè)成員函數(shù)被聲明為虛函數(shù)后,其派生類中的同名函數(shù)都自動(dòng)成為虛函數(shù)。因此在派生類重新聲明該虛函數(shù)時(shí),可以加virtual,也可以不加,但習(xí)慣上一般在每一層聲明該函數(shù)時(shí)都加virtual,使程序更加清晰。如果在派生類中沒有對(duì)基類的虛函數(shù)重新定義,則派生類簡(jiǎn)單地繼承其直接基類的虛函數(shù)。

3、定義一個(gè)指向基類對(duì)象的指針變量,并使它指向同一類族中需要調(diào)用該函數(shù)的對(duì)象。
通過(guò)該指針變量調(diào)用此虛函數(shù),此時(shí)調(diào)用的就是指針變量指向的對(duì)象的同名函數(shù)。

4、通過(guò)虛函數(shù)與指向基類對(duì)象的指針變量的配合使用,就能方便地調(diào)用同一類族中不同類的同名函數(shù).

只要先用基類指針指向即可。如果指針不斷地指向同一類族中不同類的對(duì)象,就能不斷地調(diào)用這些對(duì)象中的同名函數(shù)。這就如同前面說(shuō)的,不斷地告訴出租車司機(jī)要去的目的地,然后司機(jī)把你送到你要去的地方。

需要說(shuō)明;有時(shí)在基類中定義的非虛函數(shù)會(huì)在派生類中被重新定義(如例12.1中的area函數(shù)),如果用基類指針調(diào)用該成員函數(shù),則系統(tǒng)會(huì)調(diào)用對(duì)象中基類部分的成員函數(shù);如果用派生類指針調(diào)用該成員函數(shù),則系統(tǒng)會(huì)調(diào)用派生類對(duì)象中的成員函數(shù),這并不是多態(tài)性行為(使用的是不同類型的指針),沒有用到虛函數(shù)的功能。

虛函數(shù)表的布局

原文鏈接:https://blog.csdn.net/castle_kao/article/details/71024411

    1. 一個(gè)對(duì)象實(shí)例只有一個(gè)虛函數(shù)表,只有一個(gè)虛基類表。
    1. 對(duì)象的每個(gè)基類都有一個(gè)屬于自己的虛函數(shù)表指針(vfptr)指向虛函數(shù)表(vftbl)的某一項(xiàng),都有一個(gè)屬于自己的虛基類表指針(vbptr)指向虛基類表(vbtbl)的某一項(xiàng)。
    1. 虛函數(shù)表中按照對(duì)象繼承的順序排列對(duì)象的虛函數(shù)地址,虛基類表中按照對(duì)象繼承的順序排列對(duì)象的直接虛繼承類到虛基類的偏移。
    1. 當(dāng)基類無(wú)虛函數(shù),且派生類有獨(dú)立虛函數(shù)時(shí),派生類對(duì)象起始位置為自己的虛函數(shù)表指針。否則派生類的虛函數(shù)會(huì)歸到第一個(gè)帶虛函數(shù)表指針的基類的虛函數(shù)表指向范圍,這樣就節(jié)省了一個(gè)vfptr的空間。
class B
{
public:
    int ib;
    char cb;
public:
    B() :ib(0), cb('B') {}

    virtual void f() { cout << "B::f()" << endl; }
    virtual void Bf() { cout << "B::Bf()" << endl; }
};
class B1 : virtual public B
{
public:
    int ib1;
    char cb1;
public:
    B1() :ib1(11), cb1('1') {}

    virtual void f() { cout << "B1::f()" << endl; }
    virtual void f1() { cout << "B1::f1()" << endl; }
    virtual void Bf1() { cout << "B1::Bf1()" << endl; }


};
class B2 : virtual public B
{
public:
    int ib2;
    char cb2;
public:
    B2() :ib2(12), cb2('2') {}

    virtual void f() { cout << "B2::f()" << endl; }
    virtual void f2() { cout << "B2::f2()" << endl; }
    virtual void Bf2() { cout << "B2::Bf2()" << endl; }

};

class D : public B1, public B2
{
public:
    int id;
    char cd;
public:
    D() :id(100), cd('D') {}

    virtual void f() { cout << "D::f()" << endl; }
    virtual void f1() { cout << "D::f1()" << endl; }
    virtual void f2() { cout << "D::f2()" << endl; }
    virtual void Df() { cout << "D::Df()" << endl; }

};
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 我們知道,在同一類中是不能定義兩個(gè)名字相同、參數(shù)個(gè)數(shù)和類型都相同的函數(shù)的,否則就是“重復(fù)定義”。但是在類的繼承層次...
    踩在浪花上00閱讀 531評(píng)論 0 1
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,683評(píng)論 1 51
  • 3. 類設(shè)計(jì)者工具 3.1 拷貝控制 五種函數(shù)拷貝構(gòu)造函數(shù)拷貝賦值運(yùn)算符移動(dòng)構(gòu)造函數(shù)移動(dòng)賦值運(yùn)算符析構(gòu)函數(shù)拷貝和移...
    王偵閱讀 2,074評(píng)論 0 1
  • 不想撒謊 卻讓母親難過(guò)了 所有的不利 沒有一個(gè)方向 全都落井下石 要不要試一試 認(rèn)為可行的 全都成了嘲笑 何不置之...
    曾在天涯up閱讀 77評(píng)論 0 0
  • 除夕的由來(lái):除夕是春節(jié)的前夜,又叫年三十。有一種傳說(shuō):是古時(shí)候有個(gè)兇惡的怪獸叫夕,每到歲末便出來(lái)害人,后來(lái)人們知道...
    拜泉0458田慶虹閱讀 136評(píng)論 1 2

友情鏈接更多精彩內(nèi)容