C++虚函数

202 阅读5分钟

上一篇:C++继承

下一篇:C++接口(纯虚函数)

1. 概述

虚函数允许我们在子类中重写方法。例如,我们有两个类A和B,B是A派生出来的,也就是B是A的子类。如果我们在A类中创建一个方法,标记为virtual,我们可以选择在B类中重写那个方法,让它做一些其他的事情。

2. 案例

1. 项目准备

准备一个简单的项目,项目有一个Main.cpp文件,文件内容如下

image.png

2. 开始项目

创建一个Entity类,Entity类唯一拥有的是一个名为GetName的公共方法。返回一个string

image.png

我们创建另一个类Player,它将是Entity类的子类。我们会增加一些内容,首先,我们要存储一个名字m_Name,然后我们在提供一个构造函数并允许指定一个名字。然后我们会给它一个叫GetName的方法,返回m_Name。代码如下

image.png

下面我们来看看如何使用上面这些设定。

在main函数中,我们在创造一个Entity,我要试着打印GetName(),然后我要创建一个Player命名为"Cherno",也调用GetName将名字打印出来。

我不打算删除这些对象,因为程序会终止,(自然这些对象就会被回收)所以这里用delete没啥用。

代码如下

image.png

按下F5运行程序

image.png

看起来不错,我们得到了EntityCherno

然而,如果我们使用多态的概念,那么到目前为止我们在这里编写的所有内容,都有问题了。如果我们指向一个player,就像它是一个Entity一样,我们就会遇到问题。

例如,如果我们创建一个名为Entity的变量,它会被赋值为p,Entity* entity = p;,这个p是指向Player的指针,任然是一个Player。现在我把它指向entity,如果我们打印。代码如下

image.png

我们F5运行代码

image.png

会发现Entity被打印了两次,这不是我们期望的,我们当然希望是Player,因为即使我们指的是Entity,

image.png

但它实际上是一个Plyayer,是player类的一个实例

image.png

可能更好的例子是,如果我们有一个PrintName函数,参数是一个Entity,代码如下

image.png

我们用PrintName函数替换main函数中的打印语句。

image.png

现在我们有了一个函数,可以接受任何Entity参数。我们编译文件

image.png

可以看编译通过,没有任何错误,当我们试图将p传递给PrintName函数时,因为p是一个继承了Entity,p也是一个Entity。我们期望的是e的GetName用于Entity,而p的GetName用于Player。

然而,当我们F5运行程序

image.png

可以看到,我们打印了两次Entity。为什么会这样?得到错误的结果?

发生这种情况的原因是,在我们通常声明函数时,我们的方法通常在类内部起作用,然后当要调用方法的时候,会调用属于该类型的方法。

我们再来看看这个PrintName函数

void PrintName(Entity* entity)
{
    std::cout << entity->GetName() << std::endl;
}

它接收的参数是Entity指针,这就意味着,当我们调用GetName函数时,如果是在Entity里面,那么它会从Entity类中找到这个叫做GetName的函数。

image.png

当然,我希望C++能意识到这一点,当指针是Player时,PrintName传递的Entity实际上是Player,请调用Player的GetName函数。

image.png

这就是虚函数出现的地方,虚函数引入了一种叫做Dynamic Dispatch(动态联编)的东西,它通常通过V表(虚函数表)来实现编译。v表就是一张表,它包含基类中所有虚函数的映射,这样我们可以在它运行时,将它们映射到正确的覆写(override)函数。

这里只需简单的知道,如果我们想要覆写一个函数,那么我们必须将基类中的基函数标记为虚函数。

我们回到代码,我们在基类Entity的GetName基函数前面加上virtual这个词修饰。尽管,我们没有做很多工作,但这可以告诉编译器,我们为这个函数需要生成v表。这样如果这个函数被重写了,多态就可以指向正确的函数。

image.png

接下来,我们F5运行我们的程序。

image.png

可以看到,我们得到了Entity和Cherno,得到了期望的打印结果。

现在我们可以做的另一件事,是在C++11中引入的,将覆写的函数标记关键字override,如下

image.png

但这不是必须的。可以看到我们刚刚并没有写override,它也工作的很好。然后,我们最好还是加上override,因为首先,这会让代码更具可读性,因为我们现在知道这实际上是一个覆写的函数。而且它还可以帮助我们,预防bug的发生,比如函数名拼写的错误。

image.png

可以看到vs会给出提示,因为基类中没有这样的函数可以覆写。

这就是虚函数,但是很遗憾,虚函数并不是免费的,有两种与虚函数相关的运行时成本。

首先,我们需要额外的内存来存储v表,这样我们就可以分配到正确的函数,包括基类中要有一个成员指针,指向v表。其次,每次我们调用虚函数时,我们需要遍历这个表,来确定要映射到哪个函数,这是额外的性能损失。由于这些损失,有些人根本就不喜欢使用虚函数,但是这个性能开销基本非常小,可以放心使用。可能在一些嵌入式平台上,cpu的性能很差,那么可以避免使用虚函数,除此之外,真的不能说因为它们影响性能而不去用。

上一篇:C++继承

下一篇:C++接口(纯虚函数)