1. 项目概述:为什么我们需要智能指针?
在C++的世界里,内存管理一直是开发者必须直面的核心挑战。手动调用new和delete,就像在悬崖边行走,稍有不慎就会导致内存泄漏、悬空指针或者重复释放,这些Bug往往难以追踪,是许多项目稳定性的“定时炸弹”。我自己在早期项目里就踩过不少坑,一个看似简单的对象创建和销毁,在复杂的多线程环境或异常抛出时,很容易就失控了。
C++11标准引入的智能指针,本质上是对原生指针的一层“自动化”封装。它的核心目标不是增加新功能,而是通过RAII(Resource Acquisition Is Initialization,资源获取即初始化)这一经典设计理念,将资源的生命周期与对象的生命周期绑定。当智能指针对象离开其作用域时,其析构函数会自动释放所管理的内存。这听起来简单,但它将我们从繁琐且易错的手动内存管理中解放了出来,让开发者能更专注于业务逻辑本身。
简单来说,智能指针就是帮你“自动”做delete的指针。但它的价值远不止于此。不同的智能指针类型(unique_ptr,shared_ptr,weak_ptr)对应着不同的资源所有权语义,这直接反映了你对程序数据流和对象关系的设计意图。理解并正确使用它们,是写出现代、安全、高效C++代码的基石。无论你是正在学习C++核心特性的新手,还是希望优化现有项目内存模型的老手,深入理解智能指针的原理、实现细节和实战中的那些“坑”,都至关重要。
2. 核心原理与所有权模型解析
智能指针的魅力,根植于其对资源所有权清晰、严谨的定义。所有权决定了“谁”负责销毁资源,这是避免混乱的关键。
2.1 RAII:智能指针的基石
RAII是C++管理资源的黄金法则。其核心思想是:在构造函数中获取资源(如分配内存、打开文件、加锁),在析构函数中释放资源。这样,只要对象本身的生命周期被正确管理(通常是栈对象,离开作用域自动销毁),资源管理就是安全的。
智能指针是RAII最典型的应用。一个std::unique_ptr<T>对象在构造时接受一个原生指针,这个指针就是它要“占有”的资源。当这个unique_ptr对象析构时,它的析构函数会调用delete(或自定义删除器)来释放内存。由于栈对象在离开作用域时会自动析构,内存释放也就自动化了。
void riskyFunction() { int* raw_ptr = new int(42); // 手动分配 // ... 如果这里抛出异常或提前返回,下面的delete将被跳过,内存泄漏! delete raw_ptr; } void safeFunction() { std::unique_ptr<int> smart_ptr(new int(42)); // 构造时获取资源 // ... 即使这里抛出异常,smart_ptr离开作用域时也会自动析构并释放内存 // 无需手动delete }注意:RAII的有效性依赖于对象本身的生命周期管理。如果将智能指针存储在堆上(例如通过
new创建unique_ptr本身),那么你又回到了手动管理的老路,失去了RAII的优势。智能指针本身应该作为栈对象或类的成员变量(其所属的类对象本身也是RAII管理的)。
2.2 独占所有权:std::unique_ptr
unique_ptr实现了独占式所有权。顾名思义,同一时刻,一份资源只能被一个unique_ptr拥有。这种所有权是排他的,不能复制,只能移动。
实现原理浅析:在内部,unique_ptr通常包含一个原生指针成员T* ptr。其拷贝构造函数和拷贝赋值运算符被标记为= delete,禁止拷贝。但移动构造函数和移动赋值运算符是允许的,它们会转移指针的所有权,并将源指针置为nullptr。
std::unique_ptr<MyClass> p1(new MyClass()); // std::unique_ptr<MyClass> p2 = p1; // 错误!禁止拷贝 std::unique_ptr<MyClass> p3 = std::move(p1); // 正确,移动语义转移所有权 // 此时 p1 为空(p1.get() == nullptr),p3 拥有资源这种设计直接对应了“唯一所有者”的场景,比如在工厂模式中创建对象并返回,或者作为类的成员变量来独占管理某个资源。它的开销极小,通常只比原生指针多一点点(可能包含一个自定义删除器的存储),是默认应优先考虑的智能指针。
自定义删除器:unique_ptr的强大之处在于可以定制资源释放方式。默认使用delete,但对于数组、文件指针(FILE*)或其他需要特殊清理的资源,可以指定删除器。
// 管理动态数组 std::unique_ptr<int[]> arr(new int[10]); // 离开作用域时,会调用 delete[],而不是 delete // 管理文件,使用lambda自定义删除器 std::unique_ptr<FILE, decltype([](FILE* f){ if(f) fclose(f); })> filePtr(fopen("data.txt", "r"), [](FILE* f){ if(f) fclose(f); });2.3 共享所有权:std::shared_ptr
当一份资源需要被多个对象共享时,unique_ptr就无能为力了。这时需要shared_ptr。它通过引用计数来实现共享所有权。每多一个shared_ptr指向同一资源,引用计数就加1;每有一个shared_ptr被销毁或重置,计数就减1。当计数减为0时,资源被自动释放。
实现原理深探:shared_ptr的实现比unique_ptr复杂。它通常包含两个部分:
- 指向管理对象的指针:即用户实际需要的
T*。 - 指向控制块的指针:控制块是一个动态分配的内存块,至少包含:
- 引用计数:记录有多少个
shared_ptr共享所有权。 - 弱引用计数:与
weak_ptr相关,后面会讲。 - 删除器:存储如何销毁资源的函数对象。
- 分配器(可选):用于分配控制块本身。
- 引用计数:记录有多少个
当创建一个shared_ptr时,如果是从头创建(例如通过make_shared或new),它会同时分配对象内存和控制块内存(make_shared可能将两者合并为单次分配以提高效率)。当进行拷贝时,只是拷贝指针并增加引用计数,开销很小。
{ std::shared_ptr<MyClass> sp1 = std::make_shared<MyClass>(); { std::shared_ptr<MyClass> sp2 = sp1; // 拷贝构造,引用计数+1 (现在是2) // sp1 和 sp2 共享同一对象 } // sp2 析构,引用计数-1 (现在是1) } // sp1 析构,引用计数-1 (现在是0),对象被销毁循环引用问题:这是shared_ptr最著名的“坑”。如果两个对象互相持有对方的shared_ptr,就会形成循环引用,导致引用计数永远无法归零,内存泄漏。
class Node { public: std::shared_ptr<Node> next; // std::shared_ptr<Node> prev; // 如果这也是 shared_ptr,就会和下一个例子形成循环 }; { auto node1 = std::make_shared<Node>(); auto node2 = std::make_shared<Node>(); node1->next = node2; node2->prev = node1; // 互相持有,循环引用形成! } // 作用域结束,node1和node2的引用计数仍为1(互相指着),内存泄漏。解决循环引用的钥匙,就是weak_ptr。
2.4 弱引用:std::weak_ptr
weak_ptr是对由shared_ptr管理对象的“弱引用”。它不拥有资源的所有权,因此不会增加引用计数。它的存在不影响对象的生命周期。你可以将weak_ptr视为一个观察者。
核心用途:
- 打破循环引用:将上面例子中的
prev改为std::weak_ptr<Node>,它指向node1但不会增加其引用计数。当node1的其他shared_ptr都销毁后,即使node2的weak_ptr还指着它,node1也能被正确释放。 - 缓存与观察:当你需要缓存一个对象,但又不希望缓存阻止其被释放时(例如缓存一个可能被销毁的用户会话对象),可以使用
weak_ptr。 - 避免悬挂指针:相比于保存原生指针,
weak_ptr可以安全地检测所指向的对象是否还存在。
如何使用:weak_ptr不能直接访问资源。必须通过lock()成员函数将其转换为一个shared_ptr。如果对象还存在,lock()返回一个有效的shared_ptr(增加引用计数);如果对象已被释放,则返回一个空的shared_ptr。
std::weak_ptr<MyClass> wp; { auto sp = std::make_shared<MyClass>(); wp = sp; // 弱引用,不增加计数 // 此时 sp 计数为1,wp 弱引用观察 if (auto locked = wp.lock()) { // 尝试提升为 shared_ptr // 对象存在,可以使用 locked } } // sp 析构,对象被销毁 if (auto locked = wp.lock()) { // 再次尝试提升 // 不会进入这里,因为对象已销毁,locked 为空 } else { std::cout << "对象已释放\n"; }3. 实现细节与性能考量
理解了原理,我们还需要深入实现层面,知道不同用法的开销和最佳实践,才能写出高效的代码。
3.1make_shared与make_unique的优势
C++11提供了make_shared,C++14提供了make_unique。它们都是工厂函数,用于创建智能指针管理的新对象。
为什么优先使用它们?
- 异常安全:考虑这段代码
process(std::shared_ptr<MyClass>(new MyClass), std::shared_ptr<Other>(new Other))。C++并未规定函数参数求值顺序。如果编译器先执行new MyClass,然后new Other,而后者抛出异常,那么MyClass的内存就泄漏了,因为shared_ptr的构造函数还没来得及接管它。使用make_shared和make_unique将分配对象和构造智能指针合并为一个原子操作,杜绝了此类问题。 - 性能更优(对
make_shared尤其明显):make_shared通常通过单次内存分配,同时为对象本身和控制块(包含引用计数等)分配一块连续内存。这减少了内存分配开销,可能提高缓存局部性。而直接使用new然后传给shared_ptr构造函数,需要两次分配:一次给对象,一次给控制块。 - 代码简洁:无需重复书写类型。
// 更推荐的方式 auto sp = std::make_shared<MyClass>(arg1, arg2); auto up = std::make_unique<MyClass>(arg1, arg2); // 对比传统方式 std::shared_ptr<MyClass> sp(new MyClass(arg1, arg2)); std::unique_ptr<MyClass> up(new MyClass(arg1, arg2));make_shared的局限性:
- 无法指定自定义删除器:
make_shared使用默认的delete进行销毁。如果你的资源不是通过new分配的,或者需要特殊清理,就不能用make_shared。 - 内存可能延迟释放:由于对象和控制块在同一块内存中,即使所有
shared_ptr都销毁了(引用计数为0),只要还有weak_ptr存在(弱引用计数>0),这块合并的内存就不能被释放,因为控制块还需要存在以供weak_ptr查询对象状态。只有所有weak_ptr也销毁后,整块内存才释放。这可能导致内存占用时间比预期稍长。对于大对象,这可能是个考量点。
3.2 控制块与内存布局
理解控制块对于调试和深入理解行为很有帮助。当你用原生指针构造shared_ptr时,会发生什么?
MyClass* raw_ptr = new MyClass(); std::shared_ptr<MyClass> sp1(raw_ptr); std::shared_ptr<MyClass> sp2(raw_ptr); // 灾难!第二行代码会为raw_ptr指向的对象创建一个新的控制块。第三行代码看到raw_ptr这个原生指针,它并不知道已经有一个控制块存在,于是又创建了第二个控制块。现在有两个独立的shared_ptr,各自拥有一个控制块,都认为自己是资源的唯一所有者(引用计数都是1)。当它们析构时,会各自删除一次资源,导致未定义行为(通常是重复释放,程序崩溃)。
重要规则:绝对不要用同一个原生指针初始化多个独立的
shared_ptr。如果要从原生指针创建shared_ptr,确保在一条语句中完成所有权的移交,之后只使用智能指针。
正确的共享方式是从一个shared_ptr拷贝:
auto sp1 = std::make_shared<MyClass>(); std::shared_ptr<MyClass> sp2 = sp1; // 正确!共享控制块,引用计数增加。3.3 自定义删除器与分配器
智能指针的灵活性很大程度上来自对资源释放方式的定制。
unique_ptr的删除器是类型的一部分。这意味着不同删除器的unique_ptr是不同类型,不能互相赋值。删除器通常作为第二个模板参数传入,如果删除器是无状态的(如函数指针、无捕获的lambda),得益于空基类优化,它不会增加unique_ptr对象的大小。
// 使用函数指针作为删除器 void FileDeleter(FILE* f) { if(f) fclose(f); } std::unique_ptr<FILE, decltype(&FileDeleter)> filePtr(fopen("test.txt", "r"), &FileDeleter); // 使用有状态的函数对象(例如需要记录日志) struct LoggingDeleter { void operator()(MyClass* p) { log("Deleting object at ", p); delete p; } }; std::unique_ptr<MyClass, LoggingDeleter> p(new MyClass, LoggingDeleter{});shared_ptr的删除器不是类型的一部分。它存储在控制块中。这意味着不同删除器的shared_ptr只要管理的对象类型T相同,它们就是相同类型,可以互相赋值、放入同一容器。这提供了更大的灵活性。
auto deleter = [](MyClass* p) { /* 自定义清理 */ delete p; }; std::shared_ptr<MyClass> sp1(new MyClass, deleter); std::shared_ptr<MyClass> sp2 = sp1; // 没问题,删除器信息在控制块里共享分配器的使用场景较少,主要用于控制控制块本身的内存分配位置(例如在特定的内存池中分配)。可以通过std::allocate_shared来使用。
3.4 性能开销分析
unique_ptr:开销几乎可以忽略不计。在大多数实现中,它的大小等同于一个原生指针。运行时开销就是一次析构函数调用(以及可能的自定义删除器调用)。没有额外的内存分配(控制块)。shared_ptr:开销相对较大。- 内存开销:每个被管理的对象至少额外需要一个控制块(包含至少两个引用计数和一些指针)。使用
make_shared可以合并分配,减少开销。 - 时间开销:拷贝
shared_ptr需要原子地增加引用计数(为了线程安全),这是一个相对昂贵的操作。析构时也需要原子地减少计数并检查是否为0。weak_ptr的lock()和析构同样涉及原子操作。 - 建议:在不需要共享所有权的场景,坚决使用
unique_ptr。对于shared_ptr,避免不必要的拷贝,按需使用const&传递只读引用,使用move转移所有权。
- 内存开销:每个被管理的对象至少额外需要一个控制块(包含至少两个引用计数和一些指针)。使用
4. 实战应用与设计模式
智能指针不仅是工具,更影响着代码的设计风格。现代C++鼓励使用基于智能指针和值语义的清晰所有权模型。
4.1 作为函数参数与返回值
传递智能指针的原则:
unique_ptr作为参数:通常表示函数要接管资源的所有权。应使用值传递(通过移动语义)或右值引用std::unique_ptr<T>&&。使用const std::unique_ptr<T>&几乎没有意义,因为它不能转移所有权,还不如直接传递T*或T&。void sink(std::unique_ptr<MyClass> ptr); // 函数接管所有权 auto ptr = std::make_unique<MyClass>(); sink(std::move(ptr)); // 调用后 ptr 为空shared_ptr作为参数:- 如果函数需要保留一个副本(延长对象生命周期),使用值传递
std::shared_ptr<T>。这会触发拷贝,增加引用计数。 - 如果函数只是需要使用对象,而不影响其生命周期,应使用
const std::shared_ptr<T>&或直接使用T*/T&。避免不必要的引用计数增加。
void useObject(const MyClass& obj); // 只需使用,推荐 void maybeKeepRef(const std::shared_ptr<MyClass>& sp); // 可能保留引用,但不一定 void mustKeepRef(std::shared_ptr<MyClass> sp); // 明确需要保留一个副本- 如果函数需要保留一个副本(延长对象生命周期),使用值传递
- 返回智能指针:工厂函数返回
unique_ptr明确表示将所有权转移给调用者。返回shared_ptr则表示返回的是一个共享资源。std::unique_ptr<MyClass> createObject() { return std::make_unique<MyClass>(); } auto obj = createObject(); // 所有权转移到 obj
4.2 在容器中的使用
标准容器(如std::vector,std::map)可以很好地存储智能指针,这比存储原生指针安全得多。
std::vector<std::unique_ptr<MyClass>> vec; vec.push_back(std::make_unique<MyClass>()); // vec 拥有其中所有对象的所有权,当vec销毁时,所有对象自动销毁。 std::vector<std::shared_ptr<MyClass>> sharedVec; auto obj = std::make_shared<MyClass>(); sharedVec.push_back(obj); // 共享所有权使用unique_ptr的容器需要注意,因为unique_ptr不可拷贝,所以向容器添加元素必须使用push_back(std::move(ptr))或emplace_back(...)。
4.3 与多线程的协作
shared_ptr的引用计数操作是原子的,因此从多个线程拷贝/析构同一个对象的shared_ptr是线程安全的。但是,这并不意味着它所指向的对象本身是线程安全的。引用计数的线程安全保证了控制块数据的一致性,但多个线程通过不同的shared_ptr副本去读写同一个对象,仍然需要额外的同步机制(如互斥锁)。
weak_ptr的lock()操作也是线程安全的,它原子地检查引用计数并可能创建一个新的shared_ptr。
unique_ptr的所有权是独占的,将其从一个线程移动到另一个线程需要同步,因为移动操作本身不是原子的。通常的做法是在线程间传递unique_ptr时使用队列等同步数据结构。
4.4 实现一个简易的shared_ptr
为了彻底理解原理,我们可以尝试实现一个简化版的SharedPtr(不考虑线程安全、自定义删除器、弱引用等复杂情况):
template<typename T> class SharedPtr { private: T* ptr_ = nullptr; int* ref_count_ = nullptr; void release() { if (ref_count_ && --(*ref_count_) == 0) { delete ptr_; delete ref_count_; } ptr_ = nullptr; ref_count_ = nullptr; } public: // 构造函数 explicit SharedPtr(T* ptr = nullptr) : ptr_(ptr), ref_count_(ptr ? new int(1) : nullptr) {} // 拷贝构造函数 SharedPtr(const SharedPtr& other) : ptr_(other.ptr_), ref_count_(other.ref_count_) { if (ref_count_) { ++(*ref_count_); } } // 拷贝赋值运算符 SharedPtr& operator=(const SharedPtr& other) { if (this != &other) { release(); // 释放当前资源 ptr_ = other.ptr_; ref_count_ = other.ref_count_; if (ref_count_) { ++(*ref_count_); } } return *this; } // 移动构造函数 SharedPtr(SharedPtr&& other) noexcept : ptr_(other.ptr_), ref_count_(other.ref_count_) { other.ptr_ = nullptr; other.ref_count_ = nullptr; } // 移动赋值运算符 SharedPtr& operator=(SharedPtr&& other) noexcept { if (this != &other) { release(); ptr_ = other.ptr_; ref_count_ = other.ref_count_; other.ptr_ = nullptr; other.ref_count_ = nullptr; } return *this; } // 析构函数 ~SharedPtr() { release(); } T& operator*() const { return *ptr_; } T* operator->() const { return ptr_; } T* get() const { return ptr_; } int use_count() const { return ref_count_ ? *ref_count_ : 0; } explicit operator bool() const { return ptr_ != nullptr; } };这个简易实现清晰地展示了引用计数的核心逻辑:拷贝时计数加一,析构时计数减一,减到零就销毁资源。标准库的std::shared_ptr在此基础上增加了线程安全、弱引用计数、自定义删除器、分配器、别名构造等大量复杂而精妙的功能。
5. 常见“坑点”与最佳实践
即使理解了原理,在实际编码中依然会遇到各种陷阱。下面是我总结的一些常见问题和避坑指南。
5.1 典型错误与排查
| 错误场景 | 现象与后果 | 正确做法 |
|---|---|---|
用同一个new出的指针初始化多个独立shared_ptr | 未定义行为,通常是重复释放导致程序崩溃。 | 使用make_shared,或确保指针所有权立即移交给一个shared_ptr,后续仅通过拷贝来共享。 |
在函数实参中混合new和shared_ptr构造 | 可能因求值顺序导致内存泄漏(异常不安全)。 | 使用make_shared或在单独的语句中先创建shared_ptr。 |
循环引用(两个对象互相持有shared_ptr) | 内存泄漏,对象无法被释放。 | 将其中一个方向改为weak_ptr。 |
将this指针传递给shared_ptr | 如果在对象内部创建了一个管理自己的shared_ptr,会形成新的控制块,可能导致重复释放。 | 使用std::enable_shared_from_this基类,并通过shared_from_this()获取与现有控制块关联的shared_ptr。 |
误用get()获取的裸指针 | 对get()返回的指针进行delete,或用它创建另一个智能指针。 | 将get()返回的指针视为只读的、临时使用的观察指针。绝不管理它的生命周期。 |
在容器中存储auto_ptr(C++17已移除) | auto_ptr的拷贝语义是转移所有权,在容器中会导致意外行为。 | 使用unique_ptr替代。 |
使用shared_ptr管理非堆内存或数组 | 默认删除器使用delete,管理数组应用delete[],否则行为未定义。 | 为数组使用shared_ptr<T[]>(C++17) 或指定自定义删除器[](T* p) { delete[] p; }。对非堆内存,需使用无操作的删除器。 |
5.2enable_shared_from_this的妙用
当一个对象本身已经被shared_ptr管理,而在其成员函数中需要传递指向自身的shared_ptr时(例如,在回调函数中),直接使用this创建新的shared_ptr是错误的,因为会创建新的控制块。
解决方案是让这个类继承自std::enable_shared_from_this<T>。
class MyClass : public std::enable_shared_from_this<MyClass> { public: void registerCallback() { // 错误:auto sp = std::shared_ptr<MyClass>(this); // 正确:获取与已有控制块关联的 shared_ptr auto sp = shared_from_this(); someCallbackRegistry.register(sp); // 安全地传递 shared_ptr } }; int main() { auto obj = std::make_shared<MyClass>(); // 控制块在此创建 obj->registerCallback(); // 内部使用 shared_from_this() 是安全的 }重要限制:必须在对象已经被一个shared_ptr管理之后,才能调用shared_from_this()。否则会抛出std::bad_weak_ptr异常。通常这意味着对象不能在自己的构造函数中调用shared_from_this()。
5.3 智能指针与多态、数组
- 多态:智能指针完美支持多态。基类的智能指针可以指向派生类对象,并且析构时会正确调用派生类的析构函数(前提是基类析构函数是
virtual的)。class Base { public: virtual ~Base() = default; }; class Derived : public Base {}; std::unique_ptr<Base> ptr = std::make_unique<Derived>(); // 正确 - 数组:
unique_ptr对数组有部分特化版本std::unique_ptr<T[]>。它会使用delete[]进行释放。可以通过ptr[i]访问元素。shared_ptr在C++17之前没有内置的数组支持,需要自定义删除器。C++17及以后,可以使用std::shared_ptr<T[]>。
但更现代、更安全的方式是直接使用// C++14/17 使用 unique_ptr 管理数组 std::unique_ptr<int[]> arr(new int[10]); arr[0] = 42; // C++17 之前 shared_ptr 管理数组(需自定义删除器) std::shared_ptr<int> arr_old(new int[10], [](int* p) { delete[] p; }); // C++17 及以后 std::shared_ptr<int[]> arr_new(new int[10]); arr_new[0] = 42;std::vector或std::array。
5.4 性能敏感场景的取舍
在性能至关重要的核心循环或底层代码中,需要仔细权衡智能指针的开销。
- 优先使用
unique_ptr:它的开销最小,应作为默认选择。 - 避免频繁拷贝
shared_ptr:在热点路径上,按引用传递const std::shared_ptr<T>&。考虑使用std::reference_wrapper或原生指针/引用作为函数参数,如果你能保证外部shared_ptr的生命周期覆盖函数执行过程。 - 考虑使用侵入式引用计数:有些库(如 Boost.Intrusive)或框架提供侵入式智能指针,将引用计数存储在对象内部,而不是单独的控制块。这可以减少一次内存分配,并且引用计数的操作可能更快(但对象大小会增加)。这需要对象类型本身支持。
- 手动管理生命周期:在极少数对性能有极端要求、且生命周期非常清晰简单的场景,经验丰富的开发者可能会选择在严格受控的范围内使用原生指针。但这必须是例外而非惯例,并且需要极其谨慎的代码审查和测试。
5.5 调试与排查技巧
- 检查引用计数:使用
sp.use_count()可以查看shared_ptr的引用计数。这在调试循环引用时非常有用。注意,use_count()通常用于调试,不应在业务逻辑中依赖其具体值。 - 观察
weak_ptr是否过期:wp.expired()可以快速检查对象是否已被释放。 - 使用 Valgrind / AddressSanitizer:这些工具可以帮助检测内存泄漏、重复释放等问题。即使使用了智能指针,如果误用(如循环引用),它们也能帮你发现问题。
- 理解核心转储:如果程序因智能指针误用而崩溃(如重复释放),在调试器中查看智能指针内部的控制块指针和对象指针,可以帮助你理解所有权状态。
智能指针是现代C++的支柱之一。从理解其所有权语义开始,到掌握实现细节,再到规避实战中的各种陷阱,是一个不断深化的过程。我的体会是,刚开始可能会觉得束手束脚,但一旦习惯这种清晰的所有权表达方式,代码的安全性和可维护性会得到质的提升。记住一个简单的选择策略:能用unique_ptr就不用shared_ptr;需要共享时再用shared_ptr,并时刻警惕循环引用;观察而不拥有时,使用weak_ptr。把这套组合拳打好,C++内存管理这块硬骨头,就算啃下大半了。