第25章 智能指针
第25章 智能指针
- 理解智能指针如何自动管理内存生命周期
- 掌握三种标准智能指针(unique_ptr, shared_ptr, weak_ptr)的核心用法与区别
- 在实际项目中正确选择和使用智能指针,避免常见陷阱
还记得第9章动态内存管理中,我们手动调用new和delete的繁琐与危险吗?一个疏忽就会导致内存泄漏或双重释放。智能指针就是来解决这个问题的“自动管家”。它本质上是一个类模板,包装了原始指针,并利用C++的析构函数特性,在对象生命周期结束时自动释放内存。
核心概念:所有权
生活类比: 就像你从图书馆借了一本书,在你持有(拥有)这本书期间,你有责任保管它,并在归还日期(生命周期结束)时还回去。智能指针就是帮你自动记录“借阅状态”和“归还日期”的图书管理员。
技术定义: 所有权指对动态分配对象的管理责任,包括其生命周期的控制。智能指针的核心就是清晰定义和转移这种所有权,确保在任何时刻,一个对象都有且只有一个明确的“拥有者”负责其释放。
三种智能指针的对比与选择
C++标准库提供了三种主要的智能指针,它们的所有权模型截然不同。选择哪一个,取决于你的对象需要被多少个“拥有者”持有。
| 类型 | 所有权模型 | 拷贝/赋值行为 | 典型应用场景 | 性能开销 |
|---|---|---|---|---|
std::unique_ptr | 独占所有权 | 禁止拷贝,允许移动 | 工厂函数返回值、作为类成员管理独占资源 | 几乎为零(与原始指针相当) |
std::shared_ptr | 共享所有权 | 允许拷贝,引用计数+1 | 多个对象需要共享访问同一数据(如图形资源、缓存) | 有开销(控制块内存分配,原子操作) |
std::weak_ptr | 弱引用,不拥有所有权 | 不影响引用计数 | 打破shared_ptr的循环引用、观察者模式 | 较小(仅存储弱引用) |
我的经验是,优先使用unique_ptr,它强制你思考清晰的所有权流向。只有当逻辑上确实需要共享时,才使用shared_ptr。weak_ptr则是shared_ptr的辅助工具。
独占之王:std::unique_ptr
unique_ptr如其名,独占它所指向的对象。它不可拷贝,但可以通过std::move转移所有权。这是最轻量、最高效的智能指针。
// 创建一个unique_ptr,管理一个int
std::unique_ptr<int> ptr1(new int(42));
// 使用make_unique是更安全、更高效的方式(C++14起)
auto ptr2 = std::make_unique<int>(100);
// 错误!unique_ptr禁止拷贝构造
// std::unique_ptr<int> ptr3 = ptr1;
// 正确:通过移动转移所有权,ptr1变为nullptr
auto ptr3 = std::move(ptr1);
关键点:make_unique的优势
始终优先使用std::make_unique而非直接new。原因有三:1) 避免内存泄漏(如果构造参数时抛出异常,new分配的内存可能泄漏);2) 代码更简洁;3) 可能带来更好的缓存局部性(一次性分配对象和控制块内存)。我在项目中强制要求使用make_unique。
unique_ptr还可以管理数组,并支持自定义删除器。例如,管理一个用C风格API分配的内存:
// 自定义删除器,使用free释放malloc分配的内存
auto deleter = [](void* p) { std::free(p); };
std::unique_ptr<void, decltype(deleter)> cPtr(std::malloc(1024), deleter);
共享与协作:std::shared_ptr
当多个部分需要“共同拥有”一个对象时,shared_ptr登场。它内部维护一个引用计数,记录有多少个shared_ptr指向同一对象。计数归零时,对象自动销毁。
上图展示了shared_ptr的典型结构。多个shared_ptr共享一个控制块,其中包含引用计数(强引用)和弱引用计数。同样,使用std::make_shared是首选,因为它将对象和控制块合并为一次内存分配,效率更高。
class NetworkResource { /* ... */ };
auto resource = std::make_shared<NetworkResource>();
void process(std::shared_ptr<NetworkResource> res) {
// 函数内,引用计数+1
}
process(resource); // 引用计数变为2
// process函数结束,形参res析构,引用计数变回1
常见误区
- 循环引用导致内存泄漏: 这是
shared_ptr最经典的坑。如果两个对象互相用shared_ptr指向对方,引用计数永远无法归零。解决方案是使用weak_ptr打破循环。 - 从this指针创建shared_ptr: 在类的成员函数内,直接
std::shared_ptr<MyClass>(this)是灾难性的,它会创建一个新的控制块。正确做法是让类继承std::enable_shared_from_this<T>,然后调用shared_from_this()。 - 性能盲目乐观:
shared_ptr的引用计数操作是原子的,以保证线程安全,这会带来开销。在性能关键路径上大量拷贝shared_ptr需谨慎。
观察者与破局者:std::weak_ptr
weak_ptr不单独使用,它总是从shared_ptr创建。它“观察”一个由shared_ptr管理的对象,但不会增加其引用计数。这解决了两个问题:1) 观察对象是否存在而不影响其生命周期;2) 打破shared_ptr的循环引用。
auto shared = std::make_shared<int>(200);
std::weak_ptr<int> weakObs = shared; // 创建弱引用,引用计数仍为1
// 使用前,必须将weak_ptr“提升”为shared_ptr
if (auto tmp = weakObs.lock()) { // lock()尝试提升
// 提升成功,对象还存在,可以安全使用tmp
*tmp = 300;
} else {
// 对象已被释放
}
编者提示: 理解智能指针的所有权转移,特别是std::move的运用,是掌握第26章《移动语义与右值引用》的重要前置知识。移动语义让资源所有权的转移变得高效且明确。
下面用一个典型场景——循环引用,来展示三者的协作关系:
问题场景:循环引用
- 父节点持有子节点的shared_ptr
- 子节点也持有父节点的shared_ptr
- 两者引用计数永远≥1,无法释放
解决方案:weak_ptr破局
- 子节点持有父节点的weak_ptr
- 子节点不增加父节点的引用计数
- 父节点析构时,引用计数归零,可正常释放
- 子节点通过lock()安全访问父节点
智能指针与多态、数组
智能指针完美支持多态。你可以用基类的智能指针指向派生类对象。对于数组,unique_ptr有特化版本unique_ptr<T[]>,它会调用delete[]。而shared_ptr管理数组则需要自定义删除器。
class Base { virtual ~Base() = default; };
class Derived : public Base {};
std::unique_ptr<Base> polyPtr = std::make_unique<Derived>(); // 多态,正确
// 管理数组
std::unique_ptr<int[]> arrPtr = std::make_unique<int[]>(10); // C++14,大小10的数组
arrPtr[0] = 1;
// shared_ptr管理数组(不推荐,优先用std::vector)
std::shared_ptr<int> sharedArr(new int[10], std::default_delete<int[]>());
动手试一试
1. 实现一个简单的资源管理类: 编写一个FileHandler类,使用unique_ptr配合自定义删除器(调用fclose)来管理一个FILE*文件指针。实现构造函数(打开文件)和析构函数(自动关闭)。
2. 模拟观察者模式: 创建两个类Subject和Observer。Subject包含一个vector<weak_ptr<Observer>>列表。Observer包含一个指向Subject的weak_ptr。编写代码让一个Subject和多个Observer建立双向(但非强持有)联系,并确保所有对象都能正确析构,无内存泄漏。
3. 性能对比实验: 写一个循环(例如100万次),分别测试:a) 原始指针new/delete;b) make_unique;c) make_shared。使用<chrono>库测量耗时,直观感受不同智能指针的开销差异。
检验你的理解
- 以下代码片段存在什么问题?会导致什么后果?
std::shared_ptr<int> p1(new int(10)); std::shared_ptr<int> p2(p1.get()); - 如果你想将一个
unique_ptr管理的对象传递给一个函数使用,但该函数调用后你仍需要继续拥有这个对象,函数参数应该声明为什么类型?(值、引用、原始指针?) - 判断对错:
weak_ptr的lock()方法如果返回一个空的shared_ptr,说明其所观察的对象一定已经被释放了。 - 在什么情况下,使用
make_shared创建对象,其内存释放时机可能会比用new创建然后传给shared_ptr构造函数要晚?
本章小结
- 智能指针是自动管理动态内存生命周期的RAII包装器, 核心价值是明确所有权和防止资源泄漏。
- 三种智能指针对应三种所有权模型:
unique_ptr(独占,高效)、shared_ptr(共享,有开销)、weak_ptr(观察,用于打破循环引用)。 - 优先使用
make_unique和make_shared, 它们更安全、更高效,能避免显式new带来的潜在异常安全问题。 - 警惕
shared_ptr的循环引用陷阱, 这是导致内存泄漏的常见原因,使用weak_ptr是标准解决方案。 - 智能指针支持多态和自定义删除器, 使其能够管理几乎所有类型的资源,而不仅仅是
new分配的内存。
微信扫码关注
抖音扫码关注