C++笔记(面向对象)虚析构函数 纯虚函数 抽象类 final、override关键字
为什么构造函数不可以是虚函数呢?
虚函数调用只需要 “部分的” 信息,即只需要知道函数接口,而不需要对对象的具体类型。但是构建一个对象,却必须知道具体的类型信息。如果你调用一个虚构造函数,编译器怎么知道你想构建是继承树上的哪种类型呢?所以构造函数不能为虚。
- 构造函数的用途:1)创建对象,2)初始化对象中的属性,3)类型转换。
- 在类中定义了虚函数就会有一个虚函数表(vftable),对象模型中就含有一个指向虚表的指针(_vfptr)。在定义对象时构造函数设置虚表指针指向虚函数表。
- 使用指针和引用调用虚函数,在编译只需要知道函数接口,运行时指向具体对象,才能关联具体对象的虚方法(通过虚函数指针查虚函数表得到具体对象中的虚方法)
- 构造函数是类的一个特殊的成员函数:1)定义对象由系统自动调用构造函数,对象自己是不可以调用构造函数;2)构造函数的调用属于静态联编,在编译时必须知道具体的类型信息。
- 如果构造函数可以定义为虚构造函数,使用指针调用虚构造函数,如果编译器采用静态联编,构造函数就不能为虚函数。如果采用动态联编,运行时指针指向具体对象,使用指针调用构造函数,相当于已经实例化的对象在调用构造函数,这是不容许的调用,对象的构造函数只执行一次。
- 如果指针可以调用虚构造函数,通过查虚函数表,调动虚构造函数,那么,当指针为 nullptr,如何查虚函数表呢?
- 构造函数的调用是在编译时确定,如果是虚构造函数,编译器怎么知道你想构建是继承树上的哪种类型呢?总结:构造函数不允许是虚函数。
就是为每一个要构建的类型再创建一个对应的 factory,把问题放到 factory 的 make 方法中去解决。这也是 C++ 中的通用解决方案。
虚析构函数:析构函数是类的一个特殊的成员函数:
1)当一个对象的生命周期结束时,系统会自动调用析构函数注销该对象并进行善后工作,对象自身也可以调用析构函数;
2)析构函数的善后工作是:释放对象在生命期内获得的资源(如动态分配的内存,内核资源);3)析构函数也用来执行对象即将被撤销之前的任何操作。
根据赋值兼容规则,可以用基类的指针指向派生类对象,如果使用基类型指针指向动态创建的派生类对象,由该基类指针撤销派生类对象,则必须将析构函数定义为虚函数,实现多态性,自动调用派生类析构函数,否则可能存在内存泄漏问题。总结:在实现运行时的多态,无论其他程序员怎样调用析构函数都必须保证不出错,所以必须把析构函数定义为虚函数。
注意:类中没有虚函数,就不要把析构函数定义为虚。
1. 虚析构函数 (Virtual Destructor)
为什么要用虚析构函数?
问题场景:
cpp
Base* ptr = new Derived(); delete ptr; // 如果析构函数不是虚的,只会调用Base的析构函数!
后果:
派生类的析构函数不被调用
派生类中分配的资源无法释放 → 内存泄漏
其他清理工作无法执行
解决方案:
cpp
class Base {
public:virtual ~Base() = default; // 虚析构函数
};工作原理:
通过虚函数表(vtable)实现多态析构
delete ptr时,通过vptr找到实际的析构函数地址先调用派生类析构函数,再自动调用基类析构函数
重要规则:
任何可能被继承的类都应该有虚析构函数
如果类有虚函数,它必须有虚析构函数
STL容器存储多态对象时,必须用虚析构函数
2. 纯虚函数 (Pure Virtual Function)
定义和语法:
cpp
virtual 返回类型 函数名(参数) = 0;
特点:
只有声明,没有实现(在基类中不提供函数体)
使类成为抽象类
派生类必须实现所有纯虚函数才能实例化
示例:
cpp
class Drawable {
public:virtual void draw() const = 0; // 纯虚函数virtual ~Drawable() = default;
};特殊 case:纯虚析构函数
cpp
class AbstractBase {
public:virtual ~AbstractBase() = 0; // 纯虚析构函数
};
// 但必须提供实现!
AbstractBase::~AbstractBase() {}3. 抽象类 (Abstract Class)
定义:
包含至少一个纯虚函数的类
特性:
❌ 不能实例化:
AbstractClass obj;// 编译错误✅ 可以定义指针和引用
✅ 可以有构造函数和析构函数
✅ 可以包含数据成员和普通成员函数
✅ 派生类必须实现所有纯虚函数才能实例化
使用场景:
定义接口规范
提供部分实现(模板方法模式)
阻止实例化
示例:
cpp
class Animal { // 抽象类
public:virtual void speak() const = 0; // 纯虚函数virtual void eat() = 0; // 纯虚函数// 普通函数void sleep() { cout << "Sleeping..." << endl; }virtual ~Animal() = default;
};4. final 关键字 (C++11)
两种用法:
1. 用于类:禁止继承
cpp
class Base final { // 这个类不能被继承// ...
};
// class Derived : public Base { }; // 错误!编译失败使用场景:
工具类、工具函数集合
性能敏感,避免虚函数开销
设计上不应该被扩展的类
2. 用于虚函数:禁止重写
cpp
class Base {
public:virtual void func() final { // 派生类不能重写这个函数// 实现}
};class Derived : public Base {// void func() override { } // 错误!编译失败
};使用场景:
基类中的关键算法,不允许修改
模板方法模式中的固定步骤
5. override 关键字 (C++11)
为什么要用 override?
问题: 意外的函数隐藏
cpp
class Base {
public:virtual void func(int x) { }
};class Derived : public Base {
public:virtual void func(double x) { } // 本想重写,实际是隐藏!
};解决方案:
cpp
class Derived : public Base {
public:void func(int x) override { } // 明确表示要重写
};override 的好处:
编译器检查:确保函数签名与基类完全匹配
代码清晰:明确表达设计意图
防止错误:避免拼写错误、参数类型错误等
便于维护:让阅读者立即知道这是重写函数
使用规则:
只能用于虚函数重写
函数签名必须与基类完全一致(包括const、引用限定符等)
6. 完整设计示例
良好的类层次设计:
cpp
// 抽象基类:定义接口
class Shape {
public:// 纯虚函数:接口规范virtual double area() const = 0;virtual double perimeter() const = 0;virtual void draw() const = 0;// 普通虚函数:有默认实现virtual void scale(double factor) {// 默认实现}// 虚析构函数:必须!virtual ~Shape() = default;// 普通成员函数void printInfo() const {cout << "Area: " << area() << ", Perimeter: " << perimeter() << endl;}
};// 具体实现类
class Circle final : public Shape { // final:不允许进一步继承
private:double radius;
public:// 必须实现所有纯虚函数double area() const override {return 3.14159 * radius * radius;}double perimeter() const override {return 2 * 3.14159 * radius;}void draw() const override {cout << "Drawing Circle" << endl;}// 重写普通虚函数void scale(double factor) override {radius *= factor;}
};7. 综合对比表格
| 特性 | 虚析构函数 | 纯虚函数 | 抽象类 | final | override |
|---|---|---|---|---|---|
| 作用 | 安全的多态删除 | 定义接口 | 不能实例化的基类 | 禁止继承/重写 | 明确重写意图 |
| 语法 | virtual ~Class() | virtual func() = 0 | 含纯虚函数 | class A final 或 virtual func() final | void func() override |
| 强制性 | 强烈推荐 | 派生类必须实现 | 不能实例化 | 编译时强制 | 编译时检查 |
| 使用场景 | 所有基类 | 接口定义 | 接口、部分实现 | 工具类、关键方法 | 所有虚函数重写 |
8. 最佳实践总结
设计原则:
基类法则:公有继承的基类必须有虚析构函数
接口清晰:使用纯虚函数明确接口契约
意图明确:总是使用
override标识重写限制合理:适当使用
final防止误用资源安全:结合RAII和虚析构函数确保资源释放
代码检查清单:
基类有虚析构函数吗?
重写虚函数用了override吗?
不该被继承的类用final了吗?
接口类用纯虚函数定义清楚了吗?
抽象类确实不需要实例化吗?
