一、抽象类
抽象类,why?
在前面的一直使用的animal
例子里,子类需要重写父类的虚函数。那么,假如我子类忘写了,岂不是就报错了。有没有办法,强制子类必须重写虚函数?这就是抽象类的意义。
进一步,假如我们写了一个模板接口,继承该模板的子类都重写规定的函数,这就是由抽象类进一步成为接口类。
抽象类的定义
纯虚函数
如果子类都需要重写虚函数,父类的虚函数就不需要定义函数主体了,这时候父类的虚函数就可以是纯虚函数了。
定义虚函数只需要给函数加上=0
即可。
virtual int getValue() const = 0; // a pure virtual function
抽象类
只要包含纯虚函数的类,就会成为抽象类。
抽象类无法被实例化。
这是可以推论的,如果实例化抽象类,调用了纯虚函数,因为没有函数主体,计算机不知道执行什么。
这也是为什么抽象类的子类必须重写虚函数。
例子
先看一个非抽象类的例子:
#include <string>
#include <utility>
class Animal
{
protected:
std::string m_name;
// We're making this constructor protected because
// we don't want people creating Animal objects directly,
// but we still want derived classes to be able to use it.
Animal(const std::string& name)
: m_name{ name }
{
}
public:
std::string getName() const { return m_name; }
virtual const char* speak() const { return "???"; }
virtual ~Animal() = default;
};
class Cat: public Animal
{
public:
Cat(const std::string& name)
: Animal{ name }
{
}
const char* speak() const override { return "Meow"; }
};
这个例子中,如果cat
没有重写虚函数,就会得到错误的结果,我们通过更改animal
类中的虚函数,实现一个抽象类。
class Animal // This Animal is an abstract base class
{
protected:
std::string m_name;
public:
Animal(const std::string& name)
: m_name{ name }
{
}
const std::string& getName() const { return m_name; }
virtual const char* speak() const = 0; // note that speak is now a pure virtual function
virtual ~Animal() = default;
};
上面主要更改了两个地方:
①把speak()
改成纯虚函数;
②因为animal
包含了纯虚函数,不允许创建实例,所以构造函数不需要使用protected
关键字了。
调用的话,只需要记得重写虚函数,其他的基本不变。
#include <iostream>
#include <string>
class Cow: public Animal
{
public:
Cow(const std::string& name)
: Animal(name)
{
}
const char* speak() const override { return "Moo"; }
};
int main()
{
Cow cow{ "Betsy" };
std::cout << cow.getName() << " says " << cow.speak() << '\n';
return 0;
}
二、接口类
由于抽象类的子类必须重写虚函数,所以非常适合做接口,接口类只不过是抽象类的特例。
这里展示一个实例:
class IErrorLog
{
public:
virtual bool openLog(const char *filename) = 0;
virtual bool closeLog() = 0;
virtual bool writeError(const char *errorMessage) = 0;
virtual ~IErrorLog() {} // make a virtual destructor in case we delete an IErrorLog pointer, so the proper derived destructor is called
};
从IErrorLog
继承的任何类都必须提供所有三个功能的实现才能实例化。您可以派生一个名为FileErrorLog
的类,其中openLog()
打开磁盘上的文件,closeLog()
关闭文件,而writeError()
将消息写入文件。您可以派生另一个名为ScreenErrorLog
的类,其中openLog()
和closeLog()
不执行任何操作,而writeError()
将消息打印在屏幕上的弹出消息框中。
假如我需要编写一些使用错误日志的代码,编写代码以使其直接包含FileErrorLog或ScreenErrorLog,但是不确定接受的到底是谁,这时候参数类型该选什么呢?
这个时候就该多态一展拳脚了!参数类型写IErrorLog
即可!
不要忘记为接口类包括虚拟析构函数,这样,如果删除了指向该接口的指针,则将调用正确的派生析构函数。
接口类已经变得非常流行,因为它们易于使用,易于扩展和易于维护。实际上,某些现代语言(例如Java和C#)添加了“ interface”关键字,该关键字使程序员可以直接定义接口类,而不必将所有成员函数明确标记为抽象。此外,尽管Java(版本8之前的版本)和C#不允许您在常规类上使用多重继承,但它们将使您可以随意继承多个接口。因为接口没有数据且没有函数体,所以它们避免了许多具有多重继承的传统问题,同时仍提供了很大的灵活性。
三、虚基类
为什么需要虚基类,why
对于如下图所示的继承关系,当copier
实例化的时候,父类PoweredDevice
就会实例化两次。可能某些情况下,需要这么做,但是如果在某些场景下,只需要让父类PoweredDevice
实例化一次呢?
这时候就需要虚基类,让虚基类只构造一次。
虚基类定义
要共享基类,只需在派生类的继承列表中插入virtual
关键字。这将创建所谓的虚拟基类,这意味着只有一个基对象。基础对象在继承树中的所有对象之间共享,并且仅构造一次。这是一个示例(为了简单起见,没有构造函数)显示了如何使用virtual
关键字创建共享基类:
class PoweredDevice
{
};
class Scanner: virtual public PoweredDevice
{
};
class Printer: virtual public PoweredDevice
{
};
class Copier: public Scanner, public Printer
{
};
接下来还有一个问题,如果PoweredDevice
只创造一次,是在哪个类里面实例的?
在上面的例子中,只创造一次的话是在Copier
类里创造的。
如果一个类继承了一个或多个具有虚拟父类的类,则派生程度最高的类负责构造虚拟基类。