纯虚(抽象)函数与抽象基类
迄今为止,我们编写的所有虚函数都带有函数体(定义)。然而,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
};
当我们为类添加纯虚函数时,实际上是在表达:“此函数的具体实现由派生类负责”。
引入纯虚函数带来两大后果:
- 任何含有一个或多个纯虚函数的类都成为抽象基类(abstract base class),不可实例化。
- 任何派生类必须给出该函数的实现,否则该派生类同样是抽象基类。
纯虚函数示例
在先前示例中,我们编写了简单的 Animal
基类,并派生出 Cat
与 Dog
。原代码如下:
#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
的类必须为全部三个函数提供实现方可实例化。例如,可实现 FileErrorLog
、ScreenErrorLog
或 EmailErrorLog
等。
若函数直接依赖具体日志类:
double mySqrt(double value, FileErrorLog& log) { ... }
则调用者被迫使用 FileErrorLog
;若改为:
double mySqrt(double value, IErrorLog& log) { ... }
调用者可自由传入任何符合接口的实现,函数更独立、更灵活。
务必为接口类提供虚析构函数,以便通过接口指针删除派生对象时能调用正确的析构函数。
接口类因其易用、易扩展、易维护而广受欢迎。Java、C# 等现代语言甚至提供 interface
关键字直接支持接口定义,并允许多接口继承,避免传统多重继承的复杂性。
纯虚函数与虚表
为保持一致性,抽象类仍拥有虚表。其构造函数或析构函数可能调用虚函数,需正确解析到本类版本(因派生类尚未构造或已析构)。
若类含纯虚函数,其虚表对应条目通常置为 nullptr
或指向一个打印错误的通用函数(有时名为 __purecall
)。