先说一点虚函数和非虚函数的区别:
代码中的每个非虚拟函数调用都可以在编译或链接期间解析。
resolved是指函数的地址可以由编译器或链接器计算出来。
所以在创建的目标代码中,函数调用可以替换为操作码,用于跳转到该函数在内存中的地址。
使用虚函数,您可以调用只能在运行时解析的函数。
我们不解释它,让我们通过一个简单的场景:
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);
// A different function may execute at each iteration
}
您可能会理解,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(); // must invoke the destructor of class 'B' at some later point
...
delete b; // the destructor of class 'B' is never invoked
}