指针进阶:动态内存分配与智能指针
指针这东西,C++ 里绕不开。基础用法大家都会,但真正让指针变得危险又强大的,是动态内存分配。我见过太多项目崩溃在内存泄漏上,也见过老手在 shared_ptr 循环引用里翻车。今天咱们就把这块彻底讲透。
一、new 和 delete:手动管理内存
动态内存分配,说白了就是程序运行时跟操作系统要内存。C 里用 malloc/free,C++ 里用 new/delete。区别在哪?new 不光分配内存,还会调用构造函数。
int* p = new int(42); // 分配一个 int,初始化为 42
delete p; // 释放内存
int* arr = new int[10]; // 分配数组
delete[] arr; // 注意是 delete[]
嗯,这里要注意:new 和 delete 必须配对。new[] 必须配 delete[]。混用会导致未定义行为——程序可能崩,也可能不崩,但数据已经坏了。
二、为什么需要智能指针?
手动管理内存,最怕两件事:忘记 delete 导致内存泄漏,或者多次 delete 导致 double free。你想想看,一个函数里如果有多个 return 分支,每个分支前都要记得 delete,多容易漏?
智能指针就是来解决这个问题的。它把内存管理的责任从程序员手里拿走了,交给 RAII(资源获取即初始化)机制。说白了,智能指针对象销毁时,自动释放它管理的资源。
三、unique_ptr:独占所有权
unique_ptr 是我个人用得最多的智能指针。它的语义很明确:一个资源只能被一个 unique_ptr 拥有。不能拷贝,只能移动。
#include <memory>
std::unique_ptr<int> p1(new int(10));
// std::unique_ptr<int> p2 = p1; // 编译错误!不能拷贝
std::unique_ptr<int> p2 = std::move(p1); // 可以移动
// 推荐用 make_unique(C++14 起)
auto p3 = std::make_unique<int>(20);
为什么推荐 make_unique?两个原因:一是代码更简洁,二是异常安全。如果 new 表达式和 unique_ptr 构造之间抛出异常,可能会泄漏内存。make_unique 一步到位,不会有这个问题。
四、shared_ptr:共享所有权
有些场景下,多个对象需要共享同一个资源。比如多个视图共享同一个数据模型。这时候就需要 shared_ptr。
shared_ptr 内部维护一个引用计数。每次拷贝,计数加一;每次析构,计数减一。计数归零时,自动释放资源。
auto sp1 = std::make_shared<int>(100);
{
auto sp2 = sp1; // 引用计数变为 2
// 使用 sp2
} // sp2 析构,计数变为 1
// sp1 析构时,计数归零,释放内存
这里有个性能细节:shared_ptr 的引用计数是原子操作,线程安全。但原子操作有开销,比 unique_ptr 慢。所以能用 unique_ptr 就别用 shared_ptr。
五、weak_ptr:打破循环引用
shared_ptr 有个大坑:循环引用。两个对象互相持有对方的 shared_ptr,引用计数永远降不到零,内存泄漏。
struct Node {
std::shared_ptr<Node> next;
// 如果两个 Node 互相指向,就循环引用了
};
// 解决方案:用 weak_ptr
struct Node {
std::weak_ptr<Node> next; // 不增加引用计数
};
weak_ptr 是一种弱引用。它不增加引用计数,只是观察资源是否还存在。使用时需要调用 lock() 获取一个 shared_ptr,如果资源已被释放,lock() 返回空。
六、指针与函数
指针作为函数参数,有两种常见用途:输出参数和避免拷贝。
// 输出参数(C 风格)
void getValue(int* out) {
*out = 42;
}
// 现代 C++ 推荐用引用
void getValue(int& out) {
out = 42;
}
// 指针作为函数参数,避免拷贝大对象
void process(const std::vector<int>* data) {
// 处理 data
}
我个人建议:能用引用就别用指针。引用更安全,不可能为空。指针可以为空,调用方必须检查。
函数指针也是 C++ 的重要特性,虽然现代 C++ 更推荐用 std::function 和 lambda。
// 函数指针
int add(int a, int b) { return a + b; }
int (*funcPtr)(int, int) = add;
int result = funcPtr(3, 4); // 7
// 现代 C++ 用 std::function
std::function<int(int, int)> func = add;
// 或者 lambda
auto func2 = [](int a, int b) { return a + b; };
七、知识体系总览
下面这张图把本章的核心知识点串起来了。你看一遍,心里就有谱了。
八、避坑指南
最后,分享几个我实际项目中遇到的教训:
- 不要返回裸指针指向内部数据。 调用方可能忘记 delete,或者指针悬空。返回 shared_ptr 或 unique_ptr 更安全。
- shared_ptr 的循环引用。 我维护过一个缓存系统,对象互相引用,内存只增不减。用 weak_ptr 替换其中一个方向就解决了。
- make_shared 和 shared_ptr 构造的区别。 make_shared 一次分配对象和控制块,性能更好。但如果你需要自定义删除器,只能用 shared_ptr 构造。
- unique_ptr 可以转换为 shared_ptr。 这是合法的,因为独占所有权可以变成共享所有权。反过来不行。
指针进阶这块,说白了就是学会用工具代替手工。智能指针就是 C++ 给你的好工具,别浪费了。
公众号:蓝海资料掘金营,微信deep3321