Learn C++学习笔记:第十二章—抽象类、虚基类的定义、用途

一、抽象类

抽象类,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类里创造的。

如果一个类继承了一个或多个具有虚拟父类的类,则派生程度最高的类负责构造虚拟基类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值