上一期我们学习了虚函数,本期我们学习一种特殊的虚函数,纯虚函数。
C++ 纯虚函数本质上与其他语言中的抽象方法或接口相同,基本上,纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数。
我们可以看一下之前关于虚函数的例子。
你可以看到在 Entity 类中有一个虚函数 GetName,然后我们在 Player 类中重写了那个函数,在基类中,这个 GetName 函数有函数体,意味着在某个类中重写它只是一个可选项,即使不重写它,仍然是可以调用它的。
然而在某些情况下,提供这种默认实现是没有意义的,实际上我们可能想要强制子类为特定的函数提供自己的定义。
在面向对象编程中,创建一个类,只由未实现的方法组成,然后强制子类去实现它们的操作非常常见,这种类通常被称为接口。
因此,类中的接口只包含未实现的方法作为模板,由于这个接口实际上并不包含方法实现,我们实际上不可能实例化那个类。
纯虚函数
让我们来看看这个在 Entity 类中的 GetName 函数能不能搞成纯虚函数。
在上面的代码中,我们依然是将 GetName 函数定义为虚函数,但等于0使它成为一个纯虚函数,这意味着它必须在一个子类中实现,如果你想实例化这个子类的话,确实需要做出一些改变。
首先,我们看一下 main 函数,可以看到现在不具有实例化 Entity 类的能力,我们必须给它一个子类来实现这个函数。
使用 Player 类。
你可以看到 Player 类是可以正常工作的。
这只是因为我们在 Player 类中实现了那个 GetName 函数,你可以试试注释掉 Player 中的 GetName 函数,你会发现 Player 也不能进行实例化了。
本质上,你只能在实现了所有这些纯虚函数之后,才能够实例化,或者在更上层次的类中完成。比如,Player 类是另一个类(Entity 的子类)的子类,而这个类实现了 GetName 函数,那也是可以的。
纯虚函数必须被实现,才能创建这个类的实例。
让我们来看另外一个更好的例子。
我们编写一个函数来打印这些类的类名,保证 obj 有GetClassName 函数。现在我们需要一个类型,可以提供 GetClassName 函数,这就是所谓的接口。
我们把问号的位置叫做 Printable,然后设置它。
在上面,我们创建一个新类,叫 Printable,它只有这些内容,它会创建一个 public virtual 字符串函数,返回一个字符串,这个函数是纯虚函数。然后让 Entity 实现那个接口。
注意 Player 现在已经是一个 Entity 了,所以我们不需要实现 Printable 接口了。
还有就是,我虽然把 Printable 叫做接口 interface,但它其实只是一个类,所以还是叫 class 而 不是叫做 interface,因为它只不过有一个纯虚函数,仅此而已。
实际上,其他语言有 interface 关键字而不是叫做 class,C++ 是没有的,接口只是 C++ 的类而已。
现在所有的类都需要实现这个 GetClassName 函数了。
让我们继续在 Entity 类中添加一个 GetClassName 函数,
我们无法实现实例化的问题已经解决了, 现在已经提供了那个功能。
然而,你可能会注意到,我们还没有为 Player 提供一个覆写函数,如果现在调用函数 Print 并运行,你可以看到打印了两次 Entity,因为我们还没有在 Player 中提供定义。
我们直接复制具体的代码并做一些修改,再次运行。
可以看到,现在我们得到了正确的类名,所有的这些都来自一个 Print 函数。这个函数接受 Printable 作为参数,它并不关心具体是什么类。
比如,我们可以创建一个完全不同的类,比如 A,它是一个 Printable 类的子类,它必须有相应的函数。
我现在创建了一个全新的类,并且实现了 Printable 这个接口,即实现了这个函数,像之前那样调用打印,你会看到结果是正常的。
注意:这样的写法不推荐哈,容易造成内存泄露,不过作为测试是没有问题的。
最后的话
这就是纯虚函数整个的工作原理,它知道任何 Printable 的东西,它们都有一个 GetClassName 函数去调用,如果你不实现这个函数,你就不能实例化这个类。
这就是 C++ 中的接口,也就是 C++ 中的纯虚函数,它是非常有用的一个工具,可以被用在刚才的这个场景下,如果想要确保类都有一个特定的方法,那么可以将这个类作为参数放入一个通用的函数中,然后就可以调用这个方法或者做其他事情。
本期内容就是这些,下期再见。