网易首页 > 网易号 > 正文 申请入驻

封神!万字长文带你吃透 C++ 智能指针

0
分享至

在 C 和 C++ 的世界里,指针几乎无处不在,今天我们拆解原生指针的坑,以及智能指针如何帮我们 “躺平” 管理内存,把底层逻辑摸透。

当指针指向栈内存空间时(即指向函数中定义的局部变量或对象),这些局部变量及对象的生命周期与其所处作用域绑定。这意味着一旦函数调用结束,函数栈帧销毁,其中定义的局部变量和对象也会随之销毁。

但当指针指向堆内存空间时(即指向堆变量或堆数组),情况则不同:堆变量及堆数组的生命周期不再与其所处作用域绑定,而是由程序员显式控制。我们必须通过调用 delete 运算符(C++)或 free 函数(C)来决定何时释放这些内存空间。若未手动释放,这些堆内存对象的生命周期将与程序本身一样长。

因此,当指针指向堆内存时,程序员必须承担动态内存管理的职责。动态内存管理的本质是:当堆内存空间在后续代码中不再需要时,必须及时调用 delete 或 free 释放该空间。这与栈变量 / 数组的自动管理形成鲜明对比 —— 栈对象的生命周期与作用域绑定,作用域结束时系统会自动销毁它们。

虽然程序终止时,操作系统会回收所有内存(包括未释放的堆内存),但这绝不意味着我们可以忽视手动释放。对于需要长期运行的程序(如服务器),频繁申请堆内存时若忘记释放,会导致内存泄漏。由于堆内存空间有限,我们应遵循 "循环利用" 原则:使用完毕后立即释放,以便后续重新分配。严重的内存泄漏最终将耗尽可用堆内存,导致程序崩溃。

通过上文讲解,我们已认识到动态内存管理的重要性,必须确保及时调用 delete 或 free 释放不再使用的堆空间。

然而开发中难免会有疏忽:例如在函数中定义指针指向堆分配的空间,但在执行 delete 前函数就已返回。函数返回时栈帧销毁,存储堆地址的指针(作为局部变量)也随之销毁,导致无法释放对应的堆空间,引发内存泄漏。

虽然通过严谨编码可避免此类疏忽,但即使最谨慎的程序员仍可能遭遇内存泄漏,这与 C++ 的异常机制密切相关:

void fun()myclass* ptr = new myclass;  // 堆内存分配throw exception;             // 异常抛出delete ptr;                  // 此代码被跳过}

C++ 异常机制允许在任何位置抛出异常。异常抛出后,程序会立即在当前作用域查找匹配的 try-catch 块:若存在匹配的 catch 块,则跳过后续代码直接执行 catch 块;若当前作用域无匹配处理,运行时系统会沿调用栈向上回溯,逐层销毁栈帧并查找匹配的 catch 块。

在这个过程中,若释放堆内存的代码(如 delete ptr)位于函数中定义的 try-catch 块之前,或所在函数未包含 try-catch 块,则无法执行释放操作。此时可能有人建议:在 catch 块中添加释放内存的代码。但这种方法仅在异常抛出函数自身包含 try-catch 时有效。

实际上,try-catch 块通常位于调用者而非被调用函数中。这如同工厂车间的分工:被调用函数(“工人”)专注于执行任务,遇到错误时向上级(调用者)报告而非自行处理。因此当异常跨越函数边界传播时,原函数的栈帧已被销毁,持有堆地址的指针不复存在,调用者的 catch 块无法释放该内存。

这种情况下的内存泄漏并非程序员过失所致 —— 即使我们精心编写了 delete 语句,异常仍会中断正常执行流,导致释放代码无法执行。此时,智能指针便成为解决问题的关键方案。

智能指针

由上文可知,由于疏忽或者异常中断程序正常执行流,会导致无法释放动态资源。这种现象的核心原因在于:动态内存管理的责任完全交给了程序员,每次都需要手动调用 delete 释放。而智能指针的本质是将这个责任转移 —— 不再由程序员负责,而是交给智能指针对象。那么智能指针如何获取并执行内存管理权呢?这与其实现机制密切相关。

智能指针的实现并不复杂,其本质是一个类模板实例化的对象。这个类模板的设计十分简洁:封装一个原生指针(用于保存堆内存空间的首地址),并通过构造函数和析构函数实现内存管理权的转移和执行。

templateclass smart_ptrpublic:smart_ptr(T* raw_ptr) : ptr(raw_ptr) {}  // 构造函数接管指针~smart_ptr() { delete ptr; }             // 析构函数自动释放//... 其他成员函数private:T* ptr;  // 封装的原始指针};

构造函数负责初始化封装的原生指针,使其指向堆内存空间。当构造函数执行完成后,该智能指针对象即成为动态资源的所有者。

这里需注意构造函数的参数来源:

// 情况一:直接传递new表达式结果smart_ptr ptr1(new int(10));// 情况二:传递已存在的原生指针int* raw_ptr = new int(20);smart_ptr ptr2(raw_ptr);

情况一:智能指针是堆空间的唯一所有者,无其他指针指向该内存。

情况二:智能指针与原生指针共享所有权(存在双重所有权风险)。

此时引出智能指针的核心机制:析构函数自动释放资源。无论程序执行流程如何(正常返回、提前返回或抛出异常),只要智能指针对象离开作用域,其析构函数就会被调用,进而执行 delete ptr 释放内存。

这种机制完美解决了前文所述问题:

当程序使用智能指针而非原生指针管理堆内存时,若发生函数提前返回或异常抛出导致代码执行中断,函数栈帧销毁前,编译器会自动调用局部对象的析构函数。智能指针的析构函数将释放其管理的动态资源,无需手动干预。此机制将内存管理权移交至智能指针的自动生命周期管理。

但情况二暴露了新的问题:所有权不统一。当原生指针和智能指针同时指向同一块堆内存时:

  • 程序员可能通过原生指针提前 delete 释放内存
  • 智能指针析构时会对已释放的内存再次 delete(导致双重释放错误)
  • 或智能指针在释放后继续访问内存(悬垂指针问题)
auto_ptr

由上文可知,动态资源所有权不统一会带来诸多问题。因此,早在 C++98 标准中,标准库便引入了智能指针的早期实现:auto_ptr。auto_ptr 的核心设计思想是严格维护动态资源所有权的独占性。

所谓所有权的独占性,是指一个动态资源在同一时刻只能被一个智能指针对象或一个原生指针所持有,不允许出现多个智能指针对象共享该资源,或智能指针与原生指针共同持有该资源的情况。

这种所有权的独占性主要体现在其拷贝构造函数和赋值运算符重载函数的实现上。对于构造函数,auto_ptr 接收一个指向堆内存的地址,并将其赋值给类内部封装的原生指针成员变量,完成对象的初始化。

而对于拷贝构造函数,auto_ptr 的行为是将源对象内部原生指针的值直接赋值给目标对象的内部原生指针,同时将源对象的原生指针置为 nullptr。由于 C++98 尚未引入移动语义以及左值(lvalue)、右值(rvalue)的概念(熟悉 C++11 的读者可能已察觉此设计的意图),其设计本质上是模拟了资源所有权的转移。当源对象是右值(例如匿名对象)时,其生命周期在表达式结束后即终止。为了避免其持有的资源随对象析构而被释放,直接将源对象的资源指针 “移动” 到目标对象,并将源指针置空,即可完成所有权的转移。这样,目标对象成为该动态资源的唯一持有者。随后源对象析构时,因其内部指针已为空,也不会错误释放目标对象所指向的资源。

