内存管理

C++ 中对象可以存放在不同区域,不同区域的对象创建方式、销毁时机、管理责任都不同。

对象的生命周期

全局对象

定义在所有函数之外的对象。

特点:

  • 程序启动时分配
  • 程序结束时销毁

局部自动对象

定义在函数或代码块中的普通局部变量。

特点:

  • 进入作用域时创建
  • 离开作用域时自动销毁

例如:

1
2
3
void f() {
int x = 10; // 进入 f 时创建,离开 f 时销毁
}

局部 static 对象

定义在函数内部,但带 static

特点:

  • 第一次执行到定义处时初始化
  • 一直到程序结束时才销毁

例如:

1
2
3
4
void f() {
static int cnt = 0;
++cnt;
}

动态分配对象

通过 new 创建的对象。

特点:

  • 存在于堆(自由空间)
  • 生命周期不由作用域决定
  • 必须显式释放,或交给智能指针管理

例如:

1
int *p = new int(42);

程序中的内存区域

复习时主要记三类:

静态内存

保存:

  • 全局变量
  • static 局部变量
  • 类的 static 数据成员

特点:

  • 程序开始前后存在
  • 由编译器管理其创建和销毁

栈内存

保存:

  • 函数中的普通局部变量
  • 临时对象等自动对象

特点:

  • 进入作用域自动分配
  • 离开作用域自动释放
  • 管理简单,但生命周期短

堆 / 自由空间

保存:

  • 动态分配的对象

特点:

  • 程序员控制分配和释放
  • 更灵活
  • 也更容易出错

为什么需要动态内存

动态内存适合这些场景:

  • 对象大小运行时才知道
  • 对象要跨作用域存在
  • 需要大量对象,不适合放栈上
  • 需要动态增长的数据结构

例如:

  • 动态数组
  • 链表、树、图
  • 运行时决定大小的缓冲区

动态内存的基本操作

C++ 用两组核心操作管理动态对象:

  • new:分配并构造对象
  • delete:销毁并释放对象

new

基本形式

1
int *p = new int;

作用:

  1. 在堆上分配空间
  2. 构造一个对象
  3. 返回指向该对象的指针

默认初始化

1
2
int *pi = new int;        // 未初始化
string *ps = new string; // 默认构造,空字符串

注意:

  • 内置类型,默认初始化后值未定义
  • 类类型,调用默认构造函数

直接初始化

1
2
int *pi = new int(1024);
string *ps = new string("hello");

即:创建对象时直接给初值。

列表初始化

1
vector<int> *pv = new vector<int>{1,2,3,4,5};

适合类类型和容器类型。

值初始化

在类型后加空括号:

1
2
int *p1 = new int;    // 未初始化
int *p2 = new int(); // 值初始化,为 0

复习时重点记:

  • new int:未定义值
  • new int():值为 0

分配失败

如果堆空间不足,普通 new 会抛出异常:

1
std::bad_alloc

例如:

1
int *p = new int;   // 失败时抛出 bad_alloc

也可以用不抛异常的版本:

1
int *p = new (nothrow) int;

此时分配失败时:

  • 返回 nullptr

需要头文件:

1
#include <new>

delete

基本形式

1
delete p;

作用:

  1. 销毁 p 指向的对象
  2. 释放对应的堆内存

delete 的要求

delete 的参数必须是:

  • 指向动态分配对象的指针
  • 或空指针 nullptr

否则行为未定义。

错误示例

删除不是 new 得到的指针

1
2
3
int x = 10;
int *p = &x;
delete p; // 错误

重复释放

1
2
3
int *p = new int(42);
delete p;
delete p; // 错误,重复释放

空悬指针

1
2
int *p = new int(42);
delete p;

删除后:

  • 对象已经没了
  • p 里可能还保留原地址

这时 p 就是空悬指针(dangling pointer)

再使用它是严重错误。

避免空悬指针

常见做法:

1
2
delete p;
p = nullptr;

这样能明确表示“现在不指向任何对象”。

动态内存常见问题

内存泄漏

申请了内存但没有释放:

1
2
int *p = new int(42);
// 忘了 delete p;

结果:

  • 这块内存直到程序结束前都回收不了

野指针 / 空悬指针

释放后还继续使用:

1
2
3
int *p = new int(42);
delete p;
cout << *p; // 错误

重复释放

同一地址 delete 多次,行为未定义。

为什么要用智能指针

手写 new/delete 很容易出错:

  • 忘记 delete
  • 提前 delete
  • 异常导致 delete 没执行
  • 多处共享同一对象难管理

所以现代 C++ 推荐:

尽量不用裸 new/delete,优先使用智能指针

智能指针定义在:

1
#include <memory>

智能指针概述

标准库主要有三种智能指针:

  • shared_ptr:共享所有权
  • unique_ptr:独占所有权
  • weak_ptr:弱引用,不拥有对象

共同操作

shared_ptrunique_ptr 都支持:

