前言

在前面的几篇文章中,我们依次学习了 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`(指针指向的内容)

代码段(常量区)

指向的正是那个字符串常量

五大内存区域总结

  1. :非静态局部变量、函数参数、返回值等。向下增长,由编译器自动管理。
  2. :动态分配的内存(malloc / new)。向上增长,需要程序员手动管理。
  3. 数据段(静态区):全局变量和静态变量。
  4. 代码段(常量区):可执行代码和只读常量(如字符串字面量)。

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 得到的内存上。所以我们需要:

  1. placement-new 调用构造函数 → 初始化对象
  2. 显式调用析构函数 → 清理资源
  3. 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.

在实际工程中,还可以使用:

  • LinuxValgrind(最常用的内存泄漏检测工具)
  • WindowsVLD(Visual Leak Detector,集成到 VS 中使用)
  • 跨平台ASan(AddressSanitizer,Google 出品,GCC/Clang 内置)
  • VS 自带:启用"启用地址检测(ASan)"编译选项

8.4 如何避免内存泄漏

  1. 事前预防:使用 RAII 思想(资源获取即初始化)和智能指针

   // 泄漏版本
   GameCharacter* p = new GameCharacter("临时角色", 50);
   // ... 如果中间 return 了,就泄漏了
   delete p;
 
   // RAII 版本 - 自动释放
   {
       unique_ptr<GameCharacter> p = make_unique<GameCharacter>("临时角色", 50);
       // ... 就算中间 return,p 的析构函数也会自动 delete
   }  // 离开作用域,p 自动释放

  1. 编码规范
  • 谁申请谁释放
  • new / delete 成对出现
  • 构造函数中申请的资源,在析构函数中释放
  • 发生异常时确保资源被释放(使用 try-catch 或 RAII)
  1. 工具辅助:将内存泄漏检测集成到 CI/CD 流程中
  2. 企业实践:许多大厂有自研的内存管理库,内置泄漏检测和内存统计功能

> 在后续课程中,我们会专门学习智能指针(unique_ptrshared_ptrweak_ptr)——它们完美地将"资源管理"封装在栈对象中,当对象离开作用域时自动释放堆内存,从根源上避免了内存泄漏。

---

九、从类与对象的视角重新审视

回到我们最初的知识体系。在 Lesson02 到 Lesson04 中,我们花了大量篇幅学习:

  • 构造函数:对象的"出生"——初始化成员变量
  • 析构函数:对象的"死亡"——清理资源
  • 拷贝构造 / 赋值重载:对象的复制语义

现在结合内存管理来看,这一切变得非常清晰:

                     对象的一生
 
           ┌──────────────────────────────────┐
           │  1. 分配原始内存                   │
           │     malloc / operator new / 栈分配  │
           └────────────────┬─────────────────┘
                            │
           ┌────────────────▼─────────────────┐
           │  2. 调用构造函数                   │
           │     new 自动完成                   │
           │     placement-new 手动完成          │
           └────────────────┬─────────────────┘
                            │
           ┌────────────────▼─────────────────┐
           │      ★ 对象在此刻诞生 ★            │
           │      可以正常使用该对象             │
           └────────────────┬─────────────────┘
                            │
                            │   (也许被拷贝、赋值、传参...)
                            │
           ┌────────────────▼─────────────────┐
           │  3. 调用析构函数                   │
           │     delete 自动完成                │
           │     显式调用 ~T() 手动完成          │
           └────────────────┬─────────────────┘
                            │
           ┌────────────────▼─────────────────┐
           │  4. 释放原始内存                   │
           │     free / operator delete         │
           │     栈自动回收                     │
           └────────────────┬─────────────────┘
                            │
           ┌────────────────▼─────────────────┐
           │      ★ 对象在此刻消亡 ★            │
           └──────────────────────────────────┘

对于有堆资源的类(比如本章的 GameCharacterBullet),析构函数必须手动释放内部申请的堆内存,否则就会造成内存泄漏。这正是为什么 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++ 模板的世界,敬请期待!

更多推荐