然而,若传入拷贝构造函数的源对象为左值,情况则大不相同。左值对象在拷贝构造后仍然有效,后续代码仍有可能通过该对象访问原资源。但 auto_ptr 无法在拷贝时执行正常的拷贝构造函数的深拷贝逻辑(即开辟新空间,然后复制资源),因为智能指针的语义应模拟原生指针的浅拷贝行为:原生指针的赋值并不会开辟新的堆空间然后让目标指针指向该空间,然后复制源指针所指向的内存内容到目标指针所指向的空间。所以智能指针的拷贝构造函数的标准行为则是浅拷贝,但如果 auto_ptr 的拷贝构造函数的执行逻辑是浅拷贝,那么将会导致多个对象共享同一资源,违背所有权独占的原则。因此,为严格贯彻独占性,C++98 中的 auto_ptr 在面对左值源对象时,其拷贝行为实际上与 C++11 中的移动构造行为一致:转移资源所有权,并将源对象置空。尽管此举维护了所有权的独占性,却导致源左值对象在拷贝后处于无效状态(左值对象内部的原生指针成为空指针),进而可能引发访问错误或逻辑混乱等一系列问题。

templateclass auto_ptrpublic:// 拷贝构造函数:转移所有权,置空源对象auto_ptr(auto_ptr& other)ptr = other.ptr;   // 转移资源指针other.ptr = nullptr; // 源对象指针置空// ... 其他成员函数(如构造函数、析构函数、运算符重载等)...private:T* ptr; // 内部封装的原生指针};

部分读者可能认为主要问题在于后续访问已悬空的智能指针会引发未定义行为(Undefined Behavior)。然而,更为关键的限制在于 auto_ptr 无法安全地与 STL 容器(如 vector)适配。

vector 等容器可以存储任意类型元素,自然也包括 auto_ptr 类型的对象。当 vector 存储 auto_ptr 元素时,常见的操作如访问容器元素或将容器中某个位置的元素赋值给另一个 auto_ptr 对象,都会触发拷贝构造或赋值操作。例如,假设存在一个指向 int 类型堆内存的 auto_ptr,将其放入 vector 后,若将 vector 中该位置的元素赋值给另一个 auto_ptr 对象:

std::vector> arr(10); // 假设已初始化// ... 初始化 arr 中的元素 ...std::auto_ptr ptr = arr[0]; // 触发拷贝构造:arr[0] 的所有权转移给 ptr, arr[0] 内部指针被置空!

赋值操作完成后,vector 中对应位置(arr [0])的元素其内部指针已被置空,变为无效状态。若后续对此 vector 进行排序操作(如 std::sort),并向排序算法提供一个比较器(Comparator),该比较器通常需要解引用 auto_ptr 对象(auto_ptr 重载了 * 运算符)以获取其指向的堆内存值进行比较。此时,由于 arr [0] 已悬空,解引用其内部空指针将导致未定义行为(通常是程序崩溃)。因此,auto_ptr 对象无法安全地用于 STL 容器。

std::sort(arr.begin(), arr.end(), compare);  // 排序时解引用空指针

同理,auto_ptr 的赋值运算符重载函数(针对左值)的实现逻辑也与其拷贝构造函数一致,执行资源所有权的转移和源对象置空操作。正因如此,C++98 的 auto_ptr 自诞生起便饱受诟病,最终在后续标准(C++11)中被更完善的智能指针(如 std::unique_ptr, std::shared_ptr)所取代,逐渐退出了历史舞台。

unique_ptr

了解了 C++98 标准中 auto_ptr 的实现缺陷后,C++ 标准委员会的成员与社区专家合作开发了第三方库 Boost。Boost 库提供了三种智能指针类型:scoped_ptr、shared_ptr 和 weak_ptr。这些实现成为了后续 C++11 标准库智能指针的雏形。具体而言:

  • C++11 的 std::unique_ptr 是对 Boost scoped_ptr 的改进和完善。
  • C++11 的 std::shared_ptr 和 std::weak_ptr 则直接借鉴了 Boost 库中同名组件的实现。

std::unique_ptr 的设计目标正是为了解决和完善 auto_ptr 的缺陷。其核心思想与 auto_ptr 一致:确保动态资源在同一时刻只能被一个智能指针对象或一个原生指针所持有,禁止共享。

如前所述,auto_ptr 的主要缺陷源于其拷贝构造函数和赋值运算符重载函数的实现。C++11 引入了移动语义以及左值(lvalue)、右值(rvalue)的概念,因此拷贝构造函数和赋值运算符通常需要提供两个重载版本:

  1. 拷贝构造 / 赋值 (Copy construction/assignment):接收左值引用 (const T&),执行资源的复制(深拷贝)。
  2. 移动构造 / 赋值 (Move construction/assignment):接收右值引用 (T&&),执行资源所有权的转移。

对于移动构造函数和移动赋值运算符,如前所述,由于它们接收的是右值对象(如临时对象),这些对象在表达式结束后会立即被销毁。因此,可以直接将源对象的资源指针转移给目标对象,并将源对象的指针置为空 (nullptr)。这既高效又安全地完成了所有权的转移。

然而,对于接收左值对象的拷贝构造函数和赋值运算符重载函数:

  • 它们不能采用浅拷贝的实现方式,因为这会导致两个智能指针共享同一资源,违背了所有权独占的原则。
  • 它们更不能采用 auto_ptr 的方式(即移动语义),因为这会强制转移所有权并置空源对象(左值),导致源对象意外失效,引发悬空指针问题。

面对这两种方案均不可行的困境,C++11 标准库为 std::unique_ptr 采用的解决方案是:显式禁用 (delete) 左值版本的拷贝构造函数和拷贝赋值运算符。这是通过在函数声明后添加 = delete 关键字实现的。delete 关键字可以应用于全局函数或类的成员函数。一旦声明为 delete:

  • 程序员不能为该函数提供定义。
  • 如果应用于编译器默认生成的特殊成员函数(如拷贝构造、拷贝赋值等),编译器也将被禁止自动生成该函数的默认实现。
  • 任何尝试调用该函数的代码都将导致编译错误。

templateclass unique_ptrpublic:// 禁用拷贝构造 (左值版本)unique_ptr(const unique_ptr& other) = delete;// 禁用拷贝赋值 (左值版本)unique_ptr& operator=(const unique_ptr& other) = delete;// ... 允许移动构造和移动赋值 ...private:T* ptr;};

通过在左值版本的拷贝构造函数和赋值运算符后添加 delete 关键字,任何尝试使用左值 unique_ptr 对象进行拷贝构造或拷贝赋值的操作都会在编译阶段被阻止。这种方法既严格保证了所有权的独占性,又彻底消除了因意外置空源对象(左值)而引发的安全隐患。

当然,实际开发中确实存在需要多个智能指针对象(或智能指针与原生指针)共享同一动态资源的需求。为此,C++ 标准库提供了第二种智能指针类型:std::shared_ptr,其实现机制将在后文详细探讨。

shared_ptr

std::shared_ptr 的设计允许多个智能指针对象(或智能指针与原生指针)共享同一份动态资源。因此,shared_ptr 与 unique_ptr 的核心区别在于其拷贝构造函数和赋值运算符重载函数的实现机制。

