解码继承——代码复用与层次化设计
继承的核心概念
继承的核心是基于已有类(基类)创建新类(派生类),实现代码复用和层次化的类结构设计,让派生类既能复用基类的属性和方法,又能扩展自身特有的功能,体现现实世界中事物的 “一般 - 特殊” 关系(如 “哺乳动物 - 猫 / 狗”“交通工具 - 汽车 / 自行车”)。

- 基类(父类):定义事物共性属性和行为的类,为派生类提供基础功能
- 派生类(子类):继承基类特性并扩展自身特有功能的类
- 核心作用:
- 代码复用:避免重复编写相同逻辑
- 层次化设计:构建事物的分类体系(如 "交通工具→汽车→轿车")
- 支持多态:实现 "一个接口,多种形态" 的编程范式
注意:只有 “类的实体成员(变量 / 函数)” 能被继承,与类的 “初始化 / 销毁 / 身份 / 关系(构造函数/析构函数/赋值运算符重载函数/友元关系/类的访问控制权限/虚函数表/静态成员的 “类属身份”——静态成员可继承,但类属关系不继承)” 相关的内容,都不继承。
继承的基本语法
#include <iostream>
#include <string>
using namespace std;
// 基类:哺乳动物(所有哺乳动物的共性)
class Mammal {
protected:
float bodyTemp; // 体温(哺乳动物为恒温动物)
float weight; // 体重
int age; // 年龄
public:
/**
* 哺乳动物构造函数
* @param temp 体温(默认37℃左右)
* @param w 体重(kg)
* @param a 年龄(岁)
* @note 初始化所有哺乳动物的共性属性
*/
Mammal(float temp = 37.0f, float w = 0.0f, int a = 0)
: bodyTemp(temp), weight(w), age(a) {}
/**
* 呼吸功能(所有哺乳动物共有的行为)
*/
void breathe() const {
cout << "用肺呼吸" << endl;
}
/**
* 哺乳行为(哺乳动物的核心特征)
*/
void lactate() const {
cout << "通过乳腺分泌乳汁哺育幼崽" << endl;
}
/**
* 展示基础生理信息
*/
void showBasicInfo() const {
cout << "体温:" << bodyTemp << "℃,体重:" << weight << "kg,年龄:" << age << "岁" << endl;
}
};
// 派生类:狗(继承自哺乳动物,添加狗的特性)
class Dog : public Mammal {
protected:
string breed; // 犬种
public:
/**
* 狗的构造函数
* @param temp 体温(狗正常体温38-39℃)
* @param w 体重
* @param a 年龄
* @param b 犬种名称
* @note 显式调用父类构造函数,初始化狗的特有属性
*/
Dog(float temp, float w, int a, string b)
: Mammal(temp, w, a), breed(b) {}
/**
* 狗的特有行为:吠叫
*/
void bark() const {
cout << breed << "汪汪叫" << endl;
}
/**
* 展示狗的基础信息
*/
void showDogInfo() const {
cout << "犬种:" << breed << ",";
showBasicInfo(); // 调用父类方法
}
};
// 派生类:猫(继承自哺乳动物,添加猫的特性)
class Cat : public Mammal {
protected:
string breed; // 猫种
public:
/**
* 猫的构造函数
* @param temp 体温(猫正常体温38-39℃)
* @param w 体重
* @param a 年龄
* @param b 猫种名称
*/
Cat(float temp, float w, int a, string b)
: Mammal(temp, w, a), breed(b) {}
/**
* 猫的特有行为:喵喵叫
*/
void miaow() const {
cout << breed << "喵喵叫" << endl;
}
/**
* 展示猫的基础信息
*/
void showCatInfo() const {
cout << "猫种:" << breed << ",";
showBasicInfo(); // 调用父类方法
}
};
// 派生类:金毛幼犬(继承自狗,添加金毛的特性)
class GoldenRetriever : public Dog {
private:
string strapColor; // 背带颜色(图示特征)
public:
/**
* 金毛幼犬构造函数
* @param temp 体温
* @param w 体重
* @param a 年龄(幼犬年龄较小)
* @param color 背带颜色
*/
GoldenRetriever(float temp, float w, int a, string color)
: Dog(temp, w, a, "金毛寻回犬"), strapColor(color) {}
/**
* 展示金毛幼犬的特有信息
*/
void showGoldenInfo() const {
showDogInfo(); // 调用父类方法
cout << "穿着" << strapColor << "背带裤" << endl;
}
};
// 派生类:哈士奇(继承自狗,添加哈士奇的特性)
class Husky : public Dog {
private:
string posture; // 姿态(图示特征:站立姿态)
public:
/**
* 哈士奇构造函数
* @param temp 体温
* @param w 体重
* @param a 年龄
* @param p 姿态描述
*/
Husky(float temp, float w, int a, string p)
: Dog(temp, w, a, "哈士奇"), posture(p) {}
/**
* 展示哈士奇的特有信息
*/
void showHuskyInfo() const {
showDogInfo(); // 调用父类方法
cout << "姿态:" << posture << endl;
}
};
// 派生类:英国短毛猫(继承自猫,添加英短的特性)
class BritishShorthair : public Cat {
private:
string hat; // 帽子(图示特征:棕色毛线帽)
public:
/**
* 英国短毛猫构造函数
* @param temp 体温
* @param w 体重
* @param a 年龄
* @param h 帽子描述
*/
BritishShorthair(float temp, float w, int a, string h)
: Cat(temp, w, a, "英国短毛猫"), hat(h) {}
/**
* 展示英短的特有信息
*/
void showBritishInfo() const {
showCatInfo(); // 调用父类方法
cout << "戴着" << hat << endl;
}
};
// 派生类:橘虎斑猫(继承自猫,添加橘猫的特性)
class OrangeTabby : public Cat {
private:
string furColor; // 毛色(图示特征:橘色虎斑)
public:
/**
* 橘虎斑猫构造函数
* @param temp 体温
* @param w 体重
* @param a 年龄
* @param color 毛色描述
*/
OrangeTabby(float temp, float w, int a, string color)
: Cat(temp, w, a, "橘虎斑猫"), furColor(color) {}
/**
* 展示橘猫的特有信息
*/
void showOrangeInfo() const {
showCatInfo(); // 调用父类方法
cout << "毛色:" << furColor << endl;
}
};
// 测试代码
int main() {
// 创建金毛幼犬实例
GoldenRetriever golden(38.5f, 8.0f, 1, "红色");
cout << "=== 金毛幼犬信息 ===" << endl;
golden.showGoldenInfo();
golden.bark(); // 调用狗的方法
golden.breathe(); // 调用哺乳动物的方法
cout << endl;
// 创建哈士奇实例
Husky husky(38.3f, 20.0f, 2, "灰白色站立姿态");
cout << "=== 哈士奇信息 ===" << endl;
husky.showHuskyInfo();
husky.bark();
husky.lactate(); // 调用哺乳动物的方法
cout << endl;
// 创建英国短毛猫实例
BritishShorthair british(38.7f, 4.5f, 3, "棕色毛线帽");
cout << "=== 英国短毛猫信息 ===" << endl;
british.showBritishInfo();
british.miaow(); // 调用猫的方法
british.breathe();
cout << endl;
// 创建橘虎斑猫实例
OrangeTabby orange(38.6f, 5.0f, 2, "橘色虎斑");
cout << "=== 橘虎斑猫信息 ===" << endl;
orange.showOrangeInfo();
orange.miaow();
orange.lactate();
return 0;
}
继承方式(public/protected/private)
权限继承规则表
| 继承方式 | 基类 public 成员在子类中的权限 | 基类 protected 成员在子类中的权限 | 基类 private 成员在子类中的权限 | 核心特点 / 适用场景 |
|---|---|---|---|---|
| 公有继承(public) | public | protected | 始终不可访问 | 体现 “is-a” 关系(如 “猫是哺乳动物”),子类对外保留基类的 public 接口,最常用。 |
| 保护继承(protected) | protected | protected | 始终不可访问 | 基类的 public 成员降级为 protected,对外隐藏接口,仅用于子类内部扩展,极少使用。 |
| 私有继承(private) | private | private | 始终不可访问 | 基类所有成员在子类中变为 private,对外完全隐藏,可模拟 “has-a” 关系(但更推荐用成员对象)。 |
公有继承(public)示例
class Base {
public:
int pub;
protected:
int pro;
private:
int pri;
public:
Base() : pub(1), pro(2), pri(3) {}
};
class PubDerived : public Base {
public:
void accessTest() {
pub = 10; // ✔ 继承为public,可访问
pro = 20; // ✔ 继承为protected,可访问
// pri = 30; // ❌ 基类private成员永远不可访问
}
};
int main() {
PubDerived d;
d.pub = 100; // ✔ public成员外部可访问
// d.pro = 200; // ❌ protected成员外部不可访问
return 0;
}
保护继承(protected)示例
class ProDerived : protected Base {
public:
void accessTest() {
pub = 10; // ✔ 继承为protected,可访问
pro = 20; // ✔ 继承为protected,可访问
}
// 使用using恢复成员访问权限(改为public)
using Base::pub;
};
int main() {
ProDerived d;
d.pub = 100; // ✔ 通过using恢复为public,外部可访问
// d.pro = 200; // ❌ 仍为protected,外部不可访问
return 0;
}
私有继承(private)示例
class PriDerived : private Base {
public:
void accessTest() {
pub = 10; // ✔ 继承为private,可访问
pro = 20; // ✔ 继承为private,可访问
}
// 使用using恢复成员访问权限(改为public)
using Base::pub;
};
int main() {
PriDerived d;
d.pub = 100; // ✔ 通过using恢复为public,外部可访问
// d.pro = 200; // ❌ 仍为private,外部不可访问
return 0;
}
继承中的构造与析构
执行顺序
- 构造顺序:基类构造函数 → 派生类成员对象构造函数 → 派生类构造函数(多级继承时,从最顶层基类依次向下)
- 析构顺序:派生类析构函数 → 派生类成员对象析构函数 → 基类析构函数(与构造顺序相反)
基类构造的调用方式
隐式调用(自动调用基类默认构造)
class Base {
public:
Base() { cout << "Base默认构造" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived构造" << endl; } // 隐式调用Base()
};
// 输出:Base默认构造 → Derived构造
显式调用(成员初始化列表指定)
class Base {
private:
int x;
public:
/**
* 基类带参构造函数
* @param val 初始化x的值
*/
Base(int val) : x(val) { cout << "Base带参构造:x=" << x << endl; }
};
class Derived : public Base {
private:
int y;
public:
/**
* 派生类构造函数
* @param a 传递给基类构造的参数
* @param b 初始化派生类成员y的参数
* @note 必须显式调用基类带参构造,否则编译错误
*/
Derived(int a, int b) : Base(a), y(b) {
cout << "Derived构造:y=" << y << endl;
}
};
// 使用:Derived d(10, 20);
// 输出:Base带参构造:x=10 → Derived构造:y=20
基类析构函数设为虚函数
非虚析构:编译期静态绑定
C++ 中,普通函数(包括非虚析构函数)是 “静态绑定”—— 编译器在编译阶段,会根据指针 / 引用的声明类型(而非实际指向的对象类型)来确定调用哪个函数。
如代码中:
Base* ptr = new Derived(); // ptr的声明类型是Base*,实际指向Derived对象
delete ptr; // 编译期绑定:根据ptr的声明类型(Base*),直接确定调用Base::~Base()
此时编译器只 “看到”ptr是Base*类型,因此直接绑定到基类的析构函数Base::~Base(),完全忽略ptr实际指向的是Derived对象。
虚析构:解决内存泄漏
class Base {
public:
virtual ~Base() { cout << "Base析构" << endl; } // 虚析构
};
class Derived : public Base {
public:
~Derived() override { cout << "Derived析构" << endl; }
};
// 使用基类指针指向派生类对象
int main() {
Base* ptr = new Derived();
delete ptr; // 若基类析构非虚,仅调用Base析构(内存泄漏);虚析构则先Derived后Base
return 0;
}
// 输出:Derived析构 → Base析构
同名成员的处理
当基类和派生类存在同名成员(属性 / 方法),派生类成员会隐藏基类成员,需用::作用域解析符访问基类版本:
class Base {
public:
int num = 10;
void show() { cout << "Base num:" << num << endl; }
};
class Derived : public Base {
public:
int num = 20;
void show() {
cout << "Derived num:" << num << endl;
cout << "Base num:" << Base::num << endl; // 访问基类同名属性
Base::show(); // 访问基类同名方法
}
};
// 使用:
Derived d;
d.show();
// 输出:
// Derived num:20
// Base num:10
// Base num:10
继承中的类型转换
向上转型(派生→基类)
派生类对象 / 指针 / 引用可自动转换为基类类型(安全,因为派生类包含基类所有成员):
#include <iostream>
#include <string>
using namespace std;
// 基类:交通工具
class Vehicle {
protected:
float speed; // 行驶速度
public:
Vehicle(float s = 0) : speed(s) {}
// 虚函数:支持多态(为后续向下转型的dynamic_cast做准备)
virtual void move() const {
cout << "以" << speed << "km/h移动" << endl;
}
};
// 派生类:汽车
class Car : public Vehicle {
private:
string brand; // 汽车品牌(派生类特有属性)
public:
Car(float s, string b) : Vehicle(s), brand(b) {}
// 重写基类虚函数
void move() const override {
cout << brand << "以" << speed << "km/h行驶" << endl;
}
// 派生类特有方法
void honk() const {
cout << brand << "鸣笛:滴滴!" << endl;
}
};
// 向上转型示例
int main() {
Car myCar(120, "宝马");
Vehicle& vRef = myCar; // 派生类引用→基类引用(无切片,支持多态)
Vehicle* vPtr = &myCar;// 派生类指针→基类指针(无切片,支持多态)
Vehicle vObj = myCar; // 派生类对象→基类对象(切片:仅复制基类部分)
// 验证效果
cout << "基类引用调用move:";
vRef.move(); // 多态:调用Car::move()
cout << "基类指针调用move:";
vPtr->move(); // 多态:调用Car::move()
cout << "基类对象调用move:";
vObj.move(); // 无多态:仅调用Vehicle::move()(切片后丢失派生类特性)
// vRef.honk(); // ❌ 错误:基类引用无法直接调用派生类特有方法
return 0;
}
关键说明:
- 指针 / 引用转型:无 “切片”,仅改变访问视角,仍指向原派生类对象,支持多态;
- 对象转型:发生 “切片”,编译器仅复制基类部分成员,派生类特有属性(如
brand)和方法(如honk())全部丢失,且无法触发多态。
向下转型(基类→派生)
需显式转换(推荐用dynamic_cast,运行时检查合法性,失败返回 nullptr):
#include <iostream>
#include <string>
using namespace std;
// 基类:交通工具(必须包含虚函数,否则dynamic_cast编译报错)
class Vehicle {
protected:
float speed;
public:
Vehicle(float s = 0) : speed(s) {}
virtual void move() const {
cout << "以" << speed << "km/h移动" << endl;
}
virtual ~Vehicle() {} // 虚析构:避免内存泄漏
};
// 派生类:汽车
class Car : public Vehicle {
private:
string brand;
public:
Car(float s, string b) : Vehicle(s), brand(b) {}
void move() const override {
cout << brand << "以" << speed << "km/h行驶" << endl;
}
void honk() const {
cout << brand << "鸣笛:滴滴!" << endl;
}
};
// 向下转型示例
int main() {
// 场景1:合法转型(基类指针实际指向派生类对象)
Vehicle* vPtr = new Car(100, "奔驰");
Car* cPtr = dynamic_cast<Car*>(vPtr); // 运行时检查:转型成功
if (cPtr) { // 判空:避免空指针访问
cout << "合法转型:";
cPtr->honk(); // 调用Car特有方法
}
// 场景2:非法转型(基类指针指向纯基类对象)
Vehicle* vPtr2 = new Vehicle();
Car* cPtr2 = dynamic_cast<Car*>(vPtr2); // 运行时检查:转型失败,返回nullptr
if (!cPtr2) {
cout << "非法转型:基类指针未指向派生类对象" << endl;
}
// 对比:static_cast(无运行时检查,风险高)
Vehicle* vPtr3 = new Vehicle(60);
Car* cPtr3 = static_cast<Car*>(vPtr3); // 编译通过,但逻辑非法
// cPtr3->honk(); // ❌ 未定义行为:访问不存在的派生类成员,程序崩溃
// 释放资源
delete vPtr;
delete vPtr2;
delete vPtr3;
return 0;
}
关键说明:
- dynamic_cast 前提:基类必须包含至少一个虚函数(否则编译报错),因为它依赖虚函数表实现运行时类型检查;
- 判空必要性:转型失败时返回
nullptr,若直接访问会导致空指针异常; - static_cast 风险:仅做编译期类型转换,无运行时检查,即使基类指针未指向派生类对象也能编译通过,运行时会触发未定义行为(如崩溃);
- 适用场景:仅当确认基类指针 / 引用实际指向目标派生类对象时,才需要向下转型(尽量避免,破坏多态抽象性)。
输出结果:
合法转型:奔驰鸣笛:滴滴!
非法转型:基类指针未指向派生类对象
多继承与菱形继承
多继承语法
一个派生类同时继承多个基类:
#include <iostream>
using namespace std;
class Teacher { // 基类1:教师
public:
int id = 1001; // 同名属性:id
void showInfo() { // 同名方法:showInfo
cout << "教师信息:ID=" << id << endl;
}
void teach() { cout << "授课" << endl; }
};
class Student { // 基类2:学生
public:
int id = 2001; // 同名属性:id
void showInfo() { // 同名方法:showInfo
cout << "学生信息:ID=" << id << endl;
}
void study() { cout << "学习" << endl; }
};
class TA : public Teacher, public Student { // 派生类:助教(多重继承)
public:
void assist() { cout << "辅助教学" << endl; }
};
int main() {
TA ta;
// 问题1:访问同名属性id → 二义性(不知道用Teacher::id还是Student::id)
// cout << ta.id << endl; // ❌ 编译错误:ambiguous(歧义)
// 问题2:调用同名方法showInfo → 二义性
// ta.showInfo(); // ❌ 编译错误:ambiguous(歧义)
ta.teach(); // ✔ 无歧义(仅Teacher有teach)
ta.study(); // ✔ 无歧义(仅Student有study)
ta.assist(); // ✔ 无歧义(TA自身方法)
return 0;
}
二义性的解决方法
-
通过基类名::成员名显式指定来源
int main() { TA ta; // 访问同名属性:显式指定基类 cout << "教师ID:" << ta.Teacher::id << endl; // ✔ 指定Teacher的id cout << "学生ID:" << ta.Student::id << endl; // ✔ 指定Student的id // 调用同名方法:显式指定基类 ta.Teacher::showInfo(); // ✔ 调用Teacher的showInfo ta.Student::showInfo(); // ✔ 调用Student的showInfo return 0; } -
在派生类中重写同名成员,消除歧义
class TA : public Teacher, public Student { public: int id = 3001; // TA自身的id(覆盖基类同名属性) void showInfo() { // 重写showInfo,内部明确调用基类版本 cout << "助教信息:ID=" << id << endl; Teacher::showInfo(); // 内部指定调用教师的showInfo Student::showInfo(); // 内部指定调用学生的showInfo } void assist() { cout << "辅助教学" << endl; } }; int main() { TA ta; cout << ta.id << endl; // ✔ 访问TA自身的id(无歧义) ta.showInfo(); // ✔ 调用TA重写的showInfo(无歧义) return 0; }
二义性的本质原因
多重继承中,若多个基类存在同名且同签名的成员(属性或方法),派生类会同时继承这些成员,但编译器无法判断你想访问哪一个基类的版本,因此必须通过显式指定或重写来消除歧义。这也是多重继承需要谨慎使用的原因之一。
菱形继承
当两个派生类继承同一基类,又被同一个类继承时,会出现数据冗余和二义性:
// 顶层基类:Animal
class Animal {
public:
int age = 0;
};
// 中间派生类1:Tiger
class Tiger : public Animal {};
// 中间派生类2:Lion
class Lion : public Animal {};
// 最终派生类:Liger(虎狮兽)
class Liger : public Tiger, public Lion {};
// 问题:Liger包含两份Animal::age,访问时二义性
int main() {
Liger lig;
// lig.age = 5; // ❌ 错误:ambiguous(不明确)
lig.Tiger::age = 5; // ✔ 明确指定Tiger的age
lig.Lion::age = 6; // ✔ 明确指定Lion的age
return 0;
}
虚继承(解决菱形继承)
虚基类(Virtual Base Class) 是通过 虚继承(virtual inheritance) 机制实现的特殊基类。当一个基类被声明为虚基类后,无论它在继承体系中被继承多少次,最终派生类中都 只保留一份该基类的成员副本。
菱形继承输出结果:
Animal构造
Tiger构造
Animal构造
Lion构造
Liger构造
问题分析:
- 内存中存在两份
Animal实例 - 访问
age需要明确指定路径 - 浪费内存空间(sizeof(Liger) = 8)
虚继承解决方案
class Tiger : virtual public Animal {/*...*/ };
class Lion : virtual public Animal {/*...*/ };
修改后的输出:
Animal构造
Tiger构造
Lion构造
Liger构造
关键变化:
- 只构造一次Animal对象
- sizeof(Liger) = 12(含虚基表指针)
虚继承实现机制
- 内存结构图示
Liger对象内存布局:
+-------------------+
| Tiger虚基表指针 | --> 虚基表条目1:Liger到Animal偏移量
+-------------------+
| Lion虚基表指针 | --> 虚基表条目2:Liger到Animal偏移量
+-------------------+
| Animal::age |
+-------------------+
| Liger特有成员 |
+-------------------+
虚基表(vbtable)原理
当一个类通过虚继承的方式继承自基类时,在编译基类时,编译器会为基类生成虚基表,存放在全局数据区。
- 每个虚继承类携带虚基表指针
- 虚基表存储两类信息:
- 当前对象到虚基类的偏移量
- 虚基类成员的访问路径
访问过程:
lig.age 实际访问步骤:
1. 通过Tiger虚基表指针找到Animal位置
2. 访问Animal::age成员
构造函数调用规则
- 构造顺序
- 虚基类构造函数(由最终派生类直接调用)
- 非虚基类构造函数(按声明顺序)
- 成员对象构造函数(按声明顺序)
- 自身构造函数
- 验证示例:
class A { public: A() { cout << "A构造" << endl; } };
class B : virtual public A { public: B() { cout << "B构造" << endl; } };
class C : virtual public A { public: C() { cout << "C构造" << endl; } };
class D : public B, public C { public: D() { cout << "D构造" << endl; } };
int main() {
D d;// 输出顺序:A构造 → B构造 → C构造 → D构造
return 0;
}
虚基类特性
- 虚基类构造函数只调用一次
- 使用虚基类表(vbtable)维护偏移量
虚基类指针(VBPtr):存储在每个包含虚基类的对象中,通常位于对象起始位置。
虚基类表(VBTable):存储在全局数据区,记录虚基类的偏移量。
类外堆内存与继承中的动态内存管理:避免泄漏的核心逻辑
基本概念:什么是类外堆内存?
类对象自身(成员变量、对象本体)存储在栈 / 堆上,但如果类的成员是指针 / 引用,指向通过malloc/new等分配的额外堆内存,这部分内存就是 “类外堆内存”。
- 核心特性:类对象销毁时(如栈对象生命周期结束、堆对象被
delete),系统仅释放对象自身内存,类外堆内存不会自动释放; - 直接后果:若不手动释放,会导致内存泄漏(系统内存被占用且无法回收),也是 C++ 内存泄漏的主要诱因(智能指针可解决此问题)。
默认陷阱:未处理类外堆内存导致的泄漏
/**
* 未正确释放类外堆内存的示例:内存泄漏
* @brief 类A的成员指针p指向堆内存,但析构函数未释放
* @note 栈对象a销毁时,仅调用析构函数打印“析构”,但p指向的1000字节堆内存未释放
*/
#include <iostream>
using namespace std;
class A {
char* p; // 指向类外堆内存的指针成员
public:
// 构造函数:分配1000字节类外堆内存
A() { p = new char[1000]; }
// 析构函数:仅打印信息,未释放p指向的堆内存
~A() { cout << "析构" << endl; }
};
int main(int argc, char const* argv[]) {
A a; // 栈上创建对象a,生命周期结束时自动调用析构
return 0;
}
Valgrind 检测结果解析:
gec@ubuntu:~$ valgrind ./memoryLeak
==154251== HEAP SUMMARY:
==154251== in use at exit: 1,000 bytes in 1 blocks // 程序退出时仍占用1000字节堆内存
==154251== total heap usage: 2 allocs, 1 frees // 2次内存分配、仅1次释放(对象自身内存释放,类外堆内存未释放)
==154251== LEAK SUMMARY:
==154251== definitely lost: 1,000 bytes in 1 blocks // 明确泄漏1000字节(p指向的类外堆内存)
- 关键结论:即使析构函数执行,未释放类外堆内存仍会导致泄漏;对象自身内存(栈上的 A 对象)被释放,但
p指向的堆内存 “失联”,系统无法回收。
解决方案:析构函数中正确释放类外堆内存
核心规则:类中分配了类外堆内存,必须重写析构函数,且释放方式与分配方式严格匹配。
/**
* 正确释放类外堆内存的示例:无内存泄漏
* @brief 析构函数中用delete[]释放new[]分配的堆内存
* @note 分配/释放匹配:new[] ↔ delete[]、new ↔ delete、malloc ↔ free;释放后置空指针避免悬垂
*/
#include <iostream>
using namespace std;
class A {
char* p;
public:
A() { p = new char[1000]; } // new[]分配字符数组(类外堆内存)
// 析构函数:释放p指向的类外堆内存
~A() {
cout << "析构" << endl;
delete[] p; // 必须用delete[],与new[]严格匹配
p = nullptr; // 可选但推荐:将指针置空,避免成为悬垂指针
}
};
int main(int argc, char const* argv[]) {
A a;
return 0;
}
Valgrind 检测结果解析:
gec@ubuntu:~$ valgrind ./memoryLeak2
==168757== HEAP SUMMARY:
==168757== in use at exit: 0 bytes in 0 blocks // 无内存残留
==168757== total heap usage: 3 allocs, 3 frees // 分配/释放次数完全匹配
==168757== All heap blocks were freed -- no leaks are possible // 无任何内存泄漏
核心注意点(分配 / 释放严格匹配,否则触发未定义行为):
| 分配方式 | 对应释放方式 | 错误示例及后果 |
|---|---|---|
new char[100](数组) |
delete[] p |
用delete p:仅释放第一个字符,剩余 99 字节泄漏 |
new int(10)(单个值) |
delete p |
用delete[] p:未定义行为,可能崩溃 |
malloc(100)(C 风格) |
free(p) |
用delete p:不调用析构,且类型不匹配 |
继承关系中的动态内存管理:父子类的内存协同
当基类使用类外堆内存时,派生类的动态内存管理需兼顾 “自身资源” 和 “基类资源”,核心是保证基类的动态内存被正确拷贝、赋值、释放。
先定义基础基类(含动态内存,实现完整的内存管理):
#include <cstring>
#include <iostream>
using namespace std;
/**
* 基类Base:含类外堆内存,实现完整的动态内存管理
* @brief 包含普通构造、拷贝构造、赋值运算符、虚析构(为多态析构做准备)
* @note 虚析构保证通过基类指针删除子类对象时,先调用子类析构,再调用基类析构
*/
class Base {
char* data; // 基类的类外堆内存(存储数据)
int size; // 数据长度
public:
// 普通构造函数:初始化基类的类外堆内存
Base(const char* d = "null", int s = 0) : size(s) {
data = new char[size];
memcpy(data, d, size); // 深拷贝数据到类外堆内存
}
// 拷贝构造函数:深拷贝基类的类外堆内存(避免浅拷贝导致重复释放)
Base(const Base& r) {
size = r.size;
data = new char[size];
memcpy(data, r.data, size);
}
// 赋值运算符函数:先释放自身资源,再深拷贝(防止自赋值+内存泄漏)
Base& operator=(const Base& r) {
if (this == &r) return *this; // 防护自赋值(避免释放自身内存后拷贝)
// 第一步:释放当前对象的类外堆内存
delete[] data;
// 第二步:深拷贝源对象的资源
size = r.size;
data = new char[size];
memcpy(data, r.data, size);
return *this;
}
// 虚析构函数:释放基类的类外堆内存
virtual ~Base() {
delete[] data;
data = nullptr;
cout << "Base析构:释放data内存" << endl;
}
};
场景 1:子类无动态内存(简单场景)
子类仅继承基类,自身无类外堆内存,此时子类无需显式定义拷贝构造、赋值运算符、析构函数 —— 编译器生成的默认版本会自动调用基类的对应函数,保证基类资源正确管理。
/**
* 子类Derived:无自身动态内存
* @brief 无需自定义拷贝构造、赋值运算符、析构函数
* @note 默认函数会自动调用基类的版本,基类已处理自身动态内存
*/
class Derived : public Base {
// 无额外动态内存成员
};
// 使用示例
int main() {
Derived d1("test", 4); // 调用Base的普通构造函数初始化基类资源
Derived d2 = d1; // 调用Base的拷贝构造函数深拷贝基类资源
Derived d3;
d3 = d2; // 调用Base的赋值运算符函数处理基类资源
return 0; // 销毁顺序:d1/d2/d3先调用Derived默认析构,再自动调用Base析构释放data
}
核心逻辑:
- 构造 / 拷贝构造 / 赋值:子类默认函数会自动调用基类的对应版本,基类已实现深拷贝和资源释放,无需子类干预;
- 析构:子类析构(即使是空的默认析构)执行完毕后,会自动调用基类析构,释放基类的类外堆内存。
场景 2:子类有动态内存(复杂场景)
子类自身也有类外堆内存,需显式定义拷贝构造、赋值运算符、析构函数,且主动调用基类的对应函数,保证基类资源被正确处理。
/**
* 子类Derived:含自身类外堆内存(info)
* @brief 自定义拷贝构造、赋值运算符、析构函数,显式调用基类版本处理基类资源
*/
class Derived : public Base {
char* info; // 子类的类外堆内存(存储额外信息)
int len; // 信息长度
public:
// 普通构造函数:先调用基类构造,再初始化子类资源
Derived(const char* d = "null", int s = 0, const char* inf = "null", int l = 0)
: Base(d, s), len(l) { // 初始化列表调用基类构造(必须优先)
info = new char[len];
memcpy(info, inf, len); // 深拷贝子类资源
}
// 拷贝构造函数:必须显式调用基类拷贝构造
Derived(const Derived& r)
: Base(r) { // 关键:将子类对象r传给基类拷贝构造(向上转型,Base&接收Derived对象)
// 初始化子类自身资源(深拷贝)
len = r.len;
info = new char[len];
memcpy(info, r.info, len);
}
// 赋值运算符函数:必须显式调用基类赋值运算符
Derived& operator=(const Derived& r) {
if (this == &r) return *this; // 防护自赋值
// 第一步:调用基类赋值运算符,处理基类资源(必须显式调用)
Base::operator=(r);
// 第二步:处理子类自身资源(先释放,再深拷贝)
delete[] info; // 释放当前子类的类外堆内存
len = r.len;
info = new char[len];
memcpy(info, r.info, len);
return *this;
}
// 析构函数:释放子类资源,基类资源由基类析构自动处理
~Derived() {
delete[] info;
info = nullptr;
cout << "Derived析构:释放info内存" << endl;
}
};
// 使用示例
int main() {
Derived d1("base", 4, "derived", 7); // 基类+子类资源初始化
Derived d2 = d1; // 调用Derived拷贝构造(含Base拷贝构造)
Derived d3;
d3 = d2; // 调用Derived赋值运算符(含Base赋值运算符)
return 0;
// 销毁顺序:
// 1. d3/d2/d1调用Derived析构(释放info)
// 2. 自动调用Base析构(释放data)
}
核心细节解析:
- 拷贝构造函数:
- 必须在初始化列表中调用
Base(r)(基类拷贝构造),否则编译器会调用基类的普通构造函数,导致基类资源初始化错误(如 data 为空); - 子类对象
r可传给基类拷贝构造的const Base&参数(C++ 天然支持向上转型)。
- 必须在初始化列表中调用
- 赋值运算符函数:
- 子类自定义赋值运算符后,默认的 “自动调用基类赋值” 会失效,需显式调用
Base::operator=(r); - 执行顺序:先处理基类资源,再处理子类资源(避免自赋值时先释放子类资源导致基类资源丢失)。
- 子类自定义赋值运算符后,默认的 “自动调用基类赋值” 会失效,需显式调用
- 析构函数:
- 子类析构只需释放自身的类外堆内存,基类析构会在子类析构执行后自动调用;
- 基类析构声明为
virtual的关键:若通过基类指针指向子类对象(如Base* ptr = new Derived();),delete ptr时会先调用子类析构(释放 info),再调用基类析构(释放 data),避免子类资源泄漏。
核心总结
- 类外堆内存:分配必释放,释放方式与分配严格匹配,析构函数是释放类外堆内存的核心入口;
- 继承中的动态内存:
- 子类无动态内存:无需自定义函数,默认版本自动调用基类的内存管理函数;
- 子类有动态内存:拷贝构造(初始化列表调基类拷贝)、赋值运算符(显式调基类赋值)、析构(仅释放自身资源);
- 基类析构加
virtual:是多态场景下避免子类资源泄漏的关键(即使子类无动态内存,也推荐基类析构为虚函数)。

浙公网安备 33010602011771号