C++传记(面向对象)虚析构函数 纯虚函数 抽象类 final、override关键字

C++面向对象编程的核心是封装、继承与多态,而虚析构函数、纯虚函数、抽象类以及final、override关键字,是实现多态、规范继承关系、避免开发陷阱的关键知识点。本文将以笔记形式,逐一拆解每个概念的定义、作用、使用场景及注意事项,结合代码示例帮助快速理解与记忆,适配日常学习与开发查阅。

一、虚析构函数

1. 定义

在基类的析构函数前添加virtual关键字,即可将其声明为虚析构函数,语法格式如下:

复制代码
class 基类名 {
public:
    virtual ~基类名() {
        // 析构函数体(释放基类资源)
    }
};

虚析构函数的核心特性的是:它会被派生类继承,且支持运行时动态绑定------当通过基类指针/引用删除派生类对象时,会自动调用派生类的析构函数,再调用基类的析构函数,确保所有资源被完整释放。

2. 核心作用:解决多态场景下的内存泄漏

在多态编程中,我们常使用"基类指针指向派生类对象"的方式实现动态调用。若基类析构函数不是虚函数,删除基类指针时,编译器会仅根据指针类型(基类)调用基类析构函数,而派生类的析构函数不会被执行,导致派生类中动态分配的资源(如堆内存、文件句柄)无法释放,造成内存泄漏。

示例:非虚析构函数导致的内存泄漏

复制代码
#include <iostream>
using namespace std;

class Base { // 基类:析构函数非虚
public:
    Base() { cout << "Base构造" << endl; }
    ~Base() { cout << "Base析构(仅释放基类资源)" << endl; }
};

class Derived : public Base { // 派生类
private:
    int* arr; // 派生类动态分配的资源
public:
    Derived() { 
        arr = new int[10];
        cout << "Derived构造(分配堆内存)" << endl; 
    }
    ~Derived() { 
        delete[] arr; // 释放派生类资源
        cout << "Derived析构(释放堆内存)" << endl; 
    }
};

int main() {
    Base* ptr = new Derived(); // 基类指针指向派生类对象
    delete ptr; // 仅调用Base析构,Derived析构未执行,arr内存泄漏
    return 0;
}

运行结果:

复制代码
Base构造
Derived构造(分配堆内存)
Base析构(仅释放基类资源)

修改方案:将基类析构函数改为虚析构函数,即可解决内存泄漏问题:

复制代码
virtual ~Base() { cout << "Base析构(仅释放基类资源)" << endl; }

修改后运行结果:

复制代码
Base构造
Derived构造(分配堆内存)
Derived析构(释放堆内存)
Base析构(仅释放基类资源)

3. 注意事项

虚析构函数的底层依赖虚函数表(vtable)机制,与普通虚函数的调用原理一致------基类虚析构函数的地址存入基类vtable,派生类析构函数会覆盖该条目,确保动态绑定生效。

派生类的析构函数会自动继承虚特性,即使不显式添加virtual关键字,也仍是虚函数,但为了代码清晰,建议显式标注。可以声明纯虚析构函数(virtual ~基类名() = 0;),此时基类会成为抽象类,但必须为纯虚析构函数提供类外定义(否则会导致链接错误),因为派生类析构执行后会自动调用基类析构。若类不涉及继承(仅作为独立类),无需将析构函数声明为虚函数,避免不必要的性能开销。

二、纯虚函数

1. 定义

纯虚函数是一种特殊的虚函数,仅在基类中声明,不提供具体实现,语法格式为:

复制代码
class 基类名 {
public:
    virtual 返回值类型 函数名(参数列表) = 0; // =0 表示纯虚函数,无函数体
};

"=0"的含义是告诉编译器:该函数没有默认实现,必须由派生类重写后才能使用。需要注意的是,纯虚函数并非绝对不能有实现,可在类外提供默认实现,但派生类仍需重写该函数,仅能通过基类作用域显式调用基类的默认实现。

2. 核心作用

定义接口规范:强制派生类必须实现该函数,确保类族拥有统一的接口,避免派生类遗漏关键功能实现。

标识抽象类:包含纯虚函数的类会自动成为抽象类,无法实例化,只能作为基类供派生类继承,是实现C++接口的核心方式。

支撑多态:纯虚函数是多态的核心载体,通过基类指针/引用调用纯虚函数时,会动态绑定到派生类的具体实现。

3. 示例

复制代码
#include <iostream>
using namespace std;

class Shape { // 基类,包含纯虚函数
public:
    virtual double calculateArea() = 0; // 纯虚函数:计算面积(接口)
    virtual ~Shape() {} // 虚析构函数,避免内存泄漏
};

class Circle : public Shape { // 派生类,必须重写纯虚函数
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double calculateArea() override { // 重写纯虚函数
        return 3.14 * radius * radius;
    }
};