如前文所述,unique_ptr 禁用了左值版本的拷贝构造函数和赋值运算符重载函数(通过 = delete),以严格维护所有权的独占性。与之相反,shared_ptr 明确提供了左值版本的拷贝构造函数和赋值运算符重载函数,这正是其支持资源共享的关键。

可以说,shared_ptr 的设计目标之一就是尽可能模拟原生指针的赋值行为。这种相似性直接源于其拷贝构造函数和赋值运算符重载函数的实现原理:

  • 原生指针的赋值操作执行的是浅拷贝 (Shallow Copy):赋值完成后,两个指针变量指向同一块堆内存空间。
  • shared_ptr 的左值版本拷贝构造函数和赋值运算符重载函数,其核心实现逻辑同样是浅拷贝:将源对象内部封装的原生指针的值直接复制给目标对象的内部指针。关键区别在于,shared_ptr 不会将源对象的内部指针置空 (nullptr)。因此,赋值操作完成后,源对象和目标对象共享对同一动态资源的访问权。

综上所述,shared_ptr 和 unique_ptr 的核心实现原理可以概括如下:

  • unique_ptr:通过禁用拷贝构造 / 赋值(左值)强制所有权独占。
  • shared_ptr:通过浅拷贝(且不置空源对象)支持资源共享。

理解原理是基础,实践则能加深理解。接下来,我们将尝试模拟实现 shared_ptr 和 unique_ptr。这一过程不仅有助于巩固对智能指针内部机制的理解,更能提升我们在实际开发中熟练、安全地使用 shared_ptr 和 unique_ptr 的能力。

关于 weak_ptr,我们将在后续讲解 shared_ptr 时揭示其必要性(涉及一个关键的使用陷阱),届时再引入其概念和实现。此处暂不展开,留作伏笔。

模拟实现unique_ptr构造函数

接下来我们将实现 unique_ptr 对应的类模板。如前文所述,该类模板需要封装一个原生指针。由于 unique_ptr 需要管理任意数据类型的堆内存资源,因此其设计必然是一个模板类。首先讨论其构造函数。

unique_ptr 的构造函数主要有两个版本:

  1. 无参构造函数:构造一个空的 unique_ptr 对象,将其内部指针成员初始化为 nullptr。
  2. 带指针参数的构造函数:接收一个指向堆内存的指针(类型为 T*),并将其赋值给内部指针成员。

templateclass unique_ptrpublic:// 无参构造函数:构造空 unique_ptrunique_ptr(): _ptr(nullptr)// 带指针参数的构造函数:接管给定指针的所有权unique_ptr(T* ptr): _ptr(ptr)private:T* _ptr; // 封装的原生指针,指向管理的堆内存资源templateclass unique_ptrpublic:// 无参构造函数:构造空 unique_ptrunique_ptr(): _ptr(nullptr)// 带指针参数的构造函数:接管给定指针的所有权unique_ptr(T* ptr): _ptr(ptr)private:T* _ptr; // 封装的原生指针,指向管理的堆内存资源};
移动构造与移动赋值

根据 unique_ptr 的设计原理,它禁用了左值版本的拷贝构造函数和拷贝赋值运算符(即禁止复制语义),但支持移动构造和移动赋值。移动语义的核心在于资源所有权的转移:

  • 将源对象(右值引用 other)内部的原生指针赋值给目标对象。
  • 随后将源对象的内部指针置为 nullptr,使其不再拥有该资源的所有权。

移动赋值操作中需要特别注意自赋值(Self-Assignment)的情况,例如:

unique_ptr ptr(new int(10));ptr = std::move(ptr); // 自赋值

在移动赋值函数中,目标对象(*this)通常已经持有一个资源。根据 unique_ptr 的核心原则 —— 同一资源只能被一个 unique_ptr 对象独占拥有 —— 在接管新资源(来自 other)之前,必须释放当前持有的资源(如果有)。

std::move 返回对象的右值引用(即对象本身),而非副本。在移动赋值运算符中,由于 unique_ptr 严格遵循 “单一所有权” 原则,在接管新资源前必须释放当前持有的资源。若发生自赋值,直接释放资源会导致后续访问悬挂指针(dangling pointer),这显然不符合预期(通常意图是保持对象状态不变,置空操作应由 reset () 函数完成)。

因此,移动赋值运算符必须检测并处理自赋值情况:

比较 this 指针与右值引用所绑定对象的地址 (this == &other)。若相同则直接返回 *this(不做任何操作)。

否则:

  • 调用 reset () 释放目标对象当前持有的资源。
  • 接管源对象 other 的资源(ptr = other.ptr;)。
  • 将源对象 other 的内部指针置空(other._ptr = nullptr;)。

unique_ptr& operator=(unique_ptr&& other)if (this != &other) // 检查自赋值reset();          // 释放当前资源_ptr = other._ptr; // 接管新资源other._ptr = nullptr; // 置空源对象指针return *this;}

这里复用了 reset() 成员函数,其功能是释放当前管理的资源并将内部指针置空:

void reset()delete _ptr;_ptr = nullptr;}

移动构造函数的实现相对直接,无需处理自赋值问题:

unique_ptr(unique_ptr&& other): _ptr(other._ptr) // 接管资源other._ptr = nullptr; // 置空源对象指针}
解引用运算符 (operator*) 重载

为了模拟原生指针的解引用行为(ptr),unique_ptr 需要重载 operator。其实现逻辑是返回内部指针所指向对象的引用。

该运算符应提供 const 和非 const 两个版本,以支持对 const unique_ptr 对象的访问:

T& operator*()return *_ptr;const T& operator*() constreturn *_ptr;}
成员访问运算符 (operator->) 重载

为了支持通过智能指针访问其指向对象的成员(ptr->member),需要重载 operator->。其实现逻辑是返回内部的原生指针 _ptr。

当编译器遇到 ptr->member 时,会进行如下操作:

  • 调用 ptr.operator->(),得到一个指向对象的指针(T*)。
  • 对该指针应用内置的 -> 运算符访问 member(即 (ptr.operator->())->member)。

编译器会自动处理这两步,用户只需写一次 ->。例如:

struct MyClass {int a;unique_ptr ptr(new MyClass);ptr->a = 20; // 等价于 (ptr.operator->())->a = 20;

同样需要提供 const 和非 const 版本:

T* operator->()return _ptr;const T* operator->() const noexceptreturn _ptr;}
析构函数

析构函数负责在 unique_ptr 对象生命周期结束时释放其管理的堆内存资源:

~unique_ptr()delete _ptr;}
关键问题:堆对象与堆数组的释放

至此,一个基础的 unique_ptr 框架已实现。然而,上述实现存在一个严重缺陷:它假设管理的资源总是通过 new 分配的单个对象,并在析构时使用 delete 释放。但 unique_ptr 同样可以管理通过 new 分配的数组,此时必须使用 delete 释放。

  • delete 用于释放 new 分配的单个对象:调用对象的析构函数一次,然后释放内存。
  • delete [] 用于释放 new 分配的数组:依次对数组中的每个元素调用析构函数,然后释放整个内存块。

二者不可混用,原因在于内存布局差异:

堆数组分配机制:分配器会在数组内存块头部存储元素数量等元数据(实际分配空间 > 用户请求大小)。new 返回的指针指向首个元素地址(非头部存储元素数量的元数据区)。

