YouTube视频链接
C++接口(纯虚函数)本文是ChernoP28视频的学习笔记。
C++纯虚函数本质上与其他语言(如Java或C#)中的抽象方法或接口相同。纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数。
如上节的Main.cpp代码
#includeclass Entity { public: virtual std::string GetName() { return "Entity"; } }; class Player :public Entity { private: std::string m_Name; public: Player(const std::string& name) :m_Name(name) {} std::string GetName()override { return m_Name; } }; void PrintName(Entity* entity) { std::cout << entity->GetName() << std::endl; } int main() { Entity* e = new Entity(); PrintName(e); Player* p = new Player("Cherno"); PrintName(p); std::cin.get(); }
Entity类中有一个虚函数GetName,然后我们重写了这个函数在Player类中。在基类中这个GetName函数有函数体,意味着在Player类中即便不重写,仍然可以调用Player.GetName,然后返回Entity字符串。
但实际上我们可能想要强制子类,为特定的函数提供自己的定义。在面向对象的编程中,创建一个类由未实现的方法组成,然后强制子类去实际实现它们是非常正常的。这通常被称为接口,因此类中的接口只包含未实现的方法作为模板,因此我们不可能实例化这个类。
class Entity
{
public:
virtual std::string GetName() = 0;
};
引入纯虚函数
因此我们去掉GetName的方法体,写成等于0。注意这里依然是定义成virtual虚函数,但等于0本质上使它成为一个纯虚函数。这意味着它必须在子类中实现。我们就不在具有实例化Entity类的能力。
我们必须给它一个子类,来实现这个函数。例如Player类,当然这里要提供一些字符串,此时编译通过。
这是因为我们实际实现了GetName函数,如果我们决定注释掉第16行的实现,可以看到不能进行实例化了。我们只有在实现了所有纯虚函数以后,才能够实例化。
假设我们想要编写一个函数来打印这些类的名字,我写上void Print,我想写???类型作为参数,obj作为例子,然后要做的是打印类名,就像obj->GetClassName()这样。
void Print(? ? ? * obj)
{
std::cout << obj->GetClassName() << std::endl;
}
这里我们需要的是一个类型来保证有GetClassName这个函数,这就是所谓的接口。我们可以把???类当做Printable,然后创建一个新的类叫做Printable,它会创建一个public的virtual字符串函数,返回一个字符串,GetClassName函数是纯虚的。
class Printable
{
public:
virtual std::string GetClassName() = 0;
};
然后我们要让Entity去实现这个接口。此时Player已经是一个Entity了,所以不需要实现Printable接口。
class Entity :public Printable
{
public:
virtual std::string GetName() { return "Entity"; }
};
如果Player不是Entity的子类,我们需要添加一个逗号来实现这个接口。
class Player :public Entity,Printable
{
private:
std::string m_Name;
public:
Player(const std::string& name) :m_Name(name) {}
std::string GetName()override { return m_Name; }
};
虽然我们把Printable叫做接口,但它其实只是一个类。因为它不过有个纯虚函数而已。
我们继续在Entity类添加一个GetClassName函数,此时无法实例化的问题就解决了。
但还会出现一些问题,我们还没有为Player提供一个覆写函数,如果现在去打印,调用Print函数,分别使用e和p参数。我们会发现打印了两次Entity,因为我们还没有在Player中提供定义
因此我们在第12行加上override,在第22行覆写函数GetClassName,就会得到正确的类名。
所有这些都来自于一个Print函数,它接受Printable作为参数。它不管具体是什么类。
我们也可以创建一个完全不同的类。比如A是Printable类的子类,A就必须有GetClassName函数,并且实现了Printable这个接口,即实现这个函数。
class A:public Printable
{
public:
virtual std::string GetClassName()override { return "A"; }
};
然后我们可以调用打印,因为A是一个Printable,它保证了A类有GetClassName函数。但不建议这么写代码因为会造成内存泄漏。



