虚函数的讲解

1、前言

简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。例:



其中:

B的虚函数表中存放着B::foo和B::bar两个函数指针。
D的虚函数表中存放的既有继承自B的虚函数B::foo,又有重写(override)了基类虚函数B::bar的D::bar,还有新增的虚函数D::quz。

2、虚函数表构造过程

从编译器的角度来说,B的虚函数表很好构造,D的虚函数表构造过程相对复杂。下面给出了构造D的虚函数表的一种方式(仅供参考):


3. 虚函数调用过程

以下面的程序为例:



编译器只知道pb是B*类型的指针,并不知道它指向的具体对象类型 :pb可能指向的是B的对象,也可能指向的是D的对象。

但对于“pb->bar()”,编译时能够确定的是:此处operator->的另一个参数是B::bar(因为pb是B*类型的,编译器认为bar是B::bar),而B::bar和D::bar在各自虚函数表中的偏移位置是相等的。

无论pb指向哪种类型的对象,只要能够确定被调函数在虚函数中的偏移值,待运行时,能够确定具体类型,并能找到相应vptr了,就能找出真正应该调用的函数。

3、虚函数的使用

人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。例如,用同一个语句“pt->display( );”可以调用不同派生层次中的display函数,只需在调用前给指针变量 pt 赋以不同的值(使之指向不同的类对象)即可。

打个比方,你要去某一地方办事,如果乘坐公交车,必须事先确定目的地,然后乘坐能够到达目的地的公交车线路。如果改为乘出租车,就简单多了,不必查行车路线,因为出租车什么地方都能去,只要在上车后临时告诉司机要到哪里即可。如果想访问多个目的地,只要在到达一个目的地后再告诉司机下一个目的地即可,显然,“打的”要比乘公交车 方便。无论到什么地方去都可以乘同—辆出租车。这就是通过同一种形式能达到不同目的的例子。

C++中的虚函数就是用来解决这个问题的。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

#include <iostream>
#include <string>
using namespace std;
//声明基类Student
class Student
{
public:
   Student(int, string,float);  //声明构造函数
   void display( );//声明输出函数
protected:  //受保护成员,派生类可以访问
   int num;
   string name;
   float score;
};
//Student类成员函数的实现
Student::Student(int n, string nam,float s)//定义构造函数
{
   num=n;
   name=nam;
   score=s;
}
void Student::display( )//定义输出函数
{
   cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\n\n";
}
//声明公用派生类Graduate
class Graduate:public Student
{
public:
   Graduate(int, string, float, float);//声明构造函数
   void display( );//声明输出函数
private:float pay;
};
// Graduate类成员函数的实现
void Graduate::display( )//定义输出函数
{
  // 该输出相比于父类中的输出多了一个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){}
//主函数
int main()
{
   Student stud1(1001,"Li",87.5);//定义Student类对象stud1
   Graduate grad1(2001,"Wang",98.5,563.5);//定义Graduate类对象grad1
   Student *pt=&stud1;//定义指向基类对象的指针变量pt
   pt->display( );
   pt=&grad1;
   pt->display( );
   return 0;
}

运行结果如下:

num:1001(stud1的数据)
name:Li
score:87.5

num:2001 (grad1中基类部分的数据)
name:wang
score:98.5

假如想输出grad1的全部数据成员,当然也可以采用这样的方法:通过对象名调用display函数,如grad1.display(),或者定义一个指向Graduate类对象的指针变量ptr,然后使ptr指向gradl,再用ptr->display()调用。这当然是可以的,但是如果该基类有多个派生类,每个派生类又产生新的派生类,形成了同一基类的类族。每个派生类都有同名函数display,在程序中要调用同一类族中不同类的同名函数,就要定义多个指向各派生类的指针变量。这两种办法都不方便,它要求在调用不同派生类的同名函数时采用不同的调用方式,正如同前面所说的那样,到不同的目的地要乘坐指定的不同的公交车,一一 对应,不能搞错。如果能够用同一种方式去调用同一类族中不同类的所有的同名函数,那就好了。

用虚函数就能顺利地解决这个问题。下面对程序作一点修改,在Student类中声明display函数时,在最左面加一个关键字virtual,即
virtual void display( );
这样就把Student类的display函数声明为虚函数。程序其他部分都不改动。再编译和运行程序,请注意分析运行结果:

num:1001(stud1的数据)
name:Li
score:87.5

num:2001 (grad1中基类部分的数据)
name:wang
score:98.5
pay=1200 (这一项以前是没有的)

看!这就是虚函数的奇妙作用。现在用同一个指针变量(指向基类对象的指针变量),不但输出了学生stud1的全部数据,而且还输出了研究生grad1的全部数据,说明已调用了grad1的display函数。用同一种调用形式“pt->display()”,而且pt是同一个基类指针,可以调用同一类族中不同类的虚函数。这就是多态性,对同一消息,不同对象有 不同的响应方式。

1、在基类用virtual声明成员函数为虚函数。

这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。在类外定义虚函数时,不必再加virtual。

2、在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。

C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。

3、定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。

4、通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类的同名函数.

只要先用基类指针指向即可。如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。这就如同前面说的,不断地告诉出租车司机要去的目的地,然后司机把你送到你要去的地方。

需要说明;有时在基类中定义的非虚函数会在派生类中被重新定义(如例12.1中的area函数),如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。

虚函数表的布局

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

    1. 一个对象实例只有一个虚函数表,只有一个虚基类表。
    1. 对象的每个基类都有一个属于自己的虚函数表指针(vfptr)指向虚函数表(vftbl)的某一项,都有一个属于自己的虚基类表指针(vbptr)指向虚基类表(vbtbl)的某一项。
    1. 虚函数表中按照对象继承的顺序排列对象的虚函数地址,虚基类表中按照对象继承的顺序排列对象的直接虚继承类到虚基类的偏移。
    1. 当基类无虚函数,且派生类有独立虚函数时,派生类对象起始位置为自己的虚函数表指针。否则派生类的虚函数会归到第一个带虚函数表指针的基类的虚函数表指向范围,这样就节省了一个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; }

};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容

  • 我们知道,在同一类中是不能定义两个名字相同、参数个数和类型都相同的函数的,否则就是“重复定义”。但是在类的继承层次...
    踩在浪花上00阅读 435评论 0 1
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,498评论 1 51
  • 3. 类设计者工具 3.1 拷贝控制 五种函数拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符析构函数拷贝和移...
    王侦阅读 1,776评论 0 1
  • 不想撒谎 却让母亲难过了 所有的不利 没有一个方向 全都落井下石 要不要试一试 认为可行的 全都成了嘲笑 何不置之...
    曾在天涯up阅读 58评论 0 0
  • 除夕的由来:除夕是春节的前夜,又叫年三十。有一种传说:是古时候有个凶恶的怪兽叫夕,每到岁末便出来害人,后来人们知道...
    拜泉0458田庆虹阅读 122评论 1 2