delete [] 的工作流程

  • 通过指针偏移访问元数据,获取元素数量 N
  • 逆序调用前 N 个元素的析构函数
  • 调用全局 operator delete [] 释放整个内存块

混用导致的未定义行为

  • 对数组使用 delete:编译器试图调用一次析构函数(针对单个对象),但实际应调用多次(针对数组元素)。未调用剩余元素的析构函数,导致资源泄漏(如果元素持有资源)。释放内存时使用的地址可能不正确(未考虑数组分配可能存在的额外头部信息),导致未定义行为(Undefined Behavior)。
  • 对单个对象使用 delete []:编译器会尝试读取数组大小元信息(位于对象内存之前),但该位置是未定义的随机值。根据这个随机值调用多次析构函数(次数错误),破坏内存。释放内存的地址同样可能错误。

编译器在分配数组时,通常会在返回给用户的指针之前存储数组大小等元信息(具体实现依赖编译器)。delete 需要利用这些信息来确定需要调用多少次析构函数。

解决方案:删除器(Deleter)

上述实现的析构函数(delete _ptr;)仅适用于单个对象。而不支持堆数组,而 unique_ptr 并不知道其管理的资源是单个对象还是数组,所以得需要一个机制,能够让 unique_ptr 在析构时选择正确的释放方式(delete 或 delete [])。这正是删除器(Deleter) 机制要解决的问题。

unique_ptr 定义了一个额外的模板参数。该模板参数将被实例化为可调用对象的类型。这个所谓的删除器,本质上是一个可调用对象。该可调用对象可以是:

  • 指向全局或静态函数的函数指针,
  • 定义了 operator () 成员函数的类(函数对象类)所实例化的对象,
  • 或 lambda 表达式对应的匿名对象。

该可调用对象定义了 unique_ptr 对其所指向的动态资源的正确析构方式。因此,unique_ptr 类内部除了维护一个原生指针 (T* _ptr),还会维护一个可调用对象成员 (Del deleter),即删除器。

我们发现,在使用库提供的 unique_ptr 时,通常不需要显式指定第二个模板参数(删除器的类型),并且在构造时也只需传递一个堆内存地址。这是因为库为该模板参数提供了一个默认模板参数。该默认参数是一个定义了 operator () 成员函数的函数对象类(仿函数类),其 operator () 实现为调用 delete 运算符。这意味着库默认 unique_ptr 指向的资源是单个对象(而非数组)。

因此,我们需要在上文给出的 unique_ptr 实现中进行扩充:

  1. 构造函数:不再只接受一个参数(堆地址),而是需要接收两个参数。其中第二个参数即为删除器对象。这允许我们传递自定义的删除器对象来初始化类内部的 deleter 成员。若未显式提供删除器对象,则第二个参数使用其默认参数(即实例化默认的模板参数类型 Del 的对象)。
  2. 资源释放:在需要释放资源时(例如在 reset () 成员函数和析构函数中),不能再直接调用 delete。由于类内部维护了删除器对象 deleter,我们应通过调用该删除器对象(使用 deleter (_ptr) 语法)来释放资源。析构函数同样如此。

// 默认删除器类:使用 delete 释放单个对象templateclass delpublic:void operator()(T* ptr)delete ptr; // 释放单个对象// unique_ptr 模板类,包含删除器类型参数 Del,默认使用上面的 deltemplate>class unique_ptrpublic:unique_ptr() : _ptr(nullptr) ,deleter(Del()){}// 关键修改:接受删除器对象的构造函数unique_ptr(T* ptr, const Del& _deleter=Del()): _ptr(ptr), deleter(_deleter) // 初始化删除器成员// ... (拷贝构造和拷贝赋值被删除)unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;// ... (移动构造和移动赋值实现)unique_ptr(unique_ptr&& other)_ptr = other._ptr;deleter = other.deleter;other._ptr = nullptr;unique_ptr& operator=(unique_ptr&& other)if (this != &other)reset();         // 释放当前资源_ptr = other._ptr;deleter = other.deleter;other._ptr = nullptr;return *this;// ... (解引用和箭头操作符)void reset()if (_ptr) {deleter(_ptr); // 关键修改:使用删除器释放资源,而非直接 delete_ptr = nullptr;T* get() { return _ptr; }~unique_ptr()if (_ptr) {deleter(_ptr); // 关键修改:使用删除器释放资源private:T* _ptr = nullptr;    // 管理的原生指针Del deleter;          // 关键新增:删除器对象成员};
补充:unique_ptr 的数组特化版

实际上,标准库在实现 unique_ptr 时,为指向数组的情况提供了特化版本 (unique_ptr)。该特化版本将第一个模板参数特化为 T []。T [] 表示一个元素类型为 T 的无边界数组类型。因此,其默认删除器的实现为调用 delete [] 而非 delete,以正确释放动态分配的数组。

shared_ptr

我们知道,shared_ptr 允许多个智能指针对象(或智能指针对象与原生指针)共享同一份资源。没有疑问的是:shared_ptr 的类模板内部必然封装了一个原生指针和一个删除器对象。然而,允许多个智能指针共享资源会引发一个问题:

当一个智能指针对象被销毁时,其析构函数会被调用,进而调用删除器对象以清理其管理的动态资源。如果析构函数按照常规逻辑直接释放该智能指针所拥有的资源,而此时仍有其他智能指针对象共享着该资源,那么释放操作将导致其他共享该资源的智能指针对象持有的原生指针指向一个已被释放的堆内存空间。后续对这些智能指针对象的访问将构成非法操作。

为了解决上述问题,需要引入引用计数机制。引用计数本质上是一个整型变量,其值表示该动态资源当前被多少个智能指针对象共享。当动态资源对应的引用计数不为零时,析构函数不能释放该资源,因为仍有其他智能指针对象共享着它。只有当引用计数降为零时,析构函数才负责释放该动态资源。

接下来的关键点在于如何实现引用计数。

  1. 非静态成员变量方案(缺陷):如果引用计数被设计为 shared_ptr 的非静态成员变量,那么每个实例化的 shared_ptr 对象内部都将持有一个引用计数的副本。共享行为主要通过拷贝构造函数和赋值运算符重载函数实现。但此方案会导致不同对象持有的引用计数副本值不一致,析构时永远无法归零,造成内存泄漏。
  2. 静态成员变量方案(缺陷):如果引用计数被设计为静态成员变量,所有由该类模板实例化产生的智能指针对象共享唯一一个静态引用计数变量。但该变量记录的是所有智能指针对象的总数,而非每一份特定动态资源的共享数量,仍会导致内存泄漏。
  3. 正确方案:使用一个非静态成员变量,但该成员变量本身是一个指向堆内存的指针(即堆变量)。每一份独立的动态资源都会在堆上分配一个对应的整型变量,该变量的值即为共享该动态资源的智能指针对象的数量。

当一个新的智能指针对象需要共享某个动态资源时(通过拷贝构造函数或赋值运算符重载函数与源对象共享其指向的资源),其行为如下:

拷贝构造函数

首先,将源对象所持有的指向动态资源的原生指针赋值给目标对象。

关键点在于,智能指针类内部维护一个指向整型(int*)的指针成员变量,该指针指向堆上分配的引用计数变量。

在拷贝构造函数中,直接将源对象持有的指向引用计数变量的指针也赋值给目标对象对应的指针成员。

这个过程的效果是:所有共享同一份动态资源的智能指针对象,其指向引用计数变量的指针都指向同一个堆内存地址。这意味着它们访问和操作的是同一份引用计数。

