CPP 内存管理
目录
一、内存模型
二、接口函数
1、malloc 与 free
2、new 与 delete
三、智能指针
1、unique_ptr
(1)基本使用
(2)不能被复制,只能被移动
(3)自定义资源释放函数
2、shared_ptr
(1)基本使用
(2)循环引用
3、weak_ptr
4、内存占用
四、注意事项
1、拥抱 RALL
2、智能指针选择指南
3、谨慎使用 shared_ptr
4、自定义内存检测工具
一、内存模型
- 栈是⼀种有限的内存区域,⽤于存储函数的局部变量、函数参数和函数调⽤信息的区域。函数的调⽤和返回通过栈来管理。栈上的变量⽣命周期与其所在函数的执⾏周期相同。栈上的内存分配和释放是⾃动的,速度较快。
- 堆⽤于存储动态分配的内存的区域,由程序员⼿动分配和释放。使⽤
new
和delete
或malloc
和free
来进⾏堆内存的分配和释放。 - 全局区存储全局变量和静态变量。⽣命周期是整个程序运⾏期间。在程序启动时分配,程序结束时释放。
- 常量区也被称为只读区。存储常量数据,如字符串常量。
- 代码区用于存储程序的代码。
二、接口函数
1、malloc 与 free
- 只分配/释放原始内存,不调用构造函数/析构函数
- 返回
void*
需手动类型转换 - 分配失败时返回
NULL
- 不感知对象生命周期
#include <cstdlib>
// 分配内存
void* malloc(size_t size); // 分配未初始化的原始内存
// 释放内存
void free(void* ptr);// 示例:基础类型
int* p = (int*)malloc(sizeof(int));
*p = 10;
free(p);// 示例:数组
int* arr = (int*)malloc(5 * sizeof(int));
free(arr);
2、new 与 delete
关键特征:
- 自动计算内存大小(无需
sizeof
) - 自动调用构造函数/析构函数
- 类型安全(无需类型转换)
- 分配失败抛出
std::bad_alloc
异常 - 支持重载(类级别或全局)
/*单个对象
*/
// 分配 + 构造
auto* ptr = new Type(args...);
// 析构 + 释放
delete ptr;/*对象数组
*/
auto* arr = new Type[N]; // 调用 N 次默认构造函数
delete[] arr; // 调用 N 次析构函数
三、智能指针
1、unique_ptr
(1)基本使用
std::unique_ptr
对其持有的堆内存具有唯一拥有权,引用计数永远是 1。std::unique_ptr
对象销毁时会释放其持有的堆内存。
可以使用以下方式初始化一个std::unique_ptr
对象:
#include <memory>
//初始化方式 1
std::unique_ptr<int> sp1(new int(123));//初始化方式 2
std::unique_ptr<int> sp2;
sp2.reset(new int(123));//初始化方式 3,强烈推荐
std::unique_ptr<int> sp3 = std::make_unique<int>(123);
std::unique_ptr<int[]> uptr_arr = std::make_unique<int[]>(10); // 10 个 int 的数组
uptr_arr[0] = 42; // 像普通数组一样使用
应该使用更安全的方式(std::make_unique
)创建unique_ptr
,《Effective Modern C++》中有如下解释。
让很多人对 C++11 规范吐槽的地方之一是,C++11 新增了 std::make_shared()
方法创建一个 std::shared_ptr
对象,却没有提供相应的 std::make_unique()
方法创建一个 std::unique_ptr
对象,这个方法直到 C++14 才被添加进来。当然,在 C++11 中你很容易实现出一个这样的方法:
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&& ...params)
{return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
std::unique_ptr
离开其作用域时会自动释放持有的内存。当然你也可以调用 reset()
立即销毁持有的内存,也可以分配新对象。
// 1. 自动释放:当 uptr 离开其作用域时
{auto uptr = std::make_unique<MyClass>();
} // 此处 ~MyClass() 被自动调用// 2. 手动释放并置空:.release() 放弃所有权,返回原始指针(你需要负责管理这个指针的生命周期)
MyClass* raw_ptr = uptr.release();
delete raw_ptr; // 现在必须手动删除// 3. 手动释放并销毁:.reset() 立即销毁其管理的对象,并将自身置空
uptr.reset(); // 删除对象,uptr = nullptr
uptr.reset(new MyClass()); // 删除旧对象,接管新对象
(2)不能被复制,只能被移动
鉴于 std::auto_ptr
的前车之鉴,std::unique_ptr
禁止复制语义,为了达到这个效果,std::unique_ptr
类的拷贝构造函数和赋值运算符(operator =
)被标记为 =delete
。
template <class T>
class unique_ptr
{//省略其他代码...//拷贝构造函数和赋值运算符被标记为deleteunique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;
};
由于无法复制,所有权的传递必须通过 std::move
。
std::unique_ptr<MyClass> uptr_src = std::make_unique<MyClass>();
// std::unique_ptr<MyClass> uptr_dst = uptr_src; // 错误!编译失败,无法复制
std::unique_ptr<MyClass> uptr_dst = std::move(uptr_src); // 正确:所有权转移// 此时 uptr_src 变为 nullptr,不再拥有任何对象
if (uptr_src == nullptr) {std::cout << "uptr_src is now empty" << std::endl;
}
// uptr_dst 现在拥有对象
uptr_dst->doSomething();
(3)自定义资源释放函数
默认情况下,智能指针对象在析构时只会释放其持有的堆内存(自动调用 delete
或者 delete[]
),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),我们可以通过自定义智能指针的资源释放函数。
假设现在有一个 socket 类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,我们可以如下自定义智能指针对象的资源析构函数,这里以 std::unique_ptr
为例:
#include <iostream>
#include <memory>class Socket
{
public:Socket(){}~Socket(){}//关闭资源句柄void close(){}
};int main()
{auto deletor = [](Socket* pSocket) {//关闭句柄pSocket->close();//TODO: 你甚至可以在这里打印一行日志...delete pSocket;};std::unique_ptr<Socket, void(*)(Socket * pSocket)> spSocket(new Socket(), deletor);return 0;
}
自定义 std::unique_ptr
的资源释放函数其规则是:
std::unique_ptr<T, DeletorFuncPtr>
其中T是你要释放的对象类型,DeletorFuncPtr
是一个自定义函数指针。上述代码 33 行表示 DeletorFuncPtr
有点复杂,我们可以使用 decltype(deletor)
让编译器自己推导deletor的类型,因此可以将 33 行代码修改为:
std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor);
2、shared_ptr
(1)基本使用
多个 shared_ptr
可以指向同一个对象,并通过引用计数机制来管理内存。每当一个新的 shared_ptr
指向该对象时,引用计数加 1;每当一个 shared_ptr
被销毁或重置时,引用计数减 1。当引用计数变为 0 时,对象被自动删除。
多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作 std::shared_ptr
引用的对象是安全的)。std::shared_ptr
提供了一个 use_count()
方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr
用法和 std::unique_ptr
基本相同。
下面是一个使用 std::shared_ptr
的示例:
std::shared_ptr<MyClass> sptr = std::make_shared<MyClass>();
std::cout << "Use count: " << sptr.use_count() << std::endl; // 输出当前引用计数 (1)
if (sptr.unique()) { // 等价于 use_count() == 1std::cout << "This is the only owner" << std::endl;
}// 使用 std::make_shared 去初始化一个 std::shared_ptr 对象
std::shared_ptr<MyClass> sptr2(sptr);sptr.reset(); // 当前 shared_ptr 放弃所有权,引用计数-1。如果变为 0 则删除对象。sptr 本身变为 nullptr
sptr.reset(new MyClass()); // 放弃旧对象的所有权,接管新对象// 获取原始指针(谨慎使用!不要手动删除它!)
MyClass* raw_ptr = sptr.get();
(2)循环引用
造成资源无法释放,此时应将其中一个指针(通常是反向指针 prev
)改为 weak_ptr
struct Node {std::shared_ptr<Node> next;std::shared_ptr<Node> prev;// ~Node() { cout << "Node destroyed" << endl; }
};auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; // node2 引用计数 = 2
node2->prev = node1; // node1 引用计数 = 2// 当离开作用域时:
// node2 引用计数从2 -> 1 (因为 node1->next 还指着它)
// node1 引用计数从2 -> 1 (因为 node2->prev 还指着它)
// 引用计数无法归零,内存泄漏!
3、weak_ptr
std::weak_ptr
是一个不控制资源生命周期的智能指针,它指向一个由 shared_ptr
管理的对象,但不增加其引用计数。它的存在不会阻止所指向对象的销毁。它主要用于解决 shared_ptr
的循环引用问题。它的构造和析构不会引起引用计数的增加或减少。
std::weak_ptr
必须从一个 shared_ptr
构造或赋值。
auto shared = std::make_shared<MyClass>();
std::weak_ptr<MyClass> weak = shared; // 引用计数仍为 1
因为 weak_ptr
不拥有对象,所以不能直接使用 ->
或 *
操作符。必须先将它转换为一个 shared_ptr
。
// 方法一:使用 .lock()
std::shared_ptr<MyClass> temp = weak.lock();
if (temp) { // 检查转换是否成功(对象是否还存在)temp->doSomething(); // 安全地使用对象// 此时引用计数至少为 2 (temp 和原来的 shared)
} else {std::cout << "Object has been destroyed" << std::endl;
}// 方法二:使用构造函数(如果对象已失效则抛出 std::bad_weak_ptr 异常)
try {std::shared_ptr<MyClass> temp(weak);temp->doSomething();
} catch (const std::bad_weak_ptr&) {std::cout << "Bad weak pointer!" << std::endl;
}
还支持查看对象是否已经被销毁。
// 检查观察的对象是否已被销毁
if (weak.expired()) { // 等价于 use_count() == 0std::cout << "The object is gone" << std::endl;
}// 查看当前引用计数(注意:可能已经过时)
std::cout << "Use count (maybe stale): " << weak.use_count() << std::endl;
4、内存占用
一个std::unique_ptr
对象大小与裸指针大小相同(即sizeof(std::unique_ptr<T>) == sizeof(void*)
)。
而std::shared_ptr
的大小是std::unique_ptr
的一倍。
#include <iostream>
#include <memory>
#include <string>int main()
{std::shared_ptr<int> sp0;std::shared_ptr<std::string> sp1;sp1.reset(new std::string());std::unique_ptr<int> sp2;std::weak_ptr<int> sp3;std::cout << "sp0 size: " << sizeof(sp0) << std::endl;std::cout << "sp1 size: " << sizeof(sp1) << std::endl;std::cout << "sp2 size: " << sizeof(sp2) << std::endl;std::cout << "sp3 size: " << sizeof(sp3) << std::endl;return 0;
}
在32位机器上,std::unique_ptr
占 4 字节,std::shared_ptr
和std::weak_ptr
占 8 字节;
在64位机器上,std_unique_ptr
占 8 字节,std::shared_ptr
和std::weak_ptr
占 16 字节。也就是说,std_unique_ptr
的大小总是和原始指针大小一样,std::shared_ptr
和std::weak_ptr
大小是原始指针的一倍。
四、注意事项
1、拥抱 RALL
RAII (Resource Acquisition Is Initialization) 是 C++ 管理的核心思想:将资源(如内存)的生命周期与对象的生命周期绑定。对象构造时获取资源,析构时自动释放资源。智能指针是 RAII 理念用于内存管理的完美实现。在现代 C++ 中,你应该几乎永远不需要直接使用 new
和 delete
。
强烈建议使用 std::make_unique
和 std::make_shared
来创建智能指针,而不是直接用 new
。它们更安全(防止异常导致泄漏)、更高效(尤其是 make_shared
)。
彻底拥抱现代 C++ 的内存管理哲学。将 new
/delete
视为遗留特性,仅在极其特殊的情况下(例如,需要与某些只接受原始指针的 C 库交互)才使用它们,并且要将其封装在 RAII 类中立即管理。你的代码安全性和可维护性将得到质的提升。
2、智能指针选择指南
特性 |
|
|
|
所有权 | 独占 | 共享 | 无(弱引用) |
复制 | 不允许(只能移动) | 允许 | 允许 |
性能开销 | 几乎为零(与原始指针无异) | 较高(引用计数,原子操作) | 较高(同 |
主要用途 | 默认选择,单一明确所有者 | 共享所有权,多个所有者 | 打破循环引用,观察 |
如何创建 |
|
| 从 |
- 默认首选
unique_ptr
:除非明确需要共享所有权,否则都应使用它。它最轻量、最安全。 - 谨慎使用
shared_ptr
:仅在确实需要多个所有者共享同一对象生命周期时使用。要警惕循环引用。 - 使用
weak_ptr
作为shared_ptr
的补充:用于解决循环引用或作为观察者。 - 坚持使用
make_unique
和make_shared
,它们提供了更强的异常安全保证,并且make_shared
通常效率更高(单次分配同时存储对象和控制块)。
3、谨慎使用 shared_ptr
shared_ptr
的引用计数问题不好排查,非必要不使用shared_ptr
,项目尽量模块化,模块之间不涉及复杂的内存管理,内存管理在框架做好。
上层业务代码一般不会出现任何内存管理,业务代码尽量不写裸指针代码,一切都底层包装好。尽量避免多线程内存管理,推荐消息传递,或者任务划分,比如A线程创建buffer, B线程处理,C线程释放,不推荐复杂对象shared_ptr
嵌套。
4、自定义内存检测工具
一个复杂的 c/c++ 程序线上运行过程中内存泄露没有释放,如何检测?可能的检测办法是自己实现一套内存检测库。这个库的具体原理和常见内存检测差不多,都是勾住new malloc free delete
等操作,拦截后记录。
不同的是,这个库是动态的,也就是说,程序在运行过程中,你可以实时查看当前c/c++ new malloc的信息,申请内存代码位置+大小,当你感觉你的程序内存泄露有没有释放,你可以及时查看程序内存申请信息,也许就会发现,某些内存应该释放但还没有释放,以此定位问题。
这个库如何应用?它可以在程序里开辟一个线程创建一个httpserver
,打开网页就可以实时查看程序的内存对象信息。