虚函数
虚函数 是 virtual 修饰的函数
语法:
virtual 函数返回类型 函数名(参数表)
{
函数体
}
虚函数必须是基类的非静态成员函数,其访问权限可以是private或protected或public,在基类的类定义中定义虚函数的一般形式:
class 基类名{
…
virtual 返回值类型 将要在派生类中重载的函数名(参数列表);
};
我们先来看这样一段代码
#include<iostream>
using namespace std;
class Base//创建基类
{
public:
Base(int a) : ma(a){}
void Show()
//virtual void Show()
{
cout<<"ma "<<ma<<endl;
}
protected:
int ma;
};
class Derive : public Base//派生类公有继承基类
{
public:
Derive(int b) : mb(b) ,Base(b){}//指明基类的构造方式
void Show()
{
cout<<"mb "<<mb<<endl;
}
protected:
int mb;
};
int main()
{
Base* pb = new Derive(10);//基类指针指向派生类指针
cout<<sizeof(Base)<<endl;//基类大小
cout<<sizeof(Derive)<<endl;//派生类大小
cout<<typeid(pb).name()<<endl;//pb指针的类型
cout<<typeid(*pb).name()<<endl;//pb指向的数据类型
pb->Show();
delete pb;
return 0;
}
得到的结果:
我们可以得到的信息:
1.基类只有一个int型的数据 占4个字节
2.派生类公有继承了基类的变量
3.pb指针是一个指向基类的指针
所以pb指针是从Base基类中找Show函数 (Base::Show)
如果将基类的Shwo函数改为虚函数 virtual void Show()
会出现什么情况?
下图即为运行结果
我们发现几个问题:
- 为什么基类和派生类都多出了4个字节
- pb指针是一个基类的类型却指向了派生类
- 调用的Show是派生类的Show函数
虚函数的机制
虚函数是为动多态提供支持
我们先要了解一些多态的基本内容,多态分为:
- 静多态:编译阶段确定函数的调用 例如:函数的重载 模板
- 动多态:运行阶段确定函数的调用
- 宏多态:预编译阶段确定函数的调用 (不常用)
那既然动多态是在运行阶段确定函数的调用,那么我们通过函数的编译过程中寻找答案。
一个源文件.cpp/.c文件变成一个可执行程序经历了一下几个阶段(可执行文件就可以运行)
我们主要看编译,链接,运行三个阶段,
- 动多态是在运行阶段确定函数的调用,那就说要在运行阶段拿到函数的入口地址,最开始文件的内容在磁盘中保存,而运行阶段所有的数据在内存中保存,如果要确定函数的调用,内存中就要有函数的入口地址的数据。所有我们要知道链接做了什么事情?
- 链接主要做的事情:
1.合并段和符号表
2.符号解析
3.分配地址和空间
4.符号的重定位
函数的入口地址是存放在符号中,但是符号表是在磁盘中存放。如图,运行阶段时的数据并没有函数的入口地址,那么一般的函数是没办法实现动多态的。
由上图我们可以知道,运行阶段只从文件中获取指令和数据
指令在.text中存放,而数据在.data,.bss和只读数据段存放,所以我们只要在指令段或者数据段中存放函数的入口地址,那么就可以实现。函数的入口地址是在编译的过程中生成,至此,我们又要看看编译时做了什么?
-编译阶段生成符号,放在符号表,一般的函数我们可以知道符号表最终不会加载到内存中,所以它不会达到我们预期的目的。所以我们要另想办法,函数的入口地址在C语言中可以用指针表示,并且指针就是一个数据,所以我们将函数入口地址放在只读数据段中(.rodata),
虚函数的机制:
- 1.编译阶段将函数的入口地址放在只读数据段和符号表中,而在只读数据段中我们存放的是一个结构体——虚函数表(vftable),在类中存在一个虚函数指针。
2.通过运行加载数据,把虚函数表加载到内存中
下面我们来了解一下虚函数表和虚函数指针。
基类指针指向派生类对象
在代码中有
Base* pb = new Derive(10);
虚函数调用:基类指针 pb 指向 派生类对象,而在派生类对象的内存布局中有一个虚表指针,其中虚表指针指向的 Derive 的虚表结构。因此,在 pb->Show()
调用时,实际上是 pb -> vfptr -> Derive::Show()
,最终在屏幕上输出了 “mb 10” 。
可以这么理解,Base pb = new Derived()
;生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,接着由Derived到Base*的转换并没有改变虚表指针,pb 所指向的对象它在构造的时候就已经指向了子类的Derive::Show(),所以调用的是子类的虚函数,这就是多态了。
另外,在基类指针 pb 解引用时,优先查看 RTTI 信息中保存的类型信息。因此,*pb 的类型被解析成 Derive 类型。
虚函数表机制
基类指针指向派生类对象实质上是指向派生类对象中基类的起始部分,在虚函数表结构中有三部分组成,分别是:
- RTTI(Run-Time Type Identification)信息:通过保存在其中的类型信息在运行时能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。
- 偏移: 成员变量与类对象的偏移地址(在 vs 中,vfptr 相对于成员变量的优先级更大,位于内存分布的首位,所以偏移量为0)。
- 虚函数入口地址:在调用函数时通过 call 指令跳转到函数,在虚表中保存虚函数的入口地址可以在运行阶段通过查虚表的方式实现动多态。
vfptr是在构造函数的栈帧进行初始化的时候:在构造函数初始化列表之后并调用构造函数第一行代码之前,函数栈帧开辟后进行赋值虚表地址赋值给vfptr的,This指针的赋值也是在构造函数的栈帧进行。
我们可以回答以前的几个问题:
基类多出来的4个字节是因为在基类中增加了一个虚函数指针,在可读数据段增加了一个虚函数表;而派生类继承基类是也将基类中的虚函数指针继承了下,在可读数据段中也增加了一个虚函数表,但是继承下来的虚函数表发生了覆盖,只显示派生类的虚函数。这也是对pb指针解引用得到的是派生类的Show函数。