赋值运算符重载函数

其核心逻辑与拷贝构造函数基本一致。

区别在于:赋值操作需要先处理目标对象当前已拥有的动态资源(如果有的话)。这通常涉及减少该旧资源引用计数,并在计数归零时释放该资源。

完成旧资源的处理后,后续步骤(复制源对象的资源指针和引用计数指针,并增加引用计数)与拷贝构造函数完全相同。

在这种实现下,析构函数能够正确释放动态资源:

所有共享同一份动态资源的智能指针对象持有指向同一个引用计数变量的指针。

当某个共享对象的析构函数被调用时:它首先将共享的引用计数值递减(--(*count)),然后检查递减后的引用计数值:

  • 如果结果为 0,表明当前被销毁的对象是最后一个拥有该动态资源的智能指针对象,因此析构函数负责释放该动态资源(通过删除器)并释放引用计数变量本身所占用的堆内存。
  • 如果结果不为 0,表明仍有其他智能指针对象共享着该资源,因此析构函数不释放该动态资源。
构造函数

在阐述原理之后,我们将探讨 shared_ptr 的模拟实现。shared_ptr 类内部封装三个成员变量:指向动态资源的指针 (_ptr)、指向引用计数的指针 (count) 以及删除器 (deleter)。与 unique_ptr 不同,shared_ptr 的类模板不维护额外的模板参数来指定可调用对象的类型。此时,包装器 (std::function) 便可发挥作用 ——std::function 能够封装符合特定函数声明的可调用对象,包括函数指针、仿函数对象或 lambda 表达式生成的匿名对象。这些可调用对象通常具有一致的函数签名:返回值类型为 void,参数类型为 T*。

templateclass shared_ptrpublic:private:T* _ptr;int* count;std::function deleter ;};

shared_ptr 的构造函数有两个版本:

默认构造函数: 创建一个空的 shared_ptr 对象,将其内部的资源指针 (_ptr) 和引用计数指针 (count) 均初始化为 nullptr。

带参构造函数: 接收两个参数:

  • 第一个参数 (ptr):指向新创建的堆对象或堆数组的指针。
  • 第二个参数 (_deleter):自定义删除器,其类型由 std::function 包装器接收。此参数提供缺省值,即一个封装了 delete 运算符的仿函数对象(例如 del 的实例),用于释放单个堆对象。

shared_ptr():_ptr(nullptr), count(nullptr)shared_ptr(T* ptr, std::function _deleter = del()): _ptr(ptr), count(new int(1)), deleter(_deleter)}
拷贝构造函数

拷贝构造函数的实现原理已在上文提及。其核心操作是将源对象 (other) 的资源指针 (_ptr)、引用计数指针 (count) 和删除器 (deleter) 复制给目标对象(当前构造的对象)。随后,需递增引用计数 ((*count)++)。

重要注意事项: 源对象可能为空(即其 count 指针为 nullptr)。直接解引用空指针进行递增操作属于未定义行为。因此,递增引用计数前,必须检查 count 指针是否为空。

shared_ptr(const shared_ptr& other)_ptr = other._ptr;count = other.count;deleter = other.deleter;if (other.count != nullptr) // 检查源对象引用计数指针是否有效(*count)++; // 递增共享计数}
赋值运算符重载函数 (operator=)

赋值运算符重载函数的实现逻辑如下:

释放目标对象当前持有的资源

  • 若目标对象的引用计数指针 (count) 非空(即持有资源),则递减其引用计数 ((*count)--)。
  • 递减后,若引用计数变为 0 且 count 指针非空(再次检查),则表明目标对象是最后一个持有该资源的对象。此时需调用删除器 (deleter (_ptr)) 释放资源,并释放引用计数对象 (delete count)。

接管源对象的资源

  • 将源对象的资源指针 (_ptr)、引用计数指针 (count) 和删除器 (deleter) 赋值给目标对象。
  • 递增新资源的引用计数 ((*count)++)。

返回目标对象的引用(return *this)。

自赋值处理

  • 直接自赋值 (this == &other): 如果源对象和目标对象是同一个对象(other 是 *this 的别名),且目标对象独占资源(引用计数为 1),递减操作会将计数减至 0,导致资源被错误释放。后续赋值操作会使指针指向已被释放的资源(悬空指针)。
  • 间接共享 (this != &other 但 ptr == other.ptr): 如果源对象和目标对象不同但已共享同一资源(引用计数 >= 2),递减再递增操作后引用计数不变。虽然逻辑正确,但释放检查是冗余的(计数不会为 0),可以进行优化。

优化策略: 在释放目标对象资源前,检查目标对象 (this) 和源对象 (other) 是否指向不同的动态资源 (if (ptr != other.ptr))。如果指向相同的资源(包括直接自赋值和间接共享),则跳过资源释放步骤。

shared_ptr& operator=(const shared_ptr& other)// 1. 释放当前对象(*this)可能持有的旧资源if (count) { // 检查当前对象是否持有资源(*count)--; // 递减旧资源的引用计数if (*count == 0) { // 检查旧资源引用计数是否归零deleter(_ptr); // 调用删除器释放资源delete count; // 释放引用计数对象// 2. 接管新资源 (other 的资源)_ptr = other._ptr;count = other.count;deleter = other.deleter;// 3. 递增新资源的引用计数 (需确保 count 有效)if (count != nullptr) { // 检查新资源引用计数指针是否有效(*count)++;return *this;}
移动构造函数与移动赋值运算符

移动构造函数(shared_ptr(shared_ptr&& other)): 其实现相对简单。它将源对象 (other) 的资源所有权(资源指针 _ptr、引用计数指针 count 和删除器 deleter)转移给新构造的目标对象。随后,将源对象的 _ptr 和 count 置为 nullptr,使其处于有效但无资源的状态。

移动赋值运算符(operator=(shared_ptr&& other)): 逻辑与拷贝赋值类似,但针对右值源对象:

  1. 释放目标对象 (*this) 当前持有的资源: 逻辑与拷贝赋值步骤 1 相同(检查 count 非空后递减,若减至 0 则释放)。
  2. 接管源对象 (other) 的资源所有权: 将源对象的 _ptr, count, deleter 转移给目标对象。
  3. 置空源对象: 将源对象的 _ptr 和 count 置为 nullptr。
  4. 返回目标对象引用: return *this。

自赋值处理:如果源对象是目标对象自身通过 std::move 得到的右值引用(即 this == &other),且目标对象独占资源(引用计数为 1),递减操作会将计数减至 0,导致资源被错误释放。优化策略:在释放目标对象资源前,检查目标对象 (this) 和源对象 (other) 是否指向不同的动态资源 (if (ptr != other.ptr))。

// 移动构造函数shared_ptr(shared_ptr&& other): _ptr(other._ptr), count(other.count), deleter(other.deleter)other._ptr = nullptr;other.count = nullptr;// other.deleter 通常保持原样或使用默认,因其类型已知且无资源关联// 移动赋值运算符shared_ptr& operator=(shared_ptr&& other)// 1. 检查并处理自移动赋值 (this == &other 或 _ptr == other._ptr)if (this != &other) { // 标准做法:比较对象地址// 2. 释放当前对象(*this)可能持有的旧资源 (逻辑同拷贝赋值步骤1)if (count) {(*count)--;if (*count == 0) {deleter(_ptr);delete count;// 3. 接管新资源 (other 的资源)_ptr = other._ptr;count = other.count;deleter = other.deleter;// 4. 置空源对象 (other)other._ptr = nullptr;other.count = nullptr;return *this;}
析构函数