操作 含义
p 作为条件判断,若非空则为真
*p 解引用
p->mem 成员访问
p.get() 返回内部原始指针
swap(p, q) 交换所管理的指针

注意:

  • get() 只是“拿到原始指针看看”
  • 不要用 get() 得到的指针去 delete
  • 也不要用它去初始化另一个智能指针

shared_ptr

shared_ptr 表示:

多个智能指针可以共同拥有同一个对象

底层有一个引用计数

  • 拷贝一个 shared_ptr:计数 +1
  • 销毁一个 shared_ptr:计数 -1
  • 计数变为 0:自动释放对象

创建 shared_ptr

推荐方式:

1
auto p = make_shared<int>(42);

优点:

  • 更安全
  • 更高效
  • 少写一次 new

常见操作

操作 含义
make_shared<T>(args) 创建并返回一个 shared_ptr
shared_ptr<T> p(q) 拷贝 q,共享对象
p = q 共享 q 所指对象
p.use_count() 当前共享者数量
p.unique() 是否只有自己一个拥有者

例子

1
2
3
auto p1 = make_shared<int>(42);
auto p2 = p1; // 共享同一个 int
cout << p1.use_count(); // 2

use_count()unique()

1
2
p.use_count();  // 返回共享对象的 shared_ptr 数量
p.unique(); // 等价于 use_count() == 1

说明:

  • 常用于调试或理解所有权
  • 实际编程中不要过度依赖它们

不要混用裸指针和 shared_ptr

错误示例:

1
2
3
int *p = new int(42);
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p); // 错误:两个 shared_ptr 各自认为自己拥有 p

这样会导致:

  • 重复释放

正确做法:

1
2
auto sp1 = make_shared<int>(42);
auto sp2 = sp1;

unique_ptr

unique_ptr 表示:

同一时刻只能有一个智能指针拥有该对象

特点:

  • 不能拷贝
  • 可以转移所有权
  • 开销小
  • 最适合“独占资源”

创建 unique_ptr

1
unique_ptr<int> p(new int(42));

注意:

  • 经典教材中这样写
  • 现代 C++14 起也常用 make_unique,但有些教材章节可能未引入

如果可用,更推荐:

1
auto p = make_unique<int>(42);

不能拷贝

1
2
unique_ptr<int> p1(new int(42));
unique_ptr<int> p2 = p1; // 错误

因为所有权不能共享。

可以移动

1
2
unique_ptr<int> p1(new int(42));
unique_ptr<int> p2 = std::move(p1);

此后:

  • p2 拥有对象
  • p1 为空

常见操作

操作 含义
u = nullptr 释放对象并置空
u.release() 放弃所有权,返回原始指针,不释放对象
u.reset() 释放当前对象
u.reset(q) 改为管理 q 指向对象
u.reset(nullptr) 置空

release()reset() 区别

release()

1
int *p = u.release();

效果:

  • u 不再管理对象
  • 返回原始指针
  • 不会自动释放对象

所以必须有人接管它,否则泄漏。


reset()

1
u.reset();

效果:

  • 直接释放当前对象

weak_ptr

weak_ptr 是配合 shared_ptr 使用的弱引用。

特点:

  • 指向 shared_ptr 管理的对象
  • 不增加引用计数
  • 不控制对象生存期

为什么需要 weak_ptr

主要有两个用途:

  1. 观察某对象是否还存在
  2. 打破 shared_ptr 循环引用

常见操作

操作 含义
weak_ptr<T> w 空弱引用
weak_ptr<T> w(sp) 观察 sp 所管理对象
w.reset() 置空
w.use_count() 对应对象的 shared_ptr 数量
w.expired() 对象是否已释放
w.lock() 若对象还在,返回一个 shared_ptr;否则返回空 shared_ptr

基本例子

1
2
auto sp = make_shared<int>(42);
weak_ptr<int> wp(sp);

此时:

  • wp 指向同一对象
  • 但不会让引用计数增加

访问 weak_ptr 所指对象

不能直接解引用 weak_ptr,应先 lock()

1
2
3
if (auto p = wp.lock()) {
cout << *p << endl;
}

这样才安全。

智能指针使用原则

优先使用 make_shared

1
auto p = make_shared<string>("hello");

能用 unique_ptr 就尽量用 unique_ptr

因为:

  • 所有权更明确
  • 成本更低
  • 不易误共享

不要手动 delete 智能指针管理的对象

错误:

1
2
auto p = make_shared<int>(42);
delete p.get(); // 严重错误

不要用同一个裸指针初始化多个智能指针

会导致重复释放。

动态数组

虽然 C++ 支持动态数组,但复习时要记住:

大多数场景优先使用 vectorstring,而不是手写动态数组

因为容器更安全、更易用。

new 分配数组

1
int *p = new int[10];

表示:

  • 分配 10 个 int
  • 返回的是指向首元素的指针

注意:

  • 得到的不是“真正的数组对象”
  • 只是一个指向第一个元素的指针

