目录
前言
本期我们学习的是 C++ 中的虚函数。
过去的几期,我们一直在讨论类、面向对象编程、继承这些内容,所有的这些内容,包括本期我们将要学习的虚函数,对整个面向对象的概念都非常重要。
虚函数是指一个类中你希望重载的成员函数 ,当你用一个基类指针或引用指向一个继承类对象的时候,调用一个虚函数时, 实际调用的是继承类的版本。
01 虚函数能干什么呢?
虚函数允许我们在子类中重写方法。
假设我们有两个类 A 和 B,B 是 A 派生出来的类。如果我们在 A 中创建一个方法,标记为 virtual,我们可以选择在 B 类中重写那个方法,让它做其他的事情。
像之前一样,我们通过一个例子来解释今天的知识点。
02 没有虚函数前的例子
我创建了两个类,一个是 Anima,它唯一拥有的是一个名为 GetName 的公共方法,它会返回一个字符串,我们让它返回 "animal"。
还有另外一个类 Dog,它是 Animal类的子集,它提供一个构造函数,允许我们指定一个名字;然后给它提供了一个叫 GetName 的方法,我们让他返回Dog字符串。
我们来看看如何使用上面这些设定。
我有一个 Print 函数,参数是一个 Animal的指针。
现在我们有了一个函数,它可以接受任何 Animal类型的参数,你可以看到,我们不会得到任何的编译错误。
当我们试图将 a 传递给函数时,因为 p 是一个 Animal,Dog 是 Animal的·子类,在函数里面我们做的就是调用 GetName 方法,我们期望的是,在主函数中调用的部分,参数为 Animal类型时,GetName 用于 Anima,而参数为 DOg类型时,GetName 用于 Dog。
然而,运行代码之后你会发现它打印了两次 Animal。
//测试代码
//执行结果为打印两次Animal
#include <iostream>
class Animal
{
public:
std::string getName(){return "Animal";};
};
class Dog: public Animal
{
public:
std::string getName(){return "Dog";};
};
void print(Animal* obj)
{
std::cout<<obj->getName()<<std::endl;
}
int main()
{
Animal *animal = new Animal;
Dog *dog = new Dog;
print(animal);
print(dog);
return 0;
}
为什么会这样呢?
03 使用虚函数后的例子
发生这种情况的原因时,在我们声明函数时,我们的方法通常在类内部起作用。然后当调用方法的时候,会调用属于该类型的方法。
我们看这个 Print 函数,它的参数是 Animal,这意味着当我们调用 GetName 函数时,如果是在 Animal里面,那么它会从 Animal类中找这个叫做 GetName 的函数。
然而,我们希望 C++ 能意识到一点:我在这里传递的 Animal实际上是 它的子类Dog,所以,请调用 Dog中的 GetName 函数。
这时候,虚函数就该出现了。
虚函数virtual 概念
虚函数引入了一种叫做 Dynameic Dispatch(动态联编)的东西,它通常通过 V 表(虚函数表)来实现编译。
V 表就是一个表,它包含基类中所有虚函数的映射,这样我们可以在它运行时,将它们映射到正确的覆写(overwrite)函数。
简单起见,现在你只需要知道,如果你想覆写一个函数,必须将基类中的基函数标记为虚函数。
我们回到代码中继续看一下。
我在基类 Animal类中 GetName 函数前面使用了 virtual 这个关键字,这可以告诉编译器,——嘿,为这个函数生成 V 表吧,这样,如果它被重写了,你可以指向正确的函数。
我们运行代码试试看。
我们得到了期望的结果。
现在,我们可以做的另一件事:使用在 C++11 引入的覆写函数标记的关键字 override。
这个不是必须的,无论有没有这个关键字,程序都会正常工作,但是我还是建议你这样做。因为首先这会让你的程序更具有可读性,阅读程序的时候我们可以知道这实际上是一个覆写的函数;它还可以帮助我们预防 Bug 的发生,比如拼写错误之类的
//用于测试的代码
#include <iostream>
class Animal
{
public:
virtual std::string getName(){return "Animal";};
};
class Dog: public Animal
{
public:
std::string getName(){return "Dog";};
};
void print(Animal* obj)
{
std::cout<<obj->getName()<<std::endl;
}
int main()
{
Animal *animal = new Animal;
Dog *dog = new Dog;
print(animal);
print(dog);
return 0;
}
虚函数使用需要一定开销
这就是虚函数的本质,但是很遗憾的一点是,虚函数并不是没有额外的开销的,有两种与虚函数相关的运行时成本。
首先,我们需要额外的内存来存储 V 表,这样我们就可以分配到正确的函数,包括基类中要有一个成员指针指向 V 表;其次,每次我们调用虚函数时,我们需要遍历这个表来确定要映射到哪个函数,这些是额外的性能损失。