26 / 80 · C++11 · 约 8 分钟
抽象类 Abstract class 与纯虚函数定义
抽象类不能创建完整对象,但可以拥有状态、构造函数和普通实现。纯虚表示具体派生类必须提供非纯的最终覆盖者,不等于函数不能有定义;纯虚析构在实际销毁派生对象时仍然需要可用定义。
抽象性看最终覆盖者
虚函数声明末尾的 = 0 是纯说明符,不是把函数指针设为空。只要类还存在最终覆盖者为纯虚的虚函数,它就是抽象类,不能直接构造完整对象;指针、引用及派生对象内部的基类子对象仍然可以使用这个类型。
抽象类可以有构造函数、数据成员以及普通成员函数,用于建立共同状态或固定流程。派生类也可以把已实现的虚函数重新声明为纯虚,表达更具体层次尚未完成的要求。最终是否可实例化,不能只看当前类有没有写出 = 0。
纯虚可以提供可复用定义
纯虚函数可以在类外定义实现,但类内不能在同一个声明上同时写纯说明符和函数体。派生类可以通过 Base::f() 显式限定调用这个实现,再增加自己的处理;这次限定调用不会动态分派,因而不会递归回到派生覆盖函数。
拥有定义并不会取消纯虚属性。示例 Job::cost 返回共同基础成本,但 Job 仍是抽象类型,ConcreteJob 必须提供自己的覆盖。纯虚析构同样需要定义,因为派生对象销毁时必然继续销毁基类子对象,基类析构不能缺席。
不要从构造阶段召唤未完成的派生层
抽象类构造函数可以调用普通成员函数,也能进行合法的基类阶段工作,但不能对正在构造的对象直接或间接进行纯虚调用;析构阶段同样如此。这是未定义行为,不会因为你给纯虚函数写了类外定义就自动变安全。
如果需要共享初始化逻辑,提取非虚辅助函数并显式调用;需要派生行为时,等对象构造完成后再经接口调用。示例只在完整 ConcreteJob 已存在时调用 cost,既展示接口约束,也避免把构造顺序与运行期扩展点混在一起。
容易答错的地方
- 纯虚函数存在定义不代表自动满足派生类的实现责任;最终覆盖者仍为纯虚时,派生类仍抽象。
- 纯虚析构的定义不是可选装饰;创建并销毁派生对象会需要它,遗漏常表现为链接错误。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <type_traits>
class Job {
public:
virtual int cost() const = 0;
virtual ~Job() = 0;
};
int Job::cost() const { return 5; }
Job::~Job() = default;
class ConcreteJob final : public Job {
public:
int cost() const override { return Job::cost() + 3; }
};
int main() {
static_assert(std::is_abstract<Job>::value, "abstract interface");
static_assert(!std::is_abstract<ConcreteJob>::value, "complete implementation");
ConcreteJob job;
const Job& interface = job;
assert(interface.cost() == 8);
assert(job.Job::cost() == 5);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-abstract.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
只删除 ConcreteJob::cost 的覆盖定义,保留 Job::cost 的类外定义,ConcreteJob 能否实例化?
查看参考答案
不能。ConcreteJob 继承的 cost 最终覆盖者仍是纯虚的 Job::cost,类外函数体只允许显式限定复用,不改变纯说明符。必须恢复一个非纯覆盖函数;它可以仅写 return Job::cost();,但这个派生声明本身仍然必要。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。