C/C++ 内存管理
前言
在前面的几篇文章中,我们依次学习了 C++ 的基础语法、类与对象的核心机制。从命名空间、引用、函数重载,到类的封装、构造与析构、拷贝控制、static 成员、友元与内部类——我们逐步搭建起了一个面向对象编程的世界观。
然而,当我们写下 new 和 delete 时,是否思考过:对象到底被分配在内存的哪个区域?构造和析构函数是在何时被调用的?`new` 和 `malloc` 除了语法不同,底层到底有什么区别?
这些问题指向的,正是 C/C++ 中一个极为核心的主题——内存管理。本文将完整梳理 C/C++ 的内存分布、动态内存管理方式、new/delete 的底层实现原理,以及如何有效防范内存泄漏,帮助你建立系统级的内存认知。
---
一、C/C++ 内存分布
在开始讨论动态内存管理之前,我们先要理解一个程序在运行时的内存布局。看下面这段代码:
int company_stock = 10000; // 模拟公司总库存
static int total_orders = 0; // 累计订单总数
void SimulateOrder()
{
static int daily_orders = 0; // 当日订单数
int current_qty = 1; // 当前订单数量
char warehouse_tag[] = "WH01"; // 仓库编号
const char* kProductName = "Laptop-X1"; // 产品名称(常量字符串)
int* invoice = (int*)malloc(sizeof(int) * 100); // 发票队列
int* buffer = (int*)calloc(50, sizeof(int)); // 缓冲区
int* expanded = (int*)realloc(buffer, sizeof(int) * 80); // 扩容
free(invoice);
free(expanded);
}
想一想:这些变量分别存放在哪里?
|
变量 |
所在区域 |
说明 |
|
`company_stock` / `total_orders` / `daily_orders` |
数据段(静态区) |
全局/静态变量,程序启动时分配,结束时释放 |
|
`current_qty` / `warehouse_tag` |
栈 |
非静态局部变量、函数参数、返回值 |
|
`invoice` / `buffer` / `expanded`(指针本身) |
栈 |
指针变量本身也是局部变量 |
|
`*invoice` / `*buffer` / `*expanded`(指向的内存) |
堆 |
`malloc` 动态申请的内存 |
|
`"Laptop-X1"`(字符串常量) |
代码段(常量区) |
只读数据,不可修改 |
|
`*kProductName`(指针指向的内容) |
代码段(常量区) |
指向的正是那个字符串常量 |
五大内存区域总结
- 栈:非静态局部变量、函数参数、返回值等。向下增长,由编译器自动管理。
- 堆:动态分配的内存(malloc / new)。向上增长,需要程序员手动管理。
- 数据段(静态区):全局变量和静态变量。
- 代码段(常量区):可执行代码和只读常量(如字符串字面量)。
5. 内存映射段:高效 I/O 映射,用于加载共享动态库、进程间通信。
> 类和对象中我们学过,类的对象(非静态成员变量)存储在栈或堆上(取决于对象创建方式),而成员函数存储在代码段,多个对象共享同一份函数代码。这正是 C++ 对象模型的内存效率所在。
---
二、C 语言的动态内存管理方式
C 语言提供了四个标准库函数用于动态内存管理:
// 学生成绩管理系统:动态存储成绩数据
// 分配一个学生的成绩记录
int* score = (int*)malloc(sizeof(int));
*score = 95;
printf("score = %d\n", *score);
// 分配一个班级的成绩数组并初始化为0
int* class_scores = (int*)calloc(40, sizeof(int));
// 班级扩容(插入新学生时)
int* expanded = (int*)realloc(class_scores, sizeof(int) * 45);
// 释放内存
free(score);
free(expanded);
malloc / calloc / realloc 的区别
|
函数 |
功能 |
初始化 |
|
`malloc` |
分配指定字节的内存 |
不初始化,内容随机 |
|
`calloc` |
分配指定数量 × 元素大小的内存 |
初始化为 0 |
|
`realloc` |
调整之前分配的内存大小 |
原内容保留,新空间未初始化 |
malloc 的局限
在类和对象(二)中我们讲到,构造函数会在对象创建时自动初始化成员变量。但 malloc 只是分配了一块原始内存,它不会调用构造函数。这对于简单类型(如 int)问题不大,但对于自定义类型比如一个 Student 类——malloc 出来的"对象"根本没有调用构造函数,内部的姓名指针、学号字段都是随机值,直接使用必然崩溃。
这正是 C++ 引入 new 的根本原因。
---
三、C++ 的内存管理方式
3.1 对内置类型
double* temperature = new double; // 申请一个 double
double* precise_temp = new double(36.5); // 申请并初始化为 36.5
double* week_temps = new double[7]; // 申请 7 个 double 的连续空间
delete temperature;
delete precise_temp;
delete[] week_temps; // 注意匹配:new[] 对应 delete[]
对于内置类型,new / delete 与 malloc / free 的行为几乎一致。但有一个重要区别:`new` 失败时抛出 `bad_alloc` 异常,而 `malloc` 返回 `NULL`。
3.2 对自定义类型——这才是重点
我们用一个新的场景来演示:游戏中的角色管理系统。
#include <iostream>
#include <cstring>
using namespace std;
class GameCharacter
{
public:
// 构造函数:分配角色名并初始化生命值
GameCharacter(const char* name, int hp = 100)
: _hp(hp)
{
_name = new char[strlen(name) + 1];
strcpy(_name, name);
cout << "[构造] 角色 \"" << _name << "\" 诞生了!HP = " << _hp << " (对象地址: " << this << ")" << endl;
}
// 析构函数:释放角色名占用的堆内存
~GameCharacter()
{
cout << "[析构] 角色 \"" << _name << "\" 阵亡了... (对象地址: " << this << ")" << endl;
delete[] _name;
_name = nullptr;
_hp = 0;
}
void TakeDamage(int dmg)
{
_hp -= dmg;
cout << _name << " 受到 " << dmg << " 点伤害,剩余 HP = " << _hp << endl;
}
private:
char* _name; // 动态分配的字符串
int _hp; // 生命值
};
现在,我们用 malloc 和 new 分别创建角色,看看两者的区别:
int main()
{
cout << "===== 测试1: malloc + free 创建角色 =====\n";
GameCharacter* hero1 = (GameCharacter*)malloc(sizeof(GameCharacter));
// ❌ malloc 只分配空间,不调用构造函数
// hero1 内部的 _name 和 _hp 都是随机值!
// hero1->TakeDamage(10); // 调用会崩溃
free(hero1); // ❌ 只释放空间,不调用析构函数
// hero1 内部的 _name 指向的堆内存没有被释放 → 内存泄漏!
cout << "\n===== 测试2: new + delete 创建角色 =====\n";
GameCharacter* hero2 = new GameCharacter("战神阿瑞斯", 200);
// ✅ new = operator new(分配空间) + 构造函数(初始化)
hero2->TakeDamage(30); // 正常运行
hero2->TakeDamage(50);
delete hero2;
// ✅ delete = 析构函数(清理_name的堆内存) + operator delete(释放空间)
cout << "\n===== 测试3: 创建角色军团 =====\n";
GameCharacter* army = new GameCharacter[3]{
{"弓箭手-雅典娜", 80},
{"剑士-赫拉克勒斯", 150},
{"法师-宙斯", 60}
};
army[0].TakeDamage(20);
army[1].TakeDamage(70);
delete[] army; // ✅ 依次析构 3 个对象,然后释放整块空间
return 0;
}
运行结果:
===== 测试1: malloc + free 创建角色 =====
===== 测试2: new + delete 创建角色 =====
[构造] 角色 "战神阿瑞斯" 诞生了!HP = 200 (对象地址: 0x...)
战神阿瑞斯 受到 30 点伤害,剩余 HP = 170
战神阿瑞斯 受到 50 点伤害,剩余 HP = 120
[析构] 角色 "战神阿瑞斯" 阵亡了... (对象地址: 0x...)
===== 测试3: 创建角色军团 =====
[构造] 角色 "弓箭手-雅典娜" 诞生了!HP = 80 (对象地址: 0x...)
[构造] 角色 "剑士-赫拉克勒斯" 诞生了!HP = 150 (对象地址: 0x...)
[构造] 角色 "法师-宙斯" 诞生了!HP = 60 (对象地址: 0x...)
弓箭手-雅典娜 受到 20 点伤害,剩余 HP = 60
剑士-赫拉克勒斯 受到 70 点伤害,剩余 HP = 80
[析构] 角色 "法师-宙斯" 阵亡了... (对象地址: 0x...)
[析构] 角色 "剑士-赫拉克勒斯" 阵亡了... (对象地址: 0x...)
[析构] 角色 "弓箭手-雅典娜" 阵亡了... (对象地址: 0x...)
`new` / `delete` 与 `malloc` / `free` 的最大区别就在于此:`new` 会在分配内存后自动调用构造函数,`delete` 会在释放内存前自动调用析构函数。
回顾 Lesson03 中我们讲过的"浅拷贝 vs 深拷贝"问题:这里的 GameCharacter 类的 _name 成员是动态分配的 char*,如果使用编译器默认生成的拷贝构造函数,那就是简单的浅拷贝(指针拷贝),两个对象会指向同一块内存,析构时造成 double-free。这正是 Lesson03 强调的——一旦涉及资源管理,必须手动实现拷贝构造和赋值重载。
---
四、operator new 与 operator delete——底层揭秘
很多人在初学时会产生一个误解:new 就是 malloc + 构造函数。这个理解从语义上是正确的,但从实现层面,这中间还隔着一层——`operator new` 和 `operator delete` 全局函数。
new 和 delete 是操作符(operator),而 operator new 和 operator delete 是底层的全局函数。
4.1 operator new 的源码实现
/*
operator new:底层通过 malloc 申请空间。
成功则直接返回;失败则尝试调用用户设置的空间不足处理函数,
如果用户未设置或处理函数也无法释放足够空间,则抛出 bad_alloc 异常。
*/
void* operator new(size_t size)
{
void* p;
// 循环尝试申请,直到成功或用户干预
while ((p = malloc(size)) == 0)
{
// 调用用户设置的新处理函数(如果存在)
if (_callnewh(size) == 0)
{
// 没有可用的处理函数,抛出异常
std::bad_alloc exception;
throw exception;
}
}
return p;
}
关键点:
- operator new 底层调用的还是 malloc
- malloc 失败时,不会返回 NULL,而是会循环尝试用户设置的"新处理函数"(new handler)
- 如果用户没有设置新处理函数,或者新处理函数也无法释放足够内存,则会抛出 `bad_alloc` 异常
- 这正是 new 与 malloc 失败行为不同的底层原因
4.2 operator delete 的源码实现
void operator delete(void* pUserData)
{
if (pUserData == NULL)
return;
// 底层调用 free 释放内存
_free_dbg(pUserData, _NORMAL_BLOCK);
}
operator delete 底层就是调用 free。
4.3 整体调用链路
执行 GameCharacter* hero = new GameCharacter("战神", 200) 时:
用户代码层
┌─────────────────────────────────────────┐
│ hero = new GameCharacter("战神", 200) │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ 步骤 1: operator new(sizeof(GameChar)) │ → malloc → 分配堆空间
└──────────────┬──────────────────────────┘
│ 返回原始内存指针
┌──────────────▼──────────────────────────┐
│ 步骤 2: 在原始内存上调用构造函数 │ → GameCharacter("战神", 200)
│ new(p) GameCharacter(...) │ → 初始化成员变量
└──────────────┬──────────────────────────┘
│ 返回已初始化的对象指针
┌──────────────▼──────────────────────────┐
│ 步骤 3: hero 指向完整的 GameCharacter 对象 │
└─────────────────────────────────────────┘
执行 delete hero 时:
用户代码层
┌─────────────────────────────────────────┐
│ delete hero; │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ 步骤 1: 调用 hero 的析构函数 │ → ~GameCharacter()
│ (清理 _name 占用的堆内存) │ → delete[] _name
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ 步骤 2: operator delete(hero) │ → free → 释放对象空间
└─────────────────────────────────────────┘
> 这个设计很巧妙:operator new 只负责分配原始内存,构造函数负责把原始内存变成对象。职责分离,中间还可以拦截(比如用 placement-new 在指定内存上构造)。这也是 C++ 设计哲学中"不为你不需要的特性付出代价"的体现。
---
五、new[] 与 delete[] 的实现原理
5.1 实现流程
GameCharacter* army = new GameCharacter[3]{ ... } 的执行流程:
┌─────────────────────────────────────┐
│ operator new[](3 * sizeof(GameChar)) │ → 实际调用 operator new
└──────────────┬──────────────────────┘
│
│ (可能多分配 4 或 8 字节记录元素个数)
▼
┌─────────────────────────────────────┐
│ 依次调用 3 次构造函数: │
│ army[0].GameCharacter("弓箭手-雅典娜") │
│ army[1].GameCharacter("剑士-赫拉克勒斯") │
│ army[2].GameCharacter("法师-宙斯") │
└─────────────────────────────────────┘
delete[] army 的执行流程:
┌─────────────────────────────────────┐
│ 从存储区读取数组长度 = 3 │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 逆序调用 3 次析构函数: │
│ army[2].~GameCharacter() ← 先销毁法师 │
│ army[1].~GameCharacter() ← 再销毁剑士 │
│ army[0].~GameCharacter() ← 最后销毁弓箭手│
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ operator delete[](army) │ → 实际调用 operator delete
└─────────────────────────────────────┘
5.2 一个关键细节
在使用 new[] 分配数组时,编译器实际上可能会多申请 4 个字节(或 8 字节),用来记录对象的个数。这样在调用 delete[] 时,编译器才知道需要调用多少次析构函数。
这就是为什么 new[] 必须与 delete[] 配套使用:
GameCharacter* squad = new GameCharacter[2]{{"士兵甲", 100}, {"士兵乙", 100}};
delete squad; // ❌ 未定义行为!只析构第一个对象,但释放了整块内存
// 编译器不知道数组有几个元素,内存管理头信息也被破坏
delete[] squad; // ✅ 正确:读取元素个数 → 依次析构 → 释放整块内存
> 实用技巧:在实际编码中,建议尽量使用 std::vector 代替手动管理的动态数组,它自动处理了构造、析构、扩容等所有细节。
---
六、定位 new 表达式(Placement-new)
定位 new(placement-new)允许我们在已经分配好的原始内存上调用构造函数来初始化对象。它通常与内存池配合使用。
6.1 使用方式
new (place_address) type;
new (place_address) type(initializer-list);
6.2 典型场景:游戏引擎的对象池
假设我们在开发一个射击游戏,子弹对象需要频繁创建和销毁。每次都调用 new/delete 会导致大量系统调用和内存碎片。我们可以预先分配一个对象池:
class Bullet
{
public:
Bullet() : _x(0), _y(0), _speed(0), _active(false)
{
cout << "[构造] 子弹就绪 " << this << endl;
}
~Bullet()
{
cout << "[析构] 子弹销毁 " << this << endl;
}
void Fire(float x, float y, float speed)
{
_x = x;
_y = y;
_speed = speed;
_active = true;
cout << "子弹发射!位置(" << _x << ", " << _y << ") 速度=" << _speed << endl;
}
void Reset()
{
_active = false;
_x = _y = _speed = 0;
}
private:
float _x, _y, _speed;
bool _active;
};
使用 placement-new 管理对象池:
int main()
{
const int POOL_SIZE = 10;
cout << "===== 步骤1: 预分配内存池 =====\n";
// 一次性分配足够容纳 10 颗子弹的原始内存
void* bullet_pool = malloc(sizeof(Bullet) * POOL_SIZE);
cout << "\n===== 步骤2: 按需创建子弹(placement-new)=====\n";
// 在索引 0 的位置构造一个子弹对象
Bullet* b1 = new(bullet_pool) Bullet();
b1->Fire(100, 200, 500.0f);
// 在索引 1 的位置构造另一个子弹对象
Bullet* b2 = new((Bullet*)bullet_pool + 1) Bullet();
b2->Fire(300, 150, 450.0f);
cout << "\n===== 步骤3: 子弹使用完毕,手动析构 =====\n";
b1->~Bullet(); // 只析构,不释放内存
b2->~Bullet();
cout << "\n===== 步骤4: 在相同位置重新创建子弹 =====\n";
// 内存池的空间可以被重用!
Bullet* b3 = new(bullet_pool) Bullet(); // 复用 b1 的位置
b3->Fire(50, 50, 600.0f);
b3->~Bullet();
cout << "\n===== 步骤5: 释放整个内存池 =====\n";
free(bullet_pool); // 一次性释放所有原始内存
return 0;
}
运行结果:
===== 步骤1: 预分配内存池 =====
===== 步骤2: 按需创建子弹(placement-new)=====
[构造] 子弹就绪 0x...
子弹发射!位置(100, 200) 速度=500.0
[构造] 子弹就绪 0x...
子弹发射!位置(300, 150) 速度=450.0
===== 步骤3: 子弹使用完毕,手动析构 =====
[析构] 子弹销毁 0x...
[析构] 子弹销毁 0x...
===== 步骤4: 在相同位置重新创建子弹 =====
[构造] 子弹就绪 0x...
子弹发射!位置(50, 50) 速度=600.0
[析构] 子弹销毁 0x...
===== 步骤5: 释放整个内存池 =====
6.3 为什么要手动调用析构函数?
因为这块内存不是通过 new 分配的,delete 不能直接用在 malloc 得到的内存上。所以我们需要:
- placement-new 调用构造函数 → 初始化对象
- 显式调用析构函数 → 清理资源
- free / operator delete → 释放内存
6.4 应用场景
placement-new 在实际工程中常用于:
- 游戏引擎的对象池(如上面的 Bullet 例子)
- 网络服务器的连接池(复用 TCP 连接对象)
- 内存池/分配器(STL 的 allocator 就是基于 placement-new 实现的)
- 嵌入式系统(内存有限,需要精确控制分配时机)
回顾 Lesson04 中讲的"初始化列表"——初始化列表保证了成员变量在被构造时就能获得正确的初始值。placement-new + 初始化列表,正是 C++ 提供精细控制对象生命周期的两把利器。
---
七、malloc/free vs new/delete:终极对比
学了这么多理论,我们用一张表格来做终极总结:
|
比较维度 |
malloc / free |
new / delete |
|
**本质** |
函数(库函数) |
操作符(C++ 关键字) |
|
**空间初始化** |
不初始化,内容随机 |
`new double(36.5)` 可以初始化 |
|
**计算大小** |
手动 `sizeof(T) * n` |
编译器自动推导 |
|
**返回值** |
`void*`,需要强转 |
对应类型指针,无需强转 |
|
**失败行为** |
返回 `NULL`,需判空 |
抛出 `bad_alloc` 异常 |
|
**自定义类型** |
只分配/释放空间,不调构造/析构 |
分配空间 + 调构造函数;释放空间 + 调析构函数 |
|
**数组支持** |
通过参数计算总大小 |
`new T[n]` / `delete[]` |
|
**可重载** |
不可重载 |
可在类级别重载 `operator new` |
---
八、内存泄漏——潜伏在代码中的定时炸弹
8.1 什么是内存泄漏?
简单说就是:申请了堆内存,用完之后忘记释放,导致这部分内存再也无法被使用。
void LeakScenario()
{
// 场景1:简单的忘记释放
char* log_buffer = (char*)malloc(1024 * 1024); // 申请 1MB 日志缓冲区
// ... 使用缓冲区 ...
// 忘记 free(log_buffer) → 泄漏 1MB
// 场景2:覆盖指针导致泄漏
int* data = new int[1000];
data = new int[500]; // 第一个 new 出来的 1000 个 int 再也找不到了!
// 场景3:异常导致释放代码跳过
double* sensor_data = new double[10000];
ProcessSensorData(sensor_data); // 如果这里抛异常...
delete[] sensor_data; // 这一行不会被执行 → 泄漏
}
8.2 内存泄漏的危害
|
程序类型 |
泄漏影响 |
|
**后台服务器**(如 Web 服务器、数据库) |
运行几天/几周后内存耗尽,服务崩溃 |
|
**游戏引擎** |
长时间游戏后越来越卡,最终掉线 |
|
**嵌入式设备** |
内存本就有限,泄漏几 KB 就可能导致死机 |
|
**桌面软件**(如 Photoshop) |
越用越卡,最终需要重启 |
8.3 如何检测内存泄漏
在 VS 下可以使用 _CrtDumpMemoryLeaks():
int main()
{
int* data = new int[500]; // 忘记 delete[]
// 在程序退出前检测内存泄漏
_CrtDumpMemoryLeaks();
return 0;
}
// 输出窗口会显示:
// Detected memory leaks!
// Dumping objects ->
// {145} normal block at 0x00BE5A30, 2000 bytes long.
// Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
// Object dump complete.
在实际工程中,还可以使用:
- Linux:Valgrind(最常用的内存泄漏检测工具)
- Windows:VLD(Visual Leak Detector,集成到 VS 中使用)
- 跨平台:ASan(AddressSanitizer,Google 出品,GCC/Clang 内置)
- VS 自带:启用"启用地址检测(ASan)"编译选项
8.4 如何避免内存泄漏
- 事前预防:使用 RAII 思想(资源获取即初始化)和智能指针
// 泄漏版本
GameCharacter* p = new GameCharacter("临时角色", 50);
// ... 如果中间 return 了,就泄漏了
delete p;
// RAII 版本 - 自动释放
{
unique_ptr<GameCharacter> p = make_unique<GameCharacter>("临时角色", 50);
// ... 就算中间 return,p 的析构函数也会自动 delete
} // 离开作用域,p 自动释放
- 编码规范:
- 谁申请谁释放
- new / delete 成对出现
- 构造函数中申请的资源,在析构函数中释放
- 发生异常时确保资源被释放(使用 try-catch 或 RAII)
- 工具辅助:将内存泄漏检测集成到 CI/CD 流程中
- 企业实践:许多大厂有自研的内存管理库,内置泄漏检测和内存统计功能
> 在后续课程中,我们会专门学习智能指针(unique_ptr、shared_ptr、weak_ptr)——它们完美地将"资源管理"封装在栈对象中,当对象离开作用域时自动释放堆内存,从根源上避免了内存泄漏。
---
九、从类与对象的视角重新审视
回到我们最初的知识体系。在 Lesson02 到 Lesson04 中,我们花了大量篇幅学习:
- 构造函数:对象的"出生"——初始化成员变量
- 析构函数:对象的"死亡"——清理资源
- 拷贝构造 / 赋值重载:对象的复制语义
现在结合内存管理来看,这一切变得非常清晰:
对象的一生
┌──────────────────────────────────┐
│ 1. 分配原始内存 │
│ malloc / operator new / 栈分配 │
└────────────────┬─────────────────┘
│
┌────────────────▼─────────────────┐
│ 2. 调用构造函数 │
│ new 自动完成 │
│ placement-new 手动完成 │
└────────────────┬─────────────────┘
│
┌────────────────▼─────────────────┐
│ ★ 对象在此刻诞生 ★ │
│ 可以正常使用该对象 │
└────────────────┬─────────────────┘
│
│ (也许被拷贝、赋值、传参...)
│
┌────────────────▼─────────────────┐
│ 3. 调用析构函数 │
│ delete 自动完成 │
│ 显式调用 ~T() 手动完成 │
└────────────────┬─────────────────┘
│
┌────────────────▼─────────────────┐
│ 4. 释放原始内存 │
│ free / operator delete │
│ 栈自动回收 │
└────────────────┬─────────────────┘
│
┌────────────────▼─────────────────┐
│ ★ 对象在此刻消亡 ★ │
└──────────────────────────────────┘
对于有堆资源的类(比如本章的 GameCharacter、Bullet),析构函数必须手动释放内部申请的堆内存,否则就会造成内存泄漏。这正是为什么 Lesson03 中强调:
> "如果类没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数;如果申请了资源,一定要写,否则会造成资源泄漏。"
再看 Lesson04 中讲的 explicit 关键字——它防止了构造函数的隐式类型转换。结合内存管理来理解:如果允许隐式转换,new 在构造对象时可能会产生意想不到的临时对象,导致额外的内存分配和拷贝开销。C++ 提供 explicit,就是让我们对"对象如何被创建"有更精确的控制。
---
十、练习题
学完本篇,不妨动手验证一下这几个问题:
问题 1
以下代码的输出是什么?为什么?
class Test {
public:
Test() { cout << "构造 "; }
~Test() { cout << "析构 "; }
};
int main() {
Test* p = new Test[3];
delete p; // 注意:不是 delete[]
return 0;
}
问题 2
下面的 safe_delete 宏能防止重复 delete 导致的崩溃吗?
#define safe_delete(p) if(p) { delete p; p = nullptr; }
int main() {
int* p = new int(42);
safe_delete(p);
safe_delete(p); // 第二次调用安全吗?
return 0;
}
问题 3
请解释以下代码为何会崩溃,以 GameCharacter 类为例。
GameCharacter getBoss() {
GameCharacter boss("最终BOSS", 9999);
return boss; // 这里发生了什么?
}
int main() {
GameCharacter b = getBoss();
b.TakeDamage(100);
return 0;
}
(提示:回顾 Lesson03 中的"浅拷贝"和"深拷贝")
问题 4
对象池(placement-new 的典型应用)相比每次 new + delete 有哪些优势?在什么场景下对象池反而会成为性能瓶颈?
总结
C/C++ 的内存管理是一门"必修课"。从 C 语言的 malloc/free 到 C++ 的 new/delete,我们看到的不仅是语法的演进,更是编程思想从"面向过程"到"面向对象"的升华。
- new 把内存分配和对象构造统一起来,让代码更安全、更符合直觉
- 理解 operator new 和 placement-new,才能在大型项目中进行定制化的内存管理
- 深刻理解内存布局和对象的生命周期,是写出高效、健壮 C++ 程序的根基
正如我们在 Lesson00 的前言中所说:C++ 不仅仅是一门语言,更是一种生活哲学。 内存管理就是这种哲学最直接的体现——它要求程序员对每一个字节负责,对每一个对象的生与死心中有数。
下一篇,我们将进入 C++ 模板的世界,敬请期待!
更多推荐

所有评论(0)