纯虚函数、抽象基类与接口类

纯虚(抽象)函数与抽象基类

迄今为止,我们编写的所有虚函数都带有函数体(定义)。然而,C++ 允许创建一种特殊的虚函数——纯虚函数(pure virtual function,或称抽象函数 abstract function),它完全没有函数体。纯虚函数仅作为占位符,用于由派生类重新定义。

要声明纯虚函数,只需在函数原型后写 = 0 而无需给出实现:

#include <string_view>

class Base
{
public:
    std::string_view sayHi() const { return "Hi"; }          // 普通非虚函数
    virtual std::string_view getName() const { return "Base"; } // 普通虚函数
    virtual int getValue() const = 0;                        // 纯虚函数

    int doSomething() = 0; // 错误:非虚函数不能设为 0
};

当我们为类添加纯虚函数时,实际上是在表达:“此函数的具体实现由派生类负责”。

引入纯虚函数带来两大后果:

  1. 任何含有一个或多个纯虚函数的类都成为抽象基类(abstract base class),不可实例化。
  2. 任何派生类必须给出该函数的实现,否则该派生类同样是抽象基类。

纯虚函数示例

在先前示例中,我们编写了简单的 Animal 基类,并派生出 CatDog。原代码如下:

#include <string>
#include <string_view>

class Animal
{
protected:
    std::string m_name {};
    Animal(std::string_view name) : m_name{ name } {}

public:
    const std::string& getName() const { return m_name; }
    virtual std::string_view speak() const { return "???"; }
    virtual ~Animal() = default;
};

class Cat: public Animal
{
public:
    Cat(std::string_view name) : Animal{ name } {}
    std::string_view speak() const override { return "Meow"; }
};

class Dog: public Animal
{
public:
    Dog(std::string_view name) : Animal{ name } {}
    std::string_view speak() const override { return "Woof"; }
};

我们通过将构造函数设为 protected 来阻止直接实例化 Animal,但仍可写出未重写 speak() 的派生类,例如:

class Cow : public Animal
{
public:
    Cow(std::string_view name) : Animal{ name } {}
    // 忘记重写 speak
};

int main()
{
    Cow cow{"Betsy"};
    std::cout << cow.getName() << " says " << cow.speak() << '\n';
}

程序输出:Betsy says ???,这显然不是我们想要的结果。

更优方案是使用纯虚函数:

class Animal // 现在是抽象基类
{
protected:
    std::string m_name {};
public:
    Animal(std::string_view name) : m_name{ name } {}
    const std::string& getName() const { return m_name; }
    virtual std::string_view speak() const = 0; // 纯虚函数
    virtual ~Animal() = default;
};

几点说明:

  • speak() 现为纯虚函数,Animal 成为抽象基类,不可实例化;因此无需再把构造函数设为 protected
  • 若派生类未实现 speak(),则该派生类同样为抽象基类;尝试实例化时将产生编译错误:
error: variable type 'Cow' is an abstract class
note: unimplemented pure virtual method 'speak' in 'Cow'

继续补全 Cow

class Cow: public Animal
{
public:
    Cow(std::string_view name) : Animal{ name } {}
    std::string_view speak() const override { return "Moo"; }
};

int main()
{
    Cow cow{ "Betsy" };
    std::cout << cow.getName() << " says " << cow.speak() << '\n';
}

输出:Betsy says Moo

纯虚函数适用于:基类需要声明某函数,但仅派生类知道其具体实现;强制派生类必须实现该函数,防止遗漏。

通过基类引用/指针调用纯虚函数

与普通虚函数一样,纯虚函数也可通过基类引用或指针调用:

int main()
{
    Cow cow{ "Betsy" };
    Animal& a{ cow };
    std::cout << a.speak(); // 动态绑定至 Cow::speak(),输出 "Moo"
}

注意:含纯虚函数的类亦应提供虚析构函数。

带有定义的纯虚函数

纯虚函数可以附带定义:

class Animal
{
    ...
    virtual std::string_view speak() const = 0;
};

std::string_view Animal::speak() const { return "buzz"; } // 定义须放在类外
  • 函数仍为纯虚函数(因 = 0),Animal 仍为抽象基类,不可实例化。
  • 派生类必须仍提供 speak() 的实现,否则亦为抽象类。
  • 若派生类愿意使用基类默认实现,可显式调用之:
class Dragonfly: public Animal
{
public:
    std::string_view speak() const override
    {
        return Animal::speak(); // 使用基类默认实现
    }
};

VS 用户注意:Visual Studio 允许在类内直接为纯虚函数给出实现,但这不符合 C++ 标准,且无法关闭。

析构函数也可声明为纯虚,但必须提供定义,以便派生类析构时能够正确调用。

接口类

接口类是没有成员变量且所有函数均为纯虚的类。其用途是规定派生类必须实现的一组功能,而具体实现细节完全交给派生类。

接口类通常以 I 开头命名,例如:

class IErrorLog
{
public:
    virtual bool openLog(std::string_view filename) = 0;
    virtual bool closeLog() = 0;
    virtual bool writeError(std::string_view errorMessage) = 0;
    virtual ~IErrorLog() {} // 虚析构确保正确析构派生对象
};

任何继承 IErrorLog 的类必须为全部三个函数提供实现方可实例化。例如,可实现 FileErrorLogScreenErrorLogEmailErrorLog 等。

若函数直接依赖具体日志类:

double mySqrt(double value, FileErrorLog& log) { ... }

则调用者被迫使用 FileErrorLog;若改为:

double mySqrt(double value, IErrorLog& log) { ... }

调用者可自由传入任何符合接口的实现,函数更独立、更灵活。

务必为接口类提供虚析构函数,以便通过接口指针删除派生对象时能调用正确的析构函数。

接口类因其易用、易扩展、易维护而广受欢迎。Java、C# 等现代语言甚至提供 interface 关键字直接支持接口定义,并允许多接口继承,避免传统多重继承的复杂性。

纯虚函数与虚表

为保持一致性,抽象类仍拥有虚表。其构造函数或析构函数可能调用虚函数,需正确解析到本类版本(因派生类尚未构造或已析构)。

若类含纯虚函数,其虚表对应条目通常置为 nullptr 或指向一个打印错误的通用函数(有时名为 __purecall)。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"