析构函数的逻辑如下:

  • 检查引用计数指针 (count) 是否非空。若非空,则递减引用计数 ((*count)--)。
  • 再次检查引用计数指针 (count) 是否非空 且 递减后的引用计数是否为 0。
  • 若两个条件均满足,表明当前对象是最后一个持有该资源的 shared_ptr。此时需调用删除器 (deleter (_ptr)) 释放动态资源,并释放引用计数对象 (delete count)。

~shared_ptr()if (count) { // 检查引用计数指针是否有效(*count)--; // 递减引用计数if (*count == 0) { // 检查引用计数是否归零deleter(_ptr); // 调用删除器释放资源delete count; // 释放引用计数对象}
循环引用

表面上看,shared_ptr 的设计似乎已经相当完善,然而它存在一个致命的缺陷:循环引用(Circular Reference)。循环引用的典型场景如下:

存在一个堆上分配的 MyClass 类型对象。该对象内部包含一个 shared_ptr类型的智能指针成员变量。随后,定义两个 shared_ptr 指针 ptr1 和 ptr2,分别指向两个新创建的、不同的堆对象 node1 和 node2。接着,令这两个智能指针所指向对象的 shared_ptr 成员变量互相指向对方,从而形成循环引用。

class MyClasspublic:shared_ptr next; // 成员变量声明int a;int main()MyClass* node1 = new MyClass;MyClass* node2 = new MyClass;shared_ptr ptr1(node1);shared_ptr ptr2(node2);ptr1->next = ptr2;ptr2->next = ptr1;}

此时,我们可以分析引用计数的情况:两个堆对象分别被两个 shared_ptr 对象(ptr1 和 ptr2)共享,同时又被对方内部的 shared_ptr 成员(next)共享。因此,每个对象的引用计数均为 2。

当 ptr2 离开其作用域被销毁时(析构),其析构函数会将 node2 的引用计数减 1(变为 1)。由于引用计数不为零,node2 所指向的动态内存资源不会被释放。随后,ptr2 的析构过程完成。

接着,当 ptr1 离开其作用域被销毁时,其析构函数会将 node1 的引用计数减 1, 但此时 node1 的引用计数依然是 2,因为 node1 没有被销毁,意味着 node1 中的 shared_ptr 依然指向 node1,此时引用计数减完的结果是 1。同样,因为引用计数不为零,node1 所指向的动态内存资源也不会被释放。

最终结果是 node1 和 node2 均未被释放,导致内存泄漏(Memory Leak)。内存泄漏的根本原因在于析构逻辑陷入了死循环:

  • node2 的释放依赖于 node1 内部的 next 成员(指向 node2)先被销毁,即 node1 需要先被销毁。
  • node1 的释放依赖于 node2 内部的 next 成员(指向 node1)先被销毁,即 node2 需要先被销毁。

这种情况如同两只蚱蜢被绑在同一根绳子上,相互牵制,无法解脱。因此,要解决上述循环引用问题,必须引入 weak_ptr。

weak_ptr

weak_ptr 是专门用于解决 shared_ptr 循环引用问题的智能指针,其核心特性是:

  • 不具备其所指向的动态资源的所有权(ownership),仅作为 “观察者”。
  • 可以指向由 shared_ptr 管理的动态资源,但不会影响引用计数。
  • 被销毁时,不会导致引用计数减少或资源释放。
  • 可通过 lock () 方法获取一个临时的 shared_ptr 来访问资源(此时会增加引用计数)。

标准库实现说明:标准库中的 weak_ptr 维护一个指向 “控制块” 的指针,控制块包含强引用计数(shared_ptr 的引用数)和弱引用计数(weak_ptr 的引用数)。本文实现为简化版本,仅维护指向强引用计数的指针。

weak_ptr 类模板内部通常封装:指向动态资源的指针 (_ptr)、指向引用计数的指针 (count) 以及删除器 (deleter)。

构造函数

weak_ptr 的构造函数通常只有一个无参版本的构造函数(默认构造函数),因为 weak_ptr 不能单独使用(创建时必须关联到一个已存在的 shared_ptr 管理的对象上,通常通过拷贝或移动构造从 shared_ptr 或另一个 weak_ptr 获得)。无参构造函数构造一个空的 weak_ptr 对象(不指向任何资源)。

weak_ptr(): _ptr(nullptr)   // 指向动态资源的指针置空, count(nullptr)  // 指向引用计数的指针置空, deleter(nullptr) // 指向删除器的指针置空(或理解为控制块指针置空)}
拷贝构造函数

拷贝构造函数有两个版本:

  1. 接收 weak_ptr 对象:直接进行浅拷贝(Shallow Copy),将源对象(other)的成员变量赋值给目标对象(this)对应的成员变量。
  2. 接收 shared_ptr 对象:同样进行浅拷贝,将源 shared_ptr 对象(other)的成员变量赋值给目标 weak_ptr 对象的成员变量。需在 shared_ptr 类内部添加友元声明(friend class weak_ptr;),允许 weak_ptr 访问 shared_ptr 的私有成员。关键:weak_ptr 不会增加引用计数(count)。

weak_ptr(const weak_ptr& other)_ptr = other._ptr;count = other.count;deleter = other.deleter;weak_ptr(const shared_ptr& other)_ptr = other._ptr;count = other.count;    // 拷贝指向引用计数的指针deleter = other.deleter;}
移动构造以及移动赋值函数
  • 移动构造:将源对象(other)的成员变量移动(转移所有权)给目标对象(this)对应的成员变量,并且将源对象的成员变量设置为空(nullptr)。
  • 移动赋值:首先判断自赋值(if (this != &other)),注意:weak_ptr 无资源管理权,无需释放资源,仅需转移成员变量并置空源对象。

weak_ptr(weak_ptr&& other)_ptr = other._ptr;count = other.count;deleter = other.deleter;other._ptr = nullptr;other.count = nullptr;other.deleter = nullptr;weak_ptr& operator=(weak_ptr&& other)if (this != &other) { // 检查自赋值_ptr = other._ptr;count = other.count;deleter = other.deleter;other._ptr = nullptr;other.count = nullptr;other.deleter = nullptr;return *this; // 返回当前对象的引用}
lock 函数

lock () 函数返回一个指向当前 weak_ptr 对象所指向的动态资源的 shared_ptr 对象。由一个 shared_ptr 对象来接收这个返回值,从而共享该动态资源(会增加引用计数)。

逻辑:

判断当前 weak_ptr 对象的状态(count 是否为空):

  • 若 count 为空(资源已被释放),返回一个空的 shared_ptr 对象。
  • 若资源仍存在,创建一个局部的 shared_ptr 对象(temp),赋值成员变量后将引用计数加一(抵消临时对象析构时的减一),最后返回该临时对象。

shared_ptr lock()if (count) {shared_ptr temp;temp._ptr = _ptr;temp.count = count;      // 共享同一个指向引用计数的指针temp.deleter = deleter;  // 共享删除器(*temp.count)++;return temp;return shared_ptr(); // 返回空的shared_ptr}
析构函数

weak_ptr 由于只是观察者,没有管理权,析构时直接将其指向动态资源的指针(_ptr)、指向引用计数的指针(count)和删除器指针(deleter)设置为空(nullptr)即可。

~weak_ptr()// 重置成员指针_ptr = nullptr;count = nullptr;deleter = nullptr;}
源码my_memory.h

#pragmaonce#includenamespace wz {templateclass delpublic:void operator()(T* ptr)delete ptr;template>class unique_ptrpublic:unique_ptr():_ptr(nullptr),deleter(Del())unique_ptr(T* ptr, const Del& _deleter=Del()):_ptr(ptr), deleter(_deleter)unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;unique_ptr(unique_ptr&& other)_ptr = other._ptr;deleter = other.deleter;other._ptr = nullptr;unique_ptr& operator=(unique_ptr&& other)if (this != &other)reset();_ptr = other._ptr;deleter = other.deleter;other._ptr = nullptr;return *this;T& operator*()return *_ptr;const T& operator*() constreturn *_ptr;T* operator->()return _ptr;const T* operator->() constreturn _ptr;void reset()deleter(_ptr);_ptr = nullptr;T* get()return _ptr;~unique_ptr()deleter(_ptr);private:T* _ptr;Del deleter;templateclass shared_ptrpublic:templatefriend class weak_ptr;shared_ptr():_ptr(nullptr), count(nullptr)shared_ptr(T* ptr, std::function_deleter=del()): _ptr(ptr), count(new int(1)), deleter(_deleter)shared_ptr(const shared_ptr& other)_ptr = other._ptr;count = other.count;deleter = other.deleter;if (other.count != nullptr)(*count)++;shared_ptr(shared_ptr&& other)_ptr = other._ptr;count = other.count;deleter = other.deleter;other._ptr = nullptr;other.count = nullptr;shared_ptr& operator=(const shared_ptr& other)if (count)(*count)--;if (_ptr != other._ptr)if (count&&(*count) == 0)deleter(_ptr);delete count;_ptr = other._ptr;count = other.count;deleter = other.deleter;(*count)++;return *this;shared_ptr& operator=(shared_ptr&& other)if (count)(*count)--;if (_ptr != other._ptr)if ((*count) == 0)deleter(_ptr);delete count;_ptr = other._ptr;count = other.count;deleter = other.deleter;other._ptr = nullptr;other.count = nullptr;return *this;T& operator*()return *_ptr;const T& operator*() constreturn *_ptr;T* operator->()return _ptr;const T* operator->() constreturn _ptr;T* get()return _ptr;~shared_ptr()if (count) {(*count)--;if (count&&(*count) == 0)deleter(_ptr);delete count;private:T* _ptr;int* count;std::function deleter ;templateclass weak_ptrpublic:weak_ptr():_ptr(nullptr), count(nullptr),deleter(nullptr)weak_ptr(const weak_ptr& other)_ptr = other._ptr;count = other.count;deleter = other.deleter;weak_ptr(const shared_ptr& other)_ptr = other._ptr;count = other.count;deleter = other.deleter;weak_ptr(weak_ptr&& other)_ptr = other._ptr;count = other.count;deleter = other.deleter;other._ptr = nullptr;other.count = nullptr;other.deleter = nullptr;weak_ptr& operator=(const weak_ptr& other)_ptr = other._ptr;count = other.count;deleter = other.deleter;return *this;weak_ptr& operator=(weak_ptr&& other)if (this != &other) {_ptr = other._ptr;count = other.count;other._ptr = nullptr;other.count = nullptr;other.deleter = nullptr;return *this;void reset()_ptr =nullptr;count = nullptr;shared_ptr lock()if (count) {shared_ptr temp;temp._ptr = _ptr;temp.count = count;temp.deleter = deleter;(*temp.count)++;return temp;return shared_ptr();~weak_ptr()_ptr = nullptr;count = nullptr;private:T* _ptr;int* count;std::function deleter;}
main.cpp

#define_CRT_SECURE_NO_WARNINGS#include"my_memory.h"#include#include#include// 测试辅助类struct TestResource {TestResource(int id) : id(id) {std::cout << "TestResource " << id << " created\n";instanceCount++;~TestResource() {std::cout << "TestResource " << id << " destroyed\n";instanceCount--;void print() const { std::cout << "Resource ID: " << id << "\n"; }int id;static int instanceCount;int TestResource::instanceCount = 0;// 自定义删除器struct CustomDeleter {void operator()(TestResource* p) {std::cout << "Custom delete for resource " << p->id << "\n";delete p;// 文件资源管理器struct FileDeleter {void operator()(FILE* f) {if (f) {std::cout << "Closing file\n";fclose(f);void test_unique_ptr() {std::cout << "\n===== Testing unique_ptr =====\n";// 测试默认构造wz::unique_ptr empty;if (empty.get() == nullptr) std::cout << "Empty unique_ptr created\n";// 测试基本功能wz::unique_ptr p1(new TestResource(1));p1->print();(*p1).print();} // 应自动销毁// 测试移动语义wz::unique_ptr p2(new TestResource(2));wz::unique_ptr p3 = std::move(p2);if (p2.get() == nullptr) std::cout << "Resource moved to p3\n";p3->print();// 测试自定义删除器wz::unique_ptr p4(new TestResource(4));p4->print();// 测试reset功能wz::unique_ptr p5(new TestResource(5));p5.reset();if (p5.get() == nullptr) std::cout << "Resource reset successfully\n";// 测试文件资源管理wz::unique_ptr file(fopen("test.txt", "w"));if (file.get() != nullptr) {fprintf(file.get(), "Test content");std::cout << "File created and written\n";} // 文件应自动关闭std::cout << "unique_ptr tests passed!\n";void test_shared_ptr() {std::cout << "\\n===== Testing shared_ptr =====\n";// 测试基本构造和析构wz::shared_ptr p1(new TestResource(10));p1->print();std::cout << "Instance count: " << TestResource::instanceCount << "\n";// 测试拷贝构造wz::shared_ptr p2(new TestResource(20));std::cout << "Instance count after p2 creation: " << TestResource::instanceCount << "\n";wz::shared_ptr p3 = p2;p3->print();std::cout << "Instance count after p3 creation: " << TestResource::instanceCount << "\n";std::cout << "Instance count after p3 destruction: " << TestResource::instanceCount << "\n";// 测试赋值操作wz::shared_ptr p4(new TestResource(40));wz::shared_ptr p5;p5 = p4;p5->print();std::cout << "Instance count after assignment: " << TestResource::instanceCount << "\n";// 测试移动语义wz::shared_ptr p6(new TestResource(60));wz::shared_ptr p7 = std::move(p6);if (p6.get() == nullptr) std::cout << "Resource moved to p7\n";p7->print();std::cout << "Instance count after move: " << TestResource::instanceCount << "\n";// 测试自定义删除器wz::shared_ptr p8(new TestResource(80), [](TestResource* p) {std::cout << "Lambda deleter for resource " << p->id << "\n";delete p;p8->print();// 测试循环引用解决方案struct Node {wz::shared_ptr next;wz::weak_ptr prev;int id;Node(int id) : id(id) {std::cout << "Node " << id << " created\n";~Node() {std::cout << "Node " << id << " destroyed\n";wz::shared_ptr node1(new Node(1));wz::shared_ptr node2(new Node(2));node1->next = node2;node2->prev = node1;// 通过weak_ptr验证引用计数auto locked = node2->prev.lock();if (locked.get() != nullptr) {std::cout << "Node1 is still alive via weak_ptr\n";} // 应正确销毁,无内存泄漏std::cout << "shared_ptr tests passed!\n";void test_weak_ptr() {std::cout << "\n===== Testing weak_ptr =====\n";// 测试基本功能wz::weak_ptr weak;wz::shared_ptr shared(new TestResource(100));weak = shared;auto locked = weak.lock();if (locked.get() != nullptr) {locked->print();std::cout << "Weak pointer locked successfully\n";std::cout << "Instance count with weak_ptr: " << TestResource::instanceCount << "\n";} // shared 超出作用域// 测试过期检查auto locked = weak.lock();if ( locked.get() != nullptr) {std::cout << "Error: Weak pointer should be expired\n";else {std::cout << "Weak pointer expired correctly\n";// 测试resetwz::shared_ptr shared(new TestResource(200));wz::weak_ptr weak2(shared);weak2.reset();auto locked = weak2.lock();if ( locked.get() != nullptr) {std::cout << "Error: Weak pointer should be reset\n";else {std::cout << "Weak pointer reset successfully\n";// 测试移动语义wz::shared_ptr shared(new TestResource(300));wz::weak_ptr weak3(shared);wz::weak_ptr weak4 = std::move(weak3);auto locked = weak4.lock();if (locked.get() != nullptr) {locked->print();std::cout << "weak_ptr tests passed!\n";int main() {test_unique_ptr();test_shared_ptr();test_weak_ptr();// 最终检查是否有内存泄漏if (TestResource::instanceCount != 0) {std::cout << "\nWARNING: Memory leak detected! Remaining instances: "<< TestResource::instanceCount << "\n";else {std::cout << "\nAll smart pointer tests completed successfully!\n";return 0;}

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
伊朗“后院”起火?特朗普抛出秘密底牌,美盟友先按捺不住了

伊朗“后院”起火?特朗普抛出秘密底牌,美盟友先按捺不住了

曹兴教授TALK
2026-03-05 13:07:33
猪油再次被关注!医生发现:高血压患者常吃猪油,或出现几种变化

猪油再次被关注!医生发现:高血压患者常吃猪油,或出现几种变化

蜉蝣说
2026-02-23 21:23:05
威胁伊朗,鲁比奥冷不丁来了句“放蒋出笼”…

威胁伊朗,鲁比奥冷不丁来了句“放蒋出笼”…

观察者网
2026-03-05 09:09:04
北京全市普降大雪,怀柔白木出现暴雪

北京全市普降大雪,怀柔白木出现暴雪

新浪财经
2026-03-05 09:57:00
赛季报销!应力性骨折,2米21巨人陨落,一年2次手术啊,真倒霉

赛季报销!应力性骨折,2米21巨人陨落,一年2次手术啊,真倒霉

球童无忌
2026-03-04 20:47:45
字母哥24分吃T复出全败!雄鹿遭老鹰16分逆转 奥孔古21+8

字母哥24分吃T复出全败!雄鹿遭老鹰16分逆转 奥孔古21+8

醉卧浮生
2026-03-05 12:58:18
两人狂奔596公里!阿森纳中场双星成英超最强打工人,有隐患?

两人狂奔596公里!阿森纳中场双星成英超最强打工人,有隐患?

仰卧撑FTUer
2026-03-05 11:00:07
对阵勇士,火箭队恐6人缺阵!内线3高塔全伤,替补2老将机会来了

对阵勇士,火箭队恐6人缺阵!内线3高塔全伤,替补2老将机会来了

熊哥爱篮球
2026-03-05 13:16:28
金价真是一夜要变天了,3月4日最新报价,全国金价竟然差这么多

金价真是一夜要变天了,3月4日最新报价,全国金价竟然差这么多

户外钓鱼哥阿旱
2026-03-05 12:48:50
美媒:卫星图像等显示,伊朗袭击对至少7座美军基地通信雷达系统造成破坏

美媒:卫星图像等显示,伊朗袭击对至少7座美军基地通信雷达系统造成破坏

环球网资讯
2026-03-04 20:22:43
斯科尔斯:他比赖斯强!曼联急寻胖虎接班人,锁定纽卡1亿真核?

斯科尔斯:他比赖斯强!曼联急寻胖虎接班人,锁定纽卡1亿真核?

仰卧撑FTUer
2026-03-05 11:31:03
美国专家称:崛起的中国并不可怕,真正可怕的是他们从来不会提及自身血统

美国专家称:崛起的中国并不可怕,真正可怕的是他们从来不会提及自身血统

文史明鉴
2026-01-29 20:01:08
林良锋:曼联记住,11个软蛋肯定干不过10头牲口!

林良锋:曼联记住,11个软蛋肯定干不过10头牲口!

体坛周报
2026-03-05 13:34:10
伊朗遇袭身亡高层官员分布一览

伊朗遇袭身亡高层官员分布一览

网易新闻出品
2026-03-03 11:58:56
从卢布到伊朗里亚尔的贬值之路中你能明白什么?

从卢布到伊朗里亚尔的贬值之路中你能明白什么?

细雨中的呼喊
2026-03-03 16:40:50
《纯真年代的爱情》大结局,陈副厂长改变6人命运,费霆最意外

《纯真年代的爱情》大结局,陈副厂长改变6人命运,费霆最意外

娱君坠星河
2026-03-05 10:48:01
普京:考虑给欧洲“断气”

普京:考虑给欧洲“断气”

第一财经资讯
2026-03-05 08:11:32
小米Tag官网上架:10g重量,售价69元起

小米Tag官网上架:10g重量,售价69元起

安卓中国
2026-03-03 11:47:07
这才是铁哥们!还清中国81亿欠债,赠百亿大礼,西方各国都眼红

这才是铁哥们!还清中国81亿欠债,赠百亿大礼,西方各国都眼红

霁寒飘雪
2025-12-30 11:54:50
张兰机场独自托运行李,忙碌3小时险误机,汪小菲直播聊娃惹争议

张兰机场独自托运行李,忙碌3小时险误机,汪小菲直播聊娃惹争议

小娱乐悠悠
2026-03-05 09:53:12
2026-03-05 14:03:00
侃故事的阿庆
侃故事的阿庆
几分钟看完一部影视剧,诙谐幽默的娓娓道来
707文章数 7803关注度
往期回顾 全部

科技要闻

阿里内部邮件回应:批准林俊旸辞职

头条要闻

伊朗女校遭袭被传是"伊朗误炸" 媒体核查

头条要闻

伊朗女校遭袭被传是"伊朗误炸" 媒体核查

体育要闻

2026年中超,为什么值得你多看一眼?

娱乐要闻

谢娜下场撕薛之谦,张杰前女友爆猛料

财经要闻

“十五五”开局之年,这么干!

汽车要闻

鸿蒙智行首款猎装车 尚界Z7/Z7T首发

态度原创

本地
手机
旅游
时尚
亲子

本地新闻

食味印象|一口入魂!康乐烤肉串起千年丝路香

手机要闻

9.08mm做到10001mAh:真我王硕揭晓Narzo Power 5G手机电池技术

旅游要闻

温泉、“渝味360碗”……重庆美食美景亮相全球最大旅游专业展会

打底衫,条纹的最适合春天!

亲子要闻

学前一年免费教育惠及1400万儿童,育儿补贴惠及3000多万婴幼儿

无障碍浏览 进入关怀版