1. 概述
上一篇中,我们讲了虚函数,这篇我们来了解下一种特殊类型的虚函数,叫做纯虚函数。纯虚函数,本质上和其他语言(如java或C#)中的抽象法方法或接口相同。纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数。
2. 案例
在上一篇例子中,可以看到我们在Entity类中有一个虚函数GetName,然后我们在Player类中重写了这个GetName函数。这基类中,这个GetName函数,有函数体,这就意味着在某个类中重写它只是一个可选项,即使我们不再子类中重写它,我们仍然可以调用player.GetName函数,然后返回Entity字符串。
然而在某些情况下,提供这种默认实现是没有意义的。实际上我们可能想要强制子类,为特定的函数提供自己的定义。在面向对象编程中,创建一个类,只由未实现的方法组成,然后强制子类去实际实现它们,这非常常见,这通常被称为接口。因此,类中的接口只包含未实现的方法,作为模版。由于这个接口类实际上并不包含方法实现,我们实际上不可能实例化那个类。
让我们看看这个在Entity类中的GetName函数,能不能搞成纯虚函数。首先,我们需要去掉方法体,就写成等于0如下
virtual std::string GetName() = 0;
注意这里依然是定义成virtual虚函数,但等于0本质上使它成为一个纯虚函数。这意味着它必须在一个子类中实现,如果我们想实例化这个子类的话。
通常这样做,确实发生了一些事情,首先,我们看main函数
可以看到,我们现在不再具有实例化Entity类的能力。我们必须给它一个子类,来实现这个函数。在这种情况下,例如实例化Player,当然,我们需要提供一些字符串
可以看到Player工作正常。然而,这只是因为Player实际上实现了GetName函数。
如果我们注释掉这个实现
可以看到我们Player也不能实例化了。我们只能在实现了所有这些纯虚函数之后,才能够实例化。或者在更上层次的类,比如,Player类是另一个类的子类(Entity子类的子类),而这个类实现了GetName函数,那也是可以的。我们的想法是,纯虚函数必须被实现,才能创建这个类的实例。
下面,我们来看一下一个更好的例子。我们先撤销下之前的修改操作。
假设我们想要编写一个函数来打印这些类的类名。我们写上void Print,参数我们想写某些类型。然后调用GetClassName这样的函数。
所以,这个函数,我们需要的是一个类型,保证我们有这个GetClassName函数,我们需要一个类型,可以提供GetClassName函数,这就是所谓的接口。让我们将这个类型叫做Printable,然后设置它。
我们在main.cpp文件的最上方创建一个新类,叫Printabl,它唯一会有的是,它会创建一个public virtual字符串函数,他会返回一个字符串,GetClassName函数virtual std::string GetClassName() = 0;。它是纯虚函数,所以要设置为0。
然后,我们让Entity实现那个接口,注意Player已经继承了Entity,由于Player已经继承了Entity,所以Player不需要再实现Printable接口。
虽然,我们将Printable叫做接口(interface),但它其实只是一个类(class),所以还是叫class,而不是interface,因为它只不过是有一个纯虚函数,仅此而已,实际上,其他语言有interface关键字而不是叫class。但是c++没有,接口只是c++的类而已。
现在所有类继承了Printablel类的派生类都需要实现这个GetClassName函数了,如果不实现,那么将无法实例化这个类。让我们继续在Entity类添加一个GetClassName函数,如果我们做之前在Player中相同的事情(可能会有问题)
当然,首先,我们无法实例化Entity的问题已经解决了,因为我们已经提供了那个功能。然而,我们还没有为Player提供一个GetClassName函数(覆写函数)。如果我们去调用Print函数,当然,Print函数这里的参数要改下,改成指针。
然后在main函数中分别调用Print函数,分别使用e和p参数。
F5运行程序
可以看到,我们打印了两次Entity,因为我们还没有在Player中提供定义。然而,如果我们在Player类中覆写GetClassName函数
F5运行程序
可以看到,我们现在得到了正确的类名。
所有这些都来自于一个Print函数
void Print(Printable* obj)
{
std::cout << obj->GetClassName() << std::endl;
}
这个函数接收Printable指针作为参数,它并不关心具体是什么类,我们完全可以创建一个完全不同的类,比如A继承Printable,那么这类A就必须有这个函数GetClassName,如果它没有实现GetClassName函数,那么将无法实例化这个类。
class A : public Printable
{
public:
std::string GetClassName() override { return "A"; }
}
这就是C++中的虚函数,非常有用的东西,它被应用在刚才的这个场景下,如果确保类都有一个特定的方法,那么可以将这个类(抽象基类)作为参数(类型)放入一个通用的函数中。然后就可以调用这个方法或做其他事情。