<memory> 是 C++ 标准库中最为核心的头文件之一,它提供了内存管理 和智能指针的基础设施。从 C++11 开始,它彻底改变了 C++ 开发者管理资源的方式,使得代码更加安全、异常友好且易于维护。
📋整体架构一览
<memory> 库可以按照功能分为以下四大模块:
| 模块 | 核心组件 | 功能描述 |
|---|---|---|
| 智能指针 | unique_ptr, shared_ptr, weak_ptr |
自动管理动态对象的生命周期,实现 RAII |
| 辅助工具 | make_unique, make_shared, allocate_shared |
安全、高效地创建智能指针 |
| 分配器 | allocator, pmr::memory_resource |
定义内存分配策略 |
| 低级内存操作 | addressof, align, uninitialized_* |
指针操作、内存对齐、原始内存构造 |
一、智能指针(Smart Pointers)
智能指针是 <memory> 的核心卖点,它们封装了原始指针,并自动管理所指向对象的生命周期。
1. std::unique_ptr
独占所有权的智能指针,不能被复制,只能被移动。
cpp
#include <memory>
#include <iostream>
class Widget {
public:
Widget() { std::cout << "Widget created\n"; }
~Widget() { std::cout << "Widget destroyed\n"; }
void doSomething() { std::cout << "Doing something\n"; }
};
int main() {
// 创建 unique_ptr(推荐使用 make_unique)
auto p = std::make_unique<Widget>();
// 调用成员函数
p->doSomething();
// 不能复制,只能移动
// auto p2 = p; // ❌ 编译错误
auto p2 = std::move(p); // ✅ 所有权转移
// 当 p2 超出作用域时,Widget 被自动销毁
return 0;
}
关键特性:
- 零开销:与原始指针大小相同,没有额外的运行时开销
- 自定义删除器:可以指定自定义的释放逻辑
- 数组支持:专门针对数组的特化版本
cpp
// 自定义删除器
auto deleter = [](int* p) {
std::cout << "Custom deleting\n";
delete p;
};
std::unique_ptr<int, decltype(deleter)> p(new int(42), deleter);
// 数组版本
auto arr = std::make_unique<int[]>(10); // C++14 起支持
arr[0] = 42;
2. std::shared_ptr
共享所有权的智能指针,通过引用计数管理生命周期。
cpp
#include <memory>
#include <iostream>
int main() {
// 创建 shared_ptr
std::shared_ptr<int> p1 = std::make_shared<int>(42);
{
std::shared_ptr<int> p2 = p1; // 拷贝,引用计数 +1
std::cout << "use_count: " << p1.use_count() << std::endl; // 2
} // p2 销毁,引用计数 -1
std::cout << "use_count: " << p1.use_count() << std::endl; // 1
return 0;
} // p1 销毁,引用计数变为 0,内存被释放
关键特性:
- 线程安全:引用计数的增加/减少是线程安全的
- 控制块:额外分配控制块存储引用计数和删除器
- 不适合数组:默认不支持数组(C++17 起支持)
cpp
// C++17 数组支持
std::shared_ptr<int[]> arr = std::make_shared<int[]>(10);
arr[0] = 42;
// 别名构造函数:共享所有权,但指向不同对象
struct Base { int x; };
struct Derived : Base { int y; };
std::shared_ptr<Derived> d = std::make_shared<Derived>();
std::shared_ptr<Base> b(d, &d->x); // 共享所有权,但 b 指向 d.x
3. std::weak_ptr
解决 shared_ptr 循环引用的弱引用智能指针。
cpp
#include <memory>
#include <iostream>
class Node {
public:
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 使用 weak_ptr 避免循环引用
~Node() { std::cout << "Node destroyed\n"; }
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2;
n2->prev = n1; // weak_ptr 不增加引用计数
// 安全访问 weak_ptr
if (auto sp = n2->prev.lock()) {
std::cout << "Successfully locked\n";
}
return 0;
} // 所有 Node 都能被正确销毁
关键用法:
lock():返回指向对象的shared_ptr,对象已释放则返回空expired():检查对象是否已被释放
4. 智能指针转换
| 转换函数 | 用途 | 说明 |
|---|---|---|
static_pointer_cast |
静态转换 | 类似 static_cast |
dynamic_pointer_cast |
动态转换 | 类似 dynamic_cast,失败返回空 |
const_pointer_cast |
常量转换 | 类似 const_cast |
reinterpret_pointer_cast |
重解释转换 | 类似 reinterpret_cast(C++17起) |
cpp
std::shared_ptr<Base> base = std::make_shared<Derived>();
auto derived = std::dynamic_pointer_cast<Derived>(base);
if (derived) {
// 转换成功
}
二、创建工具:make_unique 与 make_shared
1. std::make_unique (C++14)
cpp
// 基本用法
auto p = std::make_unique<Widget>(arg1, arg2);
// 数组版本
auto arr = std::make_unique<int[]>(10); // C++14
// 自定义删除器的 unique_ptr 不支持 make_unique
std::unique_ptr<Widget, Deleter> p(new Widget(), Deleter());
2. std::make_shared (C++11)
cpp
// 一次分配:对象和控制块在同一内存块中
auto p = std::make_shared<Widget>(arg1, arg2);
// 性能优势:比先 new 再 shared_ptr 构造少一次内存分配
// 异常安全:即使有多个参数,构造过程也不会泄露内存
对比 shared_ptr<T>(new T(args)):
| 方面 | make_shared |
shared_ptr<T>(new T) |
|---|---|---|
| 内存分配次数 | 1 次 | 2 次(对象 + 控制块) |
| 异常安全 | 完全安全 | 可能存在风险 |
| 内存局部性 | 更好(对象和控制块相邻) | 较差 |
| 自定义删除器 | 不支持 | 支持 |
| weak_ptr 延长生命周期 | 可能延长 | 不会 |
3. std::allocate_shared
使用自定义分配器创建 shared_ptr:
cpp
#include <memory>
struct MyAllocator : std::allocator<char> {
// 自定义分配逻辑
};
auto p = std::allocate_shared<Widget>(MyAllocator(), arg1, arg2);
三、分配器(Allocators)
1. 经典分配器 std::allocator
cpp
#include <memory>
#include <vector>
int main() {
std::vector<int, std::allocator<int>> vec; // 默认分配器
// allocator 定义了容器的内存分配策略
}
核心接口:
allocate(n):分配n个对象的原始内存deallocate(p, n):释放内存construct(p, args):在已分配内存上构造对象(C++17 移除)destroy(p):析构对象(C++17 移除)
2. 多态内存资源(C++17)
<memory_resource> 子库提供了更现代的内存管理框架:
cpp
#include <memory_resource>
#include <vector>
#include <iostream>
int main() {
// 1. monotonic_buffer_resource:单调增长,极快,不回收
char buffer[1024];
std::pmr::monotonic_buffer_resource pool(buffer, sizeof(buffer));
std::pmr::vector<int> vec(&pool);
for (int i = 0; i < 100; ++i) {
vec.push_back(i); // 全部从 pool 中分配
}
// 2. unsynchronized_pool_resource:对象池,适合固定大小对象
std::pmr::unsynchronized_pool_resource pool2;
std::pmr::vector<int> vec2(&pool2);
return 0;
}
可用的 memory_resource:
| 类型 | 特点 | 适用场景 |
|---|---|---|
new_delete_resource |
使用 new/delete |
默认行为 |
null_memory_resource |
分配即失败 | 测试、沙盒 |
monotonic_buffer_resource |
只分配不回收 | 一次性解析、短期任务 |
synchronized_pool_resource |
线程安全的对象池 | 多线程环境 |
unsynchronized_pool_resource |
非线程安全对象池 | 单线程环境 |
四、低级内存操作工具
1. std::addressof
获取对象的真实地址,即使 operator& 被重载。
cpp
struct Evil {
int* operator&() { return nullptr; }
} evil;
int* p1 = &evil; // nullptr(使用了重载的 &)
int* p2 = std::addressof(evil); // 对象的真实地址
2. std::align
在给定的缓冲区中对齐指针。
cpp
#include <memory>
#include <iostream>
int main() {
char buffer[100];
void* ptr = buffer;
size_t space = sizeof(buffer);
// 在 buffer 中找到第一个 16 字节对齐的位置
if (std::align(16, 10, ptr, space)) {
std::cout << "Aligned at: " << ptr << std::endl;
std::cout << "Remaining space: " << space << std::endl;
}
}
3. 未初始化内存算法
这些函数在已分配但未初始化的内存上操作,是标准库容器实现的基础:
| 函数 | C++11 | C++17 | 功能 |
|---|---|---|---|
uninitialized_copy |
✅ | ✅ (constexpr) | 复制构造到未初始化内存 |
uninitialized_fill |
✅ | ✅ (constexpr) | 填充构造到未初始化内存 |
uninitialized_default_construct |
❌ | ✅ | 默认构造到未初始化内存 |
uninitialized_value_construct |
❌ | ✅ | 值构造到未初始化内存 |
destroy |
❌ | ✅ | 销毁对象 |
cpp
#include <memory>
#include <iostream>
int main() {
// 分配内存但不构造对象
std::allocator<int> alloc;
int* p = alloc.allocate(5);
// 在已分配内存上构造对象
std::uninitialized_fill(p, p + 5, 42);
// 使用对象
for (int i = 0; i < 5; ++i) {
std::cout << p[i] << ' ';
}
// 销毁对象
std::destroy(p, p + 5);
// 释放内存
alloc.deallocate(p, 5);
}
4. 原始指针包装
cpp
// 从不释放的指针包装
std::observer_ptr<int> obs(p); // C++26 提案,类似 std::unique_ptr 但不拥有所有权
// 空指针检查
std::not_null<int*> nn(p); // GSL(Guidelines Support Library)中的工具
五、性能对比与选型建议
智能指针开销对比
| 智能指针 | 大小(64位) | 分配次数 | 适用场景 |
|---|---|---|---|
unique_ptr |
8 字节 | 0(对象本身) | 独占所有权,90% 的场景 |
shared_ptr |
16 字节(控制块指针2) | 1 或 2 次 | 共享所有权 |
weak_ptr |
16 字节 | 与 shared_ptr 相同 |
打破循环引用 |
选择指南
| 场景 | 推荐方案 |
|---|---|
| 默认选择,单个所有者 | unique_ptr |
| 需要多个所有者共享对象 | shared_ptr |
| 观察但不拥有对象 | weak_ptr |
| 避免循环引用 | weak_ptr |
| 性能敏感、嵌入式环境 | unique_ptr |
| 需要自定义释放逻辑 | 自定义删除器 |
| 缓存或对象池 | weak_ptr + shared_ptr |
| 解析 JSON、XML 等一次性任务 | monotonic_buffer_resource |
| 避免双重内存分配 | make_shared |
六、常见陷阱与最佳实践
陷阱 1:使用裸 new 创建 shared_ptr
cpp
// ❌ 可能的内存泄漏或双重删除
foo(std::shared_ptr<Widget>(new Widget), bar());
// ✅ 使用 make_shared
foo(std::make_shared<Widget>(), bar());
陷阱 2:循环引用
cpp
// ❌ 循环引用导致内存泄漏
struct A {
std::shared_ptr<A> other;
~A() { std::cout << "A destroyed\n"; }
};
auto a1 = std::make_shared<A>();
auto a2 = std::make_shared<A>();
a1->other = a2;
a2->other = a1; // 循环引用,内存泄漏
// ✅ 使用 weak_ptr 打破循环
struct B {
std::weak_ptr<B> other;
~B() { std::cout << "B destroyed\n"; }
};
陷阱 3:从 this 创建 shared_ptr
cpp
class Widget {
public:
// ❌ 错误:会导致多个控制块
std::shared_ptr<Widget> get() {
return std::shared_ptr<Widget>(this);
}
// ✅ 正确:继承 enable_shared_from_this
std::shared_ptr<Widget> share() {
return shared_from_this();
}
};
class GoodWidget : public std::enable_shared_from_this<GoodWidget> {};
陷阱 4:make_shared 的 weak_ptr 问题
cpp
auto sp = std::make_shared<LargeData>();
std::weak_ptr<LargeData> wp = sp;
sp.reset(); // shared_ptr 释放,但控制块仍然存在
// LargeData 仍然存活!需要等到所有 weak_ptr 也销毁
七、C++11/14/17/20 演进历程
| 版本 | 新增特性 |
|---|---|
| C++11 | unique_ptr, shared_ptr, weak_ptr, make_shared, addressof, 未初始化算法 |
| C++14 | make_unique, shared_ptr 数组支持 |
| C++17 | pmr 多态内存资源, uninitialized_* constexpr, reinterpret_pointer_cast, weak_from_this |
| C++20 | make_shared_for_overwrite, allocate_shared_for_overwrite |
总结
<memory> 库是现代 C++ 内存管理的基石,它的核心理念是 RAII(资源获取即初始化) 。通过智能指针,C++ 开发者可以编写异常安全、内存安全的代码,无需手动管理 new 和 delete。同时,分配器和 pmr 为高性能场景提供了灵活的内存管理策略。
开发准则:
- 优先使用
unique_ptr,除非确实需要共享所有权 - 使用
make_shared/make_unique而不是裸new - 遇到循环引用时,考虑使用
weak_ptr - 不要从裸
this指针创建shared_ptr - 需要自定义内存策略时,考虑
pmr::memory_resource