【c++】虚函数和虚表

什么是虚函数?
在某基类中声明为virtual并在一个或者多个派生类中重新定义的成员函数。实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖函数。
用法格式: virtual 函数返回类型 函数名 (参数){函数体}。

class A
{
public:
    void print()
    {
        cout << "this is A!" << endl;
    }
};
class B:public A
{
public:
    void print()
    {
        cout << "this is B!" << endl;
    }
};

int main()//为了区别,把这个叫做main1
{
    A a;
    B b;
    a.print();
    b.print();
    system("pause");
    return 0;
}

输出结果:
这里写图片描述
通过class A和class B的print()这个接口,可以看出这两个class因个体的差异而采用了不同的策略。但是这是否就做到了多态性呢? NO!多态还有个关键之处就是通过指向基类的指针或者引用来调用对象。
那我们来改改main函数的代码!

int main()//main2

{
    A a;
    B b;
    A* p1 = &a;
    A* p2 = &b;
    p1->print();
    p2->print();
    system("pause");
    return 0;
}

运行结果:
这里写图片描述
是不是很意外!p2明明指向的是class B的对象但却是调用的class A的print()函数,这不是我们所期望的结果,那么解决这个问题就需要用到虚函数。

class A
{
public:
    virtual void print()
    {
        cout << "this is A!" << endl;
    }
};
class B:public A
{
public:
    virtual void print()
    {
        cout << "this is B!" << endl;
    }
};

此时,class A的成员函数print()已经是虚函数。既然class B继承了class A;那是不是class B的成员函数print()是不是也变为了虚函数呢?答案是yes!我们只需要在基类中把成员函数定义为虚函数,那么派生类中的相应函数也自动的变为虚函数!所以,class B的print()也成了虚函数。那么对于在派生类的相应函数前是否需要用virtual关键字修饰,那就是你自己的问题了(语法上可加可不加,不加的话编译器会自动加上,但为了阅读方便和规范性,建议加上)。
我们现在运行main2函数,输出结果就是this is A,和this is B了。
我作个简单的总结,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。
纯虚函数:在成员函数的形参列表后面写上=0,则成员函数就是纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义(重写)后,派生类才能实例化出对象。
总结:1,派生类重写基类的虚函数实现多态,要求函数名,返回值,参数列表完全相同(协变除外)
2,基类定义了虚函数,在派生类中该函数始终保持虚函数特性。
3,只有非静态成员函数才可以定义为虚函数。
4,构造函数不能为虚函数,不要在构造函数和析构函数中调用虚函数,因为在这对象是不完整的,可能会出现未定义的行为。

虚表:
在C++语言中,每个有虚函数的类或者虚继承的子类,编译器都会为它生成一个虚拟函数表(简称:虚表),表中的每一个元素都指向一个虚函数的地址。(注意:虚表是从属于类的)。此外,编译器会为包含虚函数的类加上一个成员变量,是一个指向该虚函数表的指针(常被称为vptr),每一个由此类别派生出来的类,都有这么一个vptr。虚表指针是从属于对象的。也就是说,如果一个类含有虚表,则该类的所有对象都会含有一个虚表指针,并且该虚表指针指向同一个虚表。
虚表的内容是依据类中的虚函数声明次序–填入函数指针。派生类别会继承基础类别的虚表(以及所有其他可以继承的成员),当我们在派生类中改写虚函数时,虚表就受了影响;表中的元素所指的函数地址将不再是基类的函数地址,而是派生类的函数地址。

class A//虚函数示例代码
{
public:
    virtual void fun()
    {
        cout << 1 << endl;
    }
    virtual void fun2()
    { 
        cout << 2 << endl;
    }
};
class B : public A
{
public:
    void fun()
    {
        cout << 3 << endl; 
    }
    void fun2()
    { 
        cout << 4 << endl;
    }
};

由于两个类都有虚函数的存在,所以编译器会给他们两个分别插入一段你不知道的数据,并且创建一个表—->虚表。那段数据叫做vptr指针,指向那个表。那个表叫做vtbl,每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址,请看图
这里写图片描述
可以看到这两个vtbl分别为class A和class B服务。现在有了这个模型之后,我们来分析下面的代码
A *p=new A;
p->fun();
毫无疑问,调用了A::fun(),但是A::fun()是如何被调用的呢?它像普通函数那样直接跳转到函数的代码处吗?No,其实是这样的,首先是取出vptr的值,这个值就是vtbl的地址,再根据这个值来到vtbl这里,由于调用的函数A::fun()是第一个虚函数,所以取出vtbl中第一个Slot的值即为第一个虚函数的地址(),这个值就是A::fun()的地址了,最后调用这个函数。现在我们可以看出来了,只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务。
而对于class A和class B来说,他们的vptr指针存放在何处呢?其实这个指针就放在他们各自的实例对象里。由于class A和class B都没有数据成员,所以他们的实例对象里就只有一个vptr指针
这里写图片描述

小结(虚表):
1,他是存放虚函数的,存放顺序与基类的声明次序有关
2,在派生类中:先和基类虚函数保持一致,然后如果派生类重写了积基类中的某个虚函数,就会替换相同偏移量位置的基类虚函数。在派生类新增的虚函数放在最后,次序和声明次序有关。
3,多继承中,派生类新增的虚函数增加到第一继承者虚表后面。
4,相同类型的对象共用同一张虚表。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值