因此:

  • 不能直接知道长度
  • 不能对它用 begin / end
  • 不能直接范围 for

数组初始化

默认初始化

1
int *p = new int[10];   // 10 个未初始化 int

值初始化

1
int *p = new int[10](); // 10 个 0

列表初始化

1
int *p = new int[10]{0,1,2,3,4,5,6,7,8,9};

如果初始化器不足,剩余元素值初始化。

释放动态数组

释放单个对象:

1
delete p;

释放数组:

1
delete[] p;

一定要区分!

错误示例:

1
2
int *p = new int[10];
delete p; // 错误,应使用 delete[]

智能指针与动态数组

unique_ptr 管理数组

1
unique_ptr<int[]> up(new int[10]);

特点:

  • 会自动用 delete[]
  • 可用下标访问:
1
up[0] = 10;

注意:

  • 数组版 unique_ptr 不支持 ->
  • 因为它管的是数组,不是单个对象

shared_ptr 管理数组

shared_ptr 不直接内建数组版本,如果要管理数组,必须自定义删除器:

1
shared_ptr<int> sp(new int[10], [](int *p){ delete[] p; });

否则默认会用 delete,出错。

访问元素时通常要借助 get()

1
sp.get()[0] = 10;

allocator

allocator 定义在:

1
#include <memory>

作用:

将“分配内存”和“构造对象”分离

new 不同,allocator 先分配一块原始未构造内存,然后再在上面构造对象。

这主要用于:

  • 标准库底层实现
  • 高级内存管理
  • 自定义容器

考试/复习中重点是理解思想

为什么要分离

new 一步完成:

  1. 分配内存
  2. 构造对象

allocator 可以拆成两步:

  1. allocate:只拿到原始内存
  2. construct:需要时再构造对象

这样更灵活。

常见操作

操作 含义
allocator<T> a 可为 T 分配内存的分配器
a.allocate(n) 分配能放 nT 的未构造内存
a.deallocate(p, n) 释放由 allocate 得到的内存
a.construct(p, args) p 指向位置构造对象
a.destroy(p) 销毁 p 指向对象

使用规则

必须牢记:

  • allocate 只分配内存,不构造对象
  • construct 后才能使用对象
  • destroy 后对象才真正销毁
  • 最后再 deallocate 释放内存

典型流程

1
2
3
4
5
6
7
8
allocator<string> alloc;
auto p = alloc.allocate(3); // 分配 3 个 string 的原始内存
alloc.construct(p, "hello"); // 构造第一个
alloc.construct(p + 1, "world"); // 构造第二个

alloc.destroy(p + 1);
alloc.destroy(p);
alloc.deallocate(p, 3);

未初始化内存算法

定义在:

1
#include <memory>

这些算法用于:

  • 向未构造内存中构造对象

常见算法

算法 含义
uninitialized_copy(b, e, b2) [b,e) 拷贝到未构造内存 b2 开始处
uninitialized_copy_n(b, n, b2) 拷贝 n 个元素到未构造内存
uninitialized_fill(b, e, t) 在未构造内存中填充对象
uninitialized_fill_n(b, n, t) 在未构造内存中构造 n 个值为 t 的对象

注意:

  • 目标内存必须足够大
  • 目标必须是“未构造原始内存”

现代 C++ 的内存管理建议

复习和实际开发都推荐下面这套思路:

优先使用容器

  • 动态数组优先用 vector
  • 字符串优先用 string

因为容器已经封装好内存管理。

需要动态对象时优先用智能指针

  • 独占:unique_ptr
  • 共享:shared_ptr

尽量少直接写 new/delete

只有在:

  • 实现底层结构
  • 与旧代码/库交互
  • 特殊资源管理

时才考虑手动管理。

易错点总结

new intnew int() 不一样

  • new int:未初始化
  • new int():值初始化为 0

deletedelete[] 不一样

  • 单对象:delete p
  • 数组:delete[] p

delete 后指针不会自动变成 nullptr

要手动置空:

1
2
delete p;
p = nullptr;

不要释放不是 new 得到的地址

不要重复释放同一块内存

shared_ptr 不要用同一裸指针构造多个对象

不要对智能指针 get() 出来的指针手动 delete

weak_ptr 不能直接解引用,要先 lock()

动态数组最好用 vector 替代

allocator 分配的是未构造内存,不能直接用

必须先构造对象。

小结

内存管理的主线:

  1. 对象可能位于:
  • 静态区
  1. 堆对象通过 new 创建、delete 释放
  2. 手动管理内存容易出错:
  • 泄漏
  • 空悬指针
  • 重复释放
  1. 智能指针能自动管理对象生命周期:
  • shared_ptr:共享
  • unique_ptr:独占
  • weak_ptr:弱引用
  1. 动态数组通常不如 vector 安全方便
  2. allocator 用于“分配内存”和“构造对象”分离

现代 C++:优先用容器,次选智能指针,最后才是手写 new/delete