一、概述
C++ 中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
每一个含有虚函数的类(无论是自身还是继承过来的,基类中是虚函数,派生类中同名且类型相同的函数也是虚函数),都至少有一个与之对应的虚函数表,其中存放着该类所有虚函数对应的函数指针。
class Base
{
public:
Base(int a):ma(a)
{
std::cout<<"Base::Base()"<<std::endl;
}
~Base()
{
std::cout<<"Base::~Base()"<<std::endl;
}
virtual void Show()
{
std::cout<<"Base::Show()"<<std::endl;
std::cout<<"ma="<<ma<<std::endl;
}
virtual void Show(int a)
{
std::cout<<"Base::Show()"<<std::endl;
std::cout<<"ma="<<ma<<std::endl;
}
protected:
int ma;
};
class Derive:public Base
{
public:
Derive(int b):mb(b),Base(b)
{
std::cout<<"Derive::Derive()"<<std::endl;
}
~Derive()
{
std::cout<<"Derive::~Derive()"<<std::endl;
}
void Show()
{
std::cout<<"Derive::Show()"<<std::endl;
std::cout<<"ma="<<ma<<std::endl;
std::cout<<"mb="<<mb<<std::endl;
}
private:
int mb;
};
-
基类对象中包含了一个虚函数表指针 vfptr,该指针指向了基类的虚函数表 vtbl,基类的虚函数表里面存放的是基类的所有虚函数的入口地址。
-
派生类对象中也包含了一个虚函数表指针 vfptr ,该指针指向了派生类的虚函数表 vtbl,派生类的虚函数表里面存放的是三部分内容:
-
第一部分是从基类那继承过来的没被改写的虚函数的原始的入口地址;
-
第二部分是从基类那继承过来的已经被改写的虚函数的新的入口地址;
-
第三部分是派生类自己新添加的虚函数的入口地址。
-
继承过后,派生类的 vfptr 合并到了基类的 vfptr 中。
注意:虚函数表的确定是在编译期间,编译期间进行语法语义的分析之后就确定了表中的内容,在运行时对象开辟内存才让 vfptr 指向表(表存放在 .rodata段)。
以上是为了便于理解,事实是:虚函数表只是虚表(virtual table)中的一部分内容。例:
int main()
{ //声明虚函数前 声明虚函数后
std::cout<<sizeof(Base)<<std::endl; // 4 8(1?)
std::cout<<sizeof(Derive)<<std::endl; // 8 12(1?)
Base* pb = new Derive(10);
//基类的指针或引用可以指向或引用派生类的对象
std::cout<<typeid(pb).name()<<std::endl;//class Base* class Base*
std::cout<<typeid(*pb).name()<<std::endl;//class Base class Derive(2?)
return 0;
}
针对问题(1):C++语言规定了虚函数的行为,但是它的实现留给了编译器。
编译器处理虚函数的方法是:给每个对象都添加了一个隐藏成员,隐藏成员中保存了一个指向虚函数表的指针——大小多出四个字节的原因。
针对问题(2):而这个虚函数表中存储的是类对象声明以及虚函数的地址,
Base* pb = new Derive(10); 基类的指针pb指向了派生类的对象
*pb本质是一个派生类对象,通过vfptr找到派生类对象的vftable,拿到其中的RTTI,打印的就是派生类对象。
注意:无论类中包含的虚函数是一个还是多个,都只需要在对象中添加一个地址成员,只是表的大小不同而已。
早绑定(静态绑定)
在编译期间确定函数的调用(call具体的函数入口地址)
晚多态(动态绑定)
在运行期间确定函数的调用(call寄存器)——>实现动多态
二、虚函数的一些注意点
- 虚函数设置条件:
- 虚表中存放函数的入口地址——>函数能取地址
inline函数不能设置成虚函数 - 虚函数是通过对象内存布局中的 vfptr 指向的虚函数表找到的——>必须依赖对象调用
构造函数、static修饰的成员方法,普通函数不能设置成虚函数
析构函数、类成员函数可以设置成虚函数
- 什么时候会发生动多态的调动:
指针调用虚函数,但是在构造函数中调动虚函数使用this指针调动,同样是指针调动,但是这种情况发生的是一个静多态,原因是对象不完整。也就是说指针必须指向一个完整的对象时,会发生动多态。
析构函数里面调动虚函数也是静态绑定。 - 虚表的写入时机:构造函数第一行代码执行之前