抽象类与纯虚函数
定义与作用
纯虚函数是通过 = 0 声明的虚函数,它强制派生类必须提供实现:
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
};
抽象类是包含至少一个纯虚函数的类。抽象类不能直接实例化——它只定义接口,具体实现交给派生类。抽象类解决了"基类本身不合理但需要统一接口"的编程痛点。
核心原理
抽象类的设计层次
纯虚析构函数的特殊性
派生类析构时总是调用基类析构函数。如果基类析构函数是纯虚的,必须提供函数体(否则链接错误):
class AbstractBase {
public:
virtual ~AbstractBase() = 0; // 纯虚析构声明
};
AbstractBase::~AbstractBase() = default; // 必须在类外提供定义
完整示例
示例一:支付系统接口设计
场景说明:设计一个在线支付系统,不同支付方式共享统一接口。
#include <iostream>
#include <memory>
#include <vector>
#include <string>
// 抽象接口:定义支付协议
class PaymentMethod {
public:
virtual ~PaymentMethod() = default;
// 纯虚函数:每个支付方式必须实现
virtual bool pay(double amount) = 0;
virtual std::string getName() const = 0;
virtual double getBalance() const = 0;
// 非纯虚函数:提供默认实现
virtual void printReceipt(double amount) const {
std::cout << "===== 支付凭据 =====" << std::endl;
std::cout << "支付方式: " << getName() << std::endl;
std::cout << "金额: ¥" << amount << std::endl;
std::cout << "余额: ¥" << getBalance() << std::endl;
}
};
class CreditCard : public PaymentMethod {
public:
CreditCard(std::string card_no, double limit)
: card_no_(std::move(card_no)), limit_(limit), balance_(limit) {}
bool pay(double amount) override {
if (amount <= 0 || amount > balance_) return false;
balance_ -= amount;
return true;
}
std::string getName() const override {
return "信用卡 (" + card_no_.substr(card_no_.size() - 4) + ")";
}
double getBalance() const override { return balance_; }
private:
std::string card_no_;
double limit_;
double balance_;
};
class Alipay : public PaymentMethod {
public:
explicit Alipay(double balance) : balance_(balance) {}
bool pay(double amount) override {
if (amount <= 0 || amount > balance_) return false;
balance_ -= amount;
return true;
}
std::string getName() const override { return "支付宝"; }
double getBalance() const override { return balance_; }
private:
double balance_;
};
class WeChatPay : public PaymentMethod {
public:
explicit WeChatPay(double balance) : balance_(balance) {}
bool pay(double amount) override {
if (amount <= 0 || amount > balance_) return false;
balance_ -= amount;
return true;
}
std::string getName() const override { return "微信支付"; }
double getBalance() const override { return balance_; }
private:
double balance_;
};
// 订单处理系统——只依赖抽象接口
class OrderProcessor {
public:
void addPaymentMethod(std::unique_ptr<PaymentMethod> method) {
methods_.push_back(std::move(method));
}
bool processOrder(double amount, size_t method_index) {
if (method_index >= methods_.size()) {
std::cerr << "无效的支付方式索引" << std::endl;
return false;
}
auto& method = methods_[method_index];
std::cout << "尝试使用 " << method->getName() << " 支付 ¥" << amount << std::endl;
if (method->pay(amount)) {
method->printReceipt(amount);
return true;
}
std::cerr << "支付失败!余额不足。" << std::endl;
return false;
}
private:
std::vector<std::unique_ptr<PaymentMethod>> methods_;
};
int main() {
OrderProcessor processor;
processor.addPaymentMethod(std::make_unique<CreditCard>("6222-1234-5678-9012", 10000));
processor.addPaymentMethod(std::make_unique<Alipay>(5000));
processor.addPaymentMethod(std::make_unique<WeChatPay>(3000));
processor.processOrder(199.99, 0); // 信用卡
processor.processOrder(8000, 1); // 支付宝(余额不足)
processor.processOrder(150.00, 2); // 微信支付
return 0;
}
预期输出:
尝试使用 信用卡 (9012) 支付 ¥199.99
===== 支付凭据 =====
支付方式: 信用卡 (9012)
金额: ¥199.99
余额: ¥9800.01
尝试使用 支付宝 支付 ¥8000
支付失败!余额不足。
尝试使用 微信支付 支付 ¥150
===== 支付凭据 =====
支付方式: 微信支付
金额: ¥150
余额: ¥2850
逐段分析:
PaymentMethod是纯抽象接口——pay()、getName()、getBalance()都是纯虚函数printReceipt()是非纯虚函数——提供默认实现,子类可选择覆盖OrderProcessor只依赖PaymentMethod*——不关心具体支付方式- 新增支付方式无需修改
OrderProcessor代码——符合开闭原则 - 三种支付方式各自实现
pay()的业务逻辑——多态的核心价值
示例二:纯虚析构函数的必要性
#include <iostream>
#include <memory>
class IResource {
public:
// 纯虚析构:强制类为抽象 + 保证正确析构
virtual ~IResource() = 0;
virtual void release() = 0;
virtual const char* name() const = 0;
};
// 必须在类外提供纯虚析构的定义
IResource::~IResource() {
std::cout << "IResource 析构" << std::endl;
}
class FileResource : public IResource {
public:
explicit FileResource(const char* path) : path_(path) {
std::cout << "打开文件: " << path_ << std::endl;
}
~FileResource() override {
std::cout << "关闭文件: " << path_ << std::endl;
}
void release() override {
std::cout << "释放文件资源: " << path_ << std::endl;
}
const char* name() const override { return path_; }
private:
const char* path_;
};
int main() {
std::cout << "===== 纯虚析构保证正确释放 =====" << std::endl;
{
std::unique_ptr<IResource> res =
std::make_unique<FileResource>("data.txt");
std::cout << "资源名: " << res->name() << std::endl;
res->release();
} // unique_ptr 正确调用 FileResource::~FileResource → IResource::~IResource
return 0;
}
预期输出:
===== 纯虚析构保证正确释放 =====
打开文件: data.txt
资源名: data.txt
释放文件资源: data.txt
关闭文件: data.txt
IResource 析构
逐段分析:
- 纯虚析构
virtual ~IResource() = 0;让IResource成为抽象类(不能实例化) - 但析构函数必须有函数体——派生类析构时隐式调用基类析构,链接器需要找到实现
IResource::~IResource() = default;在类外提供定义——解决这一矛盾- 纯虚析构确保通过基类指针
delete时正确调用派生类析构函数
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 纯虚析构忘了提供函数体 | 链接错误 | 类外 Abstract::~Abstract() {} |
| 派生类继承抽象类但未实现所有纯虚函数 | 派生类也是抽象类 | 检查是否遗漏 override 实现 |
| 抽象类中定义数据成员 | 设计异味 | 抽象接口不应有状态,用中间抽象类 |
| 纯虚函数在构造函数中调用 | 未定义行为 | 构造函数中不调用虚函数 |
= 0 只放在声明,定义时重复 | 语法错误 | = 0 仅出现在声明处 |
常见面试问题
抽象类和普通类的区别?为什么需要抽象类?——抽象类不能被实例化,至少含一个纯虚函数。用于定义接口规范,强制派生类实现特定方法,是面向接口编程的基础。
纯虚析构函数的使用场景和注意事项?——当希望类是抽象类但没有其他合适的纯虚函数时使用。必须在类外提供函数体(
Abstract::~Abstract() {}),否则链接错误。纯虚函数可以有实现吗?——可以有。
virtual void f() = 0;后仍可提供Base::f() { ... },派生类通过Base::f()显式调用。但纯虚函数有实现的场景非常罕见。抽象类能定义构造函数吗?——可以。抽象类构造函数会被派生类构造时调用,用于初始化抽象类的成员。但抽象类本身不能直接实例化。
接口类(Interface)和抽象类的区别?——接口类只包含纯虚函数和虚析构,无数据成员、无具体实现。抽象类可以有数据成员、非虚函数、部分实现。
小结
= 0声明纯虚函数,使类成为抽象类(不可实例化)- 抽象类定义接口规范,派生类必须实现所有纯虚函数
- 纯虚析构必须有类外定义——它在派生类析构时被调用
- 接口类 = 全部纯虚 + 无数据成员 + 虚析构
- 面向接口编程降低耦合——调用者只依赖抽象而非具体实现