一、虚函数的工作原理(虚函数表)
通常我们在类中编写了虚方法后,编译器是怎么来处理呢?
编译器处理虚函数的方法是:给每个对象加一个隐藏成员。这个隐藏成员中保存了一个指向函数地址数组的指针。这个数组称为虚函数表vtbl(virtual function table)。这个指针就叫虚表指针!
(1)、虚函数表存储什么呢?
每一个类都一个自己的虚函数表(不管它是不是谁的子类),这个虚函数表是类的所有对象共享的,但每一个对象只保存该表的位置地址。虚函数表里边存放了自身类的所有虚函数和从基类继承来的虚函数的函数地址!
(2)、继承之后的虚函数表是什么情况呢?
<1>、A类是一个基类,A类的对象包含了一个指针,该指针指向A类中的所有虚函数的地址表。
<2>、A类的派生类是B类,B类的对象包含一个指向B类中所有虚函数(包括B类自己定义的虚函数和继承自A类的虚函数)的地址表的指针。
<3>、如果B类(派生类)提供了A类(基类)虚函数的新定义,则B类的虚函数表将保存新函数的地址,不保存A类的原始虚函数版本的地址。
<4>、如果B类(派生类)没有重新定义A类(基类)中的虚函数,则B类将保存A类中的虚函数版本的地址。
<5>、如果B类定义了新的虚函数,则该虚函数的地址也将被添加到B类的虚函数表中。
注意:无论类中包含的虚函数是1个还是10个,都只需要在类对象中添加一个地址成员,只是表的大小不同而已。
(3)、举个栗子
//Scientist(科学家)类是基类
class Scientist
{
... ...
char name[50];
public:
virtual void show_name();
virtual void show_all();
... ...
};
//Physicist(物理科学家)类是科学家类的派生类
class Physicist : public Scientist
{
... ...
char field[40];
public:
void show_all(); //重新定义基类中的虚方法
virtual void show_field(); //新定义的虚方法
... ...
};
(4)、调用虚函数的流程
<1>、查看存储在对象中的虚函数表地址,然后转向相应的函数地址表
<2>、在函数地址表中找到相应的函数地址,然后转去执行函数
(5)、使用虚函数,在内存和执行速度方面有一定的成本,包括
<1>、每个对象都将增大,增大量为存储虚函数表地址的隐藏成员所占的空间
<2>、对于每个类,编译器都将创建一个虚函数地址表
<3>、对于每个函数调用,都需要执行一项额外的操作(到虚函数表中查找函数地址)。
二、虚函数的注意事项
(1)、在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
(2)、如果使用指向对象的引用或指针来调用虚方法,程序将使用对象类型(引用或指针指向的类)定义的方法,而不使用引用或指针本身类型定义的方法。
(3)、如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
(4)、构造函数不能是虚函数。派生类不继承基类的构造函数,将类构造函数声明为虚也没什么意义。
(5)、析构函数应当是虚函数,除非类不用做基类。
<1>、如果析构函数不是虚的,则将只调用对应指针类型的析构函数
例: Brass是BrassPlus类的基类
Brass *pb = new BrassPlus();
delete pb;
这里delete之后,只会调用Brass类的析构函数,而不会调用子类BrassPlus的析构函数,尽管pb指向的是子类BrassPlus对象。
<2>、如果析构函数是虚的,将会调用它指向的对象的析构函数,然后自动调用其基类的析构函数。
即上面的栗子中,将先调用BrassPlus的析构函数,再调用基类Brass的析构函数!
所以,通常应给基类提供一个虚析构函数,即使它并不需要析构函数!
(6)、友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数!
(7)、如果派生类没有重新定义基类中的虚函数,将使用该函数的基类版本!如果派生类位于派生链中,则将使用最新的虚函数版本。
(8)、重定义将隐藏方法
<1>、只要派生类中出现与基类重名的虚函数,不管参数列表、返回值是否相不相同,都将隐藏同名基类虚函数。
<2>、重新定义不会生成函数的两个重载版本。
两条经验规则:
<1>、如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化。
<2>、如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。
例:
class Add
{
public:
//加法运算的三个重载版本
virtual double showperks(int a, int b);
virtual double showperks(float a, float b);
virtual double showperks(double a, double b);
... ...
}
class AddConst : Add
{
//重新定义加法运算的三个重载版本
virtual double showperks(int a, int b) const;
virtual double showperks(float a, float b) const;
virtual double showperks(double a, double b) const;
... ...
}
如果指重新定义了一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。