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->show(); // 调用Base::show()
delete ptr;
return 0;
}
3)注意事项
final修饰类时,必须写在类名后面(如class A final {}),写在类定义外部会报错。
final修饰虚函数时,仅禁止该虚函数被进一步重写,不影响派生类继承该函数并使用。
final和override可同时用于虚函数(顺序可互换),表示"重写基类虚函数,且禁止后续子类重写该函数",如void show() final override {}。
final不能用于非虚函数,修饰非虚函数无意义,且可能导致编译错误。
五、总结(核心关联与记忆要点)
-
虚析构函数:解决多态场景下的内存泄漏,核心是"动态绑定析构函数",基类析构加virtual,派生类析构自动继承虚特性。
-
纯虚函数:仅声明不实现(可类外提供默认实现),强制派生类重写,是抽象类的核心标识。
-
抽象类:含至少一个纯虚函数,无法实例化,用于定义接口规范,派生类必须重写所有纯虚函数才能实例化。
-
override:显式标识虚函数重写,编译期检查签名一致性,避免隐蔽错误。
-
final:限制类的继承或虚函数的重写,规范类族结构,提升代码安全性。
关联要点:抽象类的析构函数通常声明为虚析构函数;纯虚函数是抽象类的必要条件;override和final仅作用于虚函数(final可修饰类),均为编译期检查,不影响运行时性能。掌握这五个知识点,能有效规范面向对象代码,避免多态和继承中的常见陷阱,提升代码的可读性、可维护性和安全性。