1.虚函数的作用是什么?
C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法(virtual关键字)。
函数的重载也可以认为是多态,但是是静态的,是编译时决定的,叫做静态联编。而虚函数是运行时决定的,是动态的,叫做动态联编。
2.虚函数的底层实现机制是什么?
实现原理:虚函数表+虚函数表指针。
编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类(拥有虚函数的类才有)使用一个虚函数表,每个类对象(拥有虚函数的类才有)用一个虚表指针。
在vs2019中调试不含虚函数的类对象以及含有虚函数的类对象可以发现,只有含有虚函数的类对象才有虚函数表指针(__vfptr)。
基类的虚函数表和子类的虚函数表不是同一个表,因为虚函数表指针(__vfptr)指向的位置不一样。这是单继承的情况,如果继承了两个具有虚函数的基类,就有两个虚函数表和两个虚函数表的指针,分别在两个基类的内存空间上。
下图是测试的代码:
#include <iostream>
using namespace std;
//基类A
class BaseA {
private:
int a;
public:
BaseA()
{
cout << "construct basea" << endl;
}
virtual void funa()
{
cout << "funca" << endl;
}
virtual ~BaseA()
{
cout << "destruct basea" << endl;
}
};
//基类B
class BaseB {
private:
int b;
public:
BaseB()
{
cout << "construct baseb" << endl;
}
virtual void funb()
{
cout << "funcb" << endl;
}
virtual ~BaseB()
{
cout << "destruct baseb" << endl;
}
};
//无虚函数的类C
class NormalC {
private:
int c;
public:
NormalC()
{
cout << "construct NormalC" << endl;
}
~NormalC()
{
cout << "destruct NormalC" << endl;
}
};
//单继承子类D
class SingleDeriveD : public BaseA
{
private:
int d;
public:
SingleDeriveD()
{
cout << "construct SingleDeriveD" << endl;
}
~SingleDeriveD()
{
cout << "destruct SingleDeriveD" << endl;
}
virtual void funa()
{
cout << "SingleDeriveD funa" << endl;
}
};
//多继承子类E
class MultipleDeriveE : public BaseA,public BaseB
{
private:
int e;
public:
MultipleDeriveE()
{
cout << "construct MultipleDeriveE" << endl;
}
~MultipleDeriveE()
{
cout << "destruct MultipleDeriveE" << endl;
}
virtual void funa()
{
cout << "MultipleDeriveE funa" << endl;
}
virtual void funb()
{
cout << "MultipleDeriveE funb" << endl;
}
};
void main()
{
BaseA* a = new BaseA();
BaseB* b = new BaseB();
NormalC* c = new NormalC();
SingleDeriveD* d = new SingleDeriveD();
MultipleDeriveE* e = new MultipleDeriveE();
a->funa();
b->funb();
d->funa();
e->funa();
e->funb();
}
执行后的输出是:
在调试的时候,可以查看各个对象的__vfptr的指针以及指向的位置。
3.为什么析构函数可以是虚函数,构造函数却不能是虚函数?
因为虚函数的实现原理是:虚函数表+虚函数表指针,最关键的是寻函数表指针,这个指针是在对象构造的时候赋值的,一般是在构造函数初始化列表之后,函数体第一句代码之前赋值,因此,如果构造函数是虚函数,它的调用又要通过虚函数表指针(__vfptr)来寻找调用地址,而这个时候__vfptr还没有被赋值,这样就有问题了。
编译器处理虚函数的方法是:
给每个对象添加一个指针,存放了指向虚函数表的地址,虚函数表存储了为类对象进行声明的虚函数地址。比如基类对象包含一个指针,该指针指向基类所有虚函数的地址表,派生类对象将包含一个指向独立地址表的指针,如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被添加到虚函数表中。
使用虚函数后的变化:
(1) 对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。
(2) 每个类编译器都创建一个虚函数地址表
(3) 对每个函数调用都需要增加在表中查找地址的操作。
虚函数的注意事项
总结前面的内容
(1) 基类方法中声明了方法为虚后,该方法在基类派生类中是虚的。
(2) 若使用指向对象的引用或指针调用虚方法,程序将根据对象类型来调用方法,而不是指针的类型。
(3)如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚。
构造函数不能为虚函数。
基类的析构函数应该为虚函数。
友元函数不能为虚,因为友元函数不是类成员,只有类成员才能是虚函数。
如果派生类没有重定义函数,则会使用基类版本。
重新定义继承的方法若和基类的方法不同(协变除外),会将基类方法隐藏;如果基类声明方法被重载,则派生类也需要对重载的方法重新定义,否则调用的还是基类的方法。