class Rectangle : public Shape { // 派生类,必须重写纯虚函数
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double calculateArea() override {
        return width * height;
    }
};

4. 注意事项

纯虚函数仅在基类中声明,派生类必须重写所有纯虚函数,否则派生类也会成为抽象类,无法实例化。

纯虚函数不能直接调用,只能通过基类指针/引用调用派生类的重写版本。

纯虚函数的声明必须在类内,若需提供实现,需在类外定义(格式:返回值类型 基类名::函数名(参数列表) { ... })。

三、抽象类

1. 定义

抽象类是指**包含至少一个纯虚函数的类**,它是一种"接口类",仅用于定义类族的公共接口和部分默认实现,无法直接实例化对象------本质是为派生类提供统一的基类模板,强制派生类遵循接口规范。

补充:完全由纯虚函数组成、无任何数据成员和普通成员函数的抽象类,称为"纯抽象类",对应其他语言中的"接口",仅用于定义接口规范,不提供任何实现。

2. 核心特性

无法实例化:无论抽象类是否有其他成员,只要包含纯虚函数,就不能创建对象(如Shape s;会报错),但可以定义抽象类的指针或引用,用于指向派生类对象。

强制派生类实现接口:派生类继承抽象类后,必须重写所有纯虚函数,否则该派生类仍为抽象类,无法实例化。

可包含非纯虚成员:抽象类可以有普通成员函数、数据成员和构造函数(用于派生类初始化),这些成员会被派生类继承和使用。

3. 示例:抽象类的使用

复制代码
#include <iostream>
using namespace std;

// 抽象类:动物类
class Animal {
public:
    Animal(string name) : name(name) {} // 构造函数,用于初始化
    virtual void makeSound() = 0; // 纯虚函数(接口)
    void sleep() { // 普通成员函数,提供默认实现
        cout << name << "在睡觉" << endl;
    }
protected:
    string name; // 数据成员
};

// 派生类:猫(非抽象类,重写纯虚函数)
class Cat : public Animal {
public:
    Cat(string name) : Animal(name) {}
    void makeSound() override {
        cout << name << "喵喵叫" << endl;
    }
};

// 派生类:狗(非抽象类,重写纯虚函数)
class Dog : public Animal {
public:
    Dog(string name) : Animal(name) {}
    void makeSound() override {
        cout << name << "汪汪叫" << endl;
    }
};

int main() {
    // Animal a("动物"); // 错误:抽象类不能实例化
    Animal* cat = new Cat("小花"); // 正确:抽象类指针指向派生类对象
    Animal* dog = new Dog("旺财");
    
    cat->makeSound(); // 动态绑定:调用Cat的makeSound
    dog->makeSound(); // 动态绑定:调用Dog的makeSound
    cat->sleep(); // 调用抽象类的普通成员函数
    
    delete cat;
    delete dog;
    return 0;
}

4. 注意事项

抽象类的构造函数不能用于创建对象,仅用于派生类初始化时调用(通过派生类构造函数的初始化列表)。

抽象类的析构函数建议声明为虚析构函数,避免通过抽象类指针删除派生类对象时出现内存泄漏。

复制代码
#include <iostream>
using namespace std;

class Base {
public:
    virtual void show(int a) { // 基类虚函数,参数为int
        cout << "Base::show(int): " << a << endl;
    }
};

class Derived : public Base {
public:
    // 正确:重写基类虚函数,签名一致,添加override
    void show(int a) override {
        cout << "Derived::show(int): " << a << endl;
    }
    
    // 错误:参数为double,与基类虚函数签名不匹配,编译器报错
    // void show(double a) override;
};

int main() {
    Base* ptr = new Derived();
    ptr->show(10); // 动态绑定,调用Derived::show
    delete ptr;
    return 0;
}

抽象类可以作为多重继承的基类,用于实现多接口(如一个类同时继承多个纯抽象类,实现多个接口)。

四、final、override关键字(C++11及以上)

C++11引入final和override两个关键字,核心作用是**规范继承关系、增强代码可读性和安全性**,均为编译期检查,不产生额外运行时开销,从语法层面避免继承和重写中的隐蔽错误。

1. override关键字

(1)定义与作用

override用于**明确标识派生类中的成员函数,是对基类虚函数的重写**,告诉编译器:该函数的意图是重写基类的虚函数,若函数签名(返回值、参数列表、const/volatile修饰)与基类虚函数不匹配,编译器会直接报错,避免因签名不一致导致的"隐式隐藏"问题------即看似重写,实则定义了一个新函数的错误。

(2)使用示例
复制代码
#include <iostream>
using namespace std;

class Base {
public:
    virtual void show(int a) { // 基类虚函数,参数为int
        cout << "Base::show(int): " << a << endl;
    }
};

