首先,让我们简单介绍一下虚函数和非虚函数之间的区别:
在代码中,每个非虚函数调用都可以在编译或链接期间解决。
通过“解决”,我们是指编译器或链接器可以计算出函数的地址。
因此,在创建的目标代码中,函数调用可以替换为跳转到内存中该函数地址的操作码。
使用虚函数,您可以调用只能在运行时解决的函数。
不解释了,让我们通过一个简单的场景来演示。
class Animal
{
virtual void Eat(int amount) = 0;
};
class Lion : public Animal
{
virtual void Eat(int amount) { ... }
};
class Tiger : public Animal
{
virtual void Eat(int amount) { ... }
};
class Tigon : public Animal
{
virtual void Eat(int amount) { ... }
};
class Liger : public Animal
{
virtual void Eat(int amount) { ... }
};
void Safari(Animal* animals[], int numOfAnimals, int amount)
{
for (int i=0; i<numOfAnimals; i++)
animals[i]->Eat(amount);
}
如您所知,
Safari
函数允许您灵活地喂不同的动物。
但由于每种动物的确切类型直到运行时才知道,因此要调用精确的
Eat
函数也是如此。
类的构造函数不能是虚拟的,因为:
调用对象的虚拟函数是通过对象类的 V-Table 实现的。
每个对象都持有指向其类的 V-Table 的指针,但该指针只在运行时创建对象时初始化。
换句话说,只有在调用构造函数时才会初始化该指针,因此构造函数本身不能是虚拟的。
除此之外,构造函数本身也没有必要是虚拟的。
虚拟函数的思想在于,可以在不知道调用它们的对象的确切类型的情况下调用它们。
当您创建对象(即隐式调用构造函数)时,您确切地知道正在创建的对象的类型,因此不需要此机制。
基类的析构函数必须是虚拟的,因为:
当您静态分配一个继承自基类的对象时,然后在函数结束时(如果对象是局部的)或程序结束时(如果对象是全局的),类的析构函数会自动调用,并依次调用基类的析构函数。
在这种情况下,析构函数是虚拟的事实上没有意义。
另一方面,当您动态分配(
new
)一个继承自基类的对象时,您需要在程序执行的某个后期点上动态地释放(
delete
)它。
delete
运算符接受指向对象的指针,该指针的类型可以是基类本身。
在这种情况下,如果析构函数是虚拟的,则
delete
运算符调用类的析构函数,然后依次调用基类的析构函数。
但是,如果析构函数不是虚拟的,则
delete
运算符仅调用基类的析构函数,并且实际类的析构函数永远不会被调用。
请考虑以下示例:
class A
{
A() {...}
~A() {...}
};
class B: public A
{
B() {...}
~B() {...}
};
void func()
{
A* b = new B();
...
delete b;
}