class Derived : public Base {
public:
    // 正确:重写基类虚函数,签名一致,添加override
    void show(int a) override {
        cout << "Derived::show(int): " << a << endl;
    }
    
    // 错误:参数为double,与基类虚函数签名不匹配,编译器报错
    // void show(double a) override;
};

int main() {
    Base* ptr = new Derived();
    ptr->show(10); // 动态绑定,调用Derived::show
    delete ptr;
    return 0;
}
(3)注意事项

override仅能用于派生类的虚函数,且该函数必须重写基类的虚函数(基类函数需带virtual关键字),否则编译报错。

override仅用于编译期检查,不改变函数的虚特性,也不影响多态的实现。

复制代码
class FinalClass final { // 用final修饰类,禁止继承
public:
    void show() {
        cout << "FinalClass::show()" << endl;
    }
};

// 错误:FinalClass被final修饰,不能被继承
// class Derived : public FinalClass {};

若派生类函数未加override,即使签名与基类虚函数不一致,编译器也不会报错,会默认定义一个新函数,隐蔽性极强,建议所有重写虚函数都添加override。

2. final关键字

(1)定义与作用

final有两个核心用途:一是**禁止类被继承**,二是**禁止虚函数被进一步重写**,用于限制类的扩展和虚函数的重写,防止误用和过度扩展,同时可帮助编译器进行优化(如内联)。

(2)使用场景与示例

场景1:修饰类,禁止该类被继承

cpp 复制代码
class FinalClass final { // 用final修饰类,禁止继承
public:
    void show() {
        cout << "FinalClass::show()" << endl;
    }
};

// 错误:FinalClass被final修饰,不能被继承
// class Derived : public FinalClass {};

场景2:修饰虚函数,禁止该虚函数被派生类重写

cpp 复制代码
#include <iostream>
using namespace std;

class Base {
public:
    // 用final修饰虚函数,禁止派生类重写该函数
    virtual void show() final {
        cout << "Base::show()" << endl;
    }
};

class Derived : public Base {
public:
    // 错误:Base::show()被final修饰,不能重写
    // void show() override;
};

int main() {
    Base* ptr = new Derived();
    ptr-&gt;show(); // 调用Base::show()
    delete ptr;
    return 0;
}
3)注意事项

final修饰类时,必须写在类名后面(如class A final {}),写在类定义外部会报错。

final修饰虚函数时,仅禁止该虚函数被进一步重写,不影响派生类继承该函数并使用。

final和override可同时用于虚函数(顺序可互换),表示"重写基类虚函数,且禁止后续子类重写该函数",如void show() final override {}

final不能用于非虚函数,修饰非虚函数无意义,且可能导致编译错误。

五、总结(核心关联与记忆要点)

  1. 虚析构函数:解决多态场景下的内存泄漏,核心是"动态绑定析构函数",基类析构加virtual,派生类析构自动继承虚特性。

  2. 纯虚函数:仅声明不实现(可类外提供默认实现),强制派生类重写,是抽象类的核心标识。

  3. 抽象类:含至少一个纯虚函数,无法实例化,用于定义接口规范,派生类必须重写所有纯虚函数才能实例化。

  4. override:显式标识虚函数重写,编译期检查签名一致性,避免隐蔽错误。

  5. final:限制类的继承或虚函数的重写,规范类族结构,提升代码安全性。

关联要点:抽象类的析构函数通常声明为虚析构函数;纯虚函数是抽象类的必要条件;override和final仅作用于虚函数(final可修饰类),均为编译期检查,不影响运行时性能。掌握这五个知识点,能有效规范面向对象代码,避免多态和继承中的常见陷阱,提升代码的可读性、可维护性和安全性。

相关推荐
无巧不成书02182 小时前
30分钟入门Java:从历史到Hello World的小白指南
java·开发语言
2301_797172752 小时前
基于C++的游戏引擎开发
开发语言·c++·算法
有为少年3 小时前
告别“唯语料论”:用合成抽象数据为大模型开智
人工智能·深度学习·神经网络·算法·机器学习·大模型·预训练
比昨天多敲两行3 小时前
C++ 二叉搜索树
开发语言·c++·算法
Season4503 小时前
C++11之正则表达式使用指南--[正则表达式介绍]|[regex的常用函数等介绍]
c++·算法·正则表达式
Tisfy4 小时前
LeetCode 2839.判断通过操作能否让字符串相等 I:if-else(两两判断)
算法·leetcode·字符串·题解
问好眼4 小时前
《算法竞赛进阶指南》0x04 二分-1.最佳牛围栏
数据结构·c++·算法·二分·信息学奥赛
Birdy_x4 小时前
接口自动化项目实战(1):requests请求封装
开发语言·前端·python
海海不瞌睡(捏捏王子)4 小时前
C++ 知识点概要
开发语言·c++