【C++进阶系列】:万字详解智能指针(附模拟实现的源码)

🔥 本文专栏:c++

🌸作者主页:努力努力再努力wz


💪 今日博客励志语录 当你站在选择的关口,不妨问自己:十年后,我会为做过这件事后悔,还是为没做这件事遗憾?答案里藏着勇气的密码。


引入

提到指针,想必各位读者对此并不陌生。指针是C/C++的灵魂,通过指针我们可以更加灵活地访问内存。指针既可以指向栈内存上的一段空间,也可以指向堆内存上的一段空间,借助指针我们可以访问内存中存储的内容。

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

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

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

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


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

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

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

cpp 复制代码
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释放。而智能指针的本质是将这个责任转移------不再由程序员负责,而是交给智能指针对象。那么智能指针如何获取并执行内存管理权呢?这与其实现机制密切相关。

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

cpp 复制代码
template<typename T>
class smart_ptr
{
public:
    smart_ptr(T* raw_ptr) : ptr(raw_ptr) {}  // 构造函数接管指针
    ~smart_ptr() { delete ptr; }             // 析构函数自动释放
    //... 其他成员函数
private:
    T* ptr;  // 封装的原始指针
};

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

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

cpp 复制代码
// 情况一:直接传递new表达式结果
smart_ptr<int> ptr1(new int(10)); 

// 情况二:传递已存在的原生指针
int* raw_ptr = new int(20);
smart_ptr<int> ptr2(raw_ptr);
  • 情况一:智能指针是堆空间的唯一所有者,无其他指针指向该内存。
  • 情况二:智能指针与原生指针共享所有权(存在双重所有权风险)。

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

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

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

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

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

auto_ptr

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

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

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

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

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

cpp 复制代码
template<typename T>
class auto_ptr
{
public:
    // 拷贝构造函数:转移所有权,置空源对象
    auto_ptr(auto_ptr<T>& 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 对象:

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

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

cpp 复制代码
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_ptrshared_ptrweak_ptr。这些实现成为了后续 C++11 标准库智能指针的雏形。具体而言:

  • C++11 的 std::unique_ptr 是对 Boost scoped_ptr 的改进和完善。
  • C++11 的 std::shared_ptrstd::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

  1. 程序员不能为该函数提供定义。
  2. 如果应用于编译器默认生成的特殊成员函数(如拷贝构造、拷贝赋值等),编译器也将被禁止自动生成该函数的默认实现。
  3. 任何尝试调用该函数的代码都将导致编译错误。
cpp 复制代码
template<typename T>
class unique_ptr
{
public:
    // 禁用拷贝构造 (左值版本)
    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_ptrunique_ptr 的核心区别在于其拷贝构造函数和赋值运算符重载函数的实现机制。

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

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

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

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

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

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

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

模拟实现

unique_ptr

构造函数

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

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

  1. 无参构造函数:构造一个空的 unique_ptr 对象,将其内部指针成员初始化为 nullptr
  2. 带指针参数的构造函数:接收一个指向堆内存的指针(类型为 T*),并将其赋值给内部指针成员。
cpp 复制代码
template<typename T>
class unique_ptr
{
public:
    // 无参构造函数:构造空 unique_ptr
    unique_ptr() 
        : _ptr(nullptr)
    {
    }

// 带指针参数的构造函数:接管给定指针的所有权
 unique_ptr(T* ptr) 
    : _ptr(ptr)
{
}
//............................................

private:
    T* _ptr; // 封装的原生指针,指向管理的堆内存资源
};
移动构造与移动赋值

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

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

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

cpp 复制代码
unique_ptr<int> 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(不做任何操作)。
  • 否则:
    1. 调用 reset() 释放目标对象当前持有的资源。
    2. 接管源对象 other 的资源(_ptr = other._ptr;)。
    3. 将源对象 other 的内部指针置空(other._ptr = nullptr;)。
cpp 复制代码
unique_ptr& operator=(unique_ptr&& other) noexcept
{
    if (this != &other) // 检查自赋值
    {
        reset();          // 释放当前资源
        _ptr = other._ptr; // 接管新资源
        other._ptr = nullptr; // 置空源对象指针
    }
    return *this;
}

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

cpp 复制代码
void reset() noexcept
{
    delete _ptr; // 释放资源 (注意:此处原代码有笔误,应为 delete 而非 deleter)
    _ptr = nullptr;
}

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

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

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

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

cpp 复制代码
T& operator*() noexcept
{
    return *_ptr;
}
const T& operator*() const noexcept
{
    return *_ptr;
}
成员访问运算符 (operator->) 重载

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

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

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

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

cpp 复制代码
struct MyClass {
    int a;
};
unique_ptr<MyClass> ptr(new MyClass);
ptr->a = 20; // 等价于 (ptr.operator->())->a = 20;

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

cpp 复制代码
T* operator->() 
{
    return _ptr;
}
const T* operator->() const noexcept
{
    return _ptr;
}
析构函数

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

cpp 复制代码
~unique_ptr() 
{
    delete _ptr;
}

关键问题:堆对象与堆数组的释放

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

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

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

  1. 堆数组分配机制
    分配器会在数组内存块头部存储元素数量等元数据(实际分配空间 > 用户请求大小)。new 返回的指针指向首个元素地址(非头部存储元素数量的元数据区)。
  2. delete[] 的工作流程
    • 通过指针偏移访问元数据,获取元素数量 N
    • 逆序调用前 N 个元素的析构函数
    • 调用全局 operator delete[] 释放整个内存块
  3. 混用导致的未定义行为
    1. 对数组使用 delete
      • 编译器试图调用一次析构函数(针对单个对象),但实际应调用多次(针对数组元素)。
      • 未调用剩余元素的析构函数,导致资源泄漏(如果元素持有资源)。
      • 释放内存时使用的地址可能不正确(未考虑数组分配可能存在的额外头部信息),导致未定义行为(Undefined Behavior)。
    2. 对单个对象使用 delete[]
      • 编译器会尝试读取数组大小元信息(位于对象内存之前),但该位置是未定义的随机值。
      • 根据这个随机值调用多次析构函数(次数错误),破坏内存。
      • 释放内存的地址同样可能错误。

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

解决方案:

上述实现的析构函数(delete _ptr;)仅适用于单个对象。而不支持堆数组,而 unique_ptr并不知道其管理的资源是单个对象还是数组,所以得需要一个机制,能够让unique_ptr在析构时选择正确的释放方式(deletedelete[])。这正是删除器(Deleter) 机制要解决的问题。我们将在后续讨论中实现自定义删除器功能,使 unique_ptr 能够灵活、安全地管理不同类型的资源。

删除器

因此,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) 语法)来释放资源。析构函数同样如此。
cpp 复制代码
// 默认删除器类:使用 delete 释放单个对象
template<typename T>
class del
{
public:
    void operator()(T* ptr) 
    {
        delete ptr; // 释放单个对象
    }
};

// unique_ptr 模板类,包含删除器类型参数 Del,默认使用上面的 del<T>
template<typename T, typename Del = del<T>>
class unique_ptr
{
public:
    unique_ptr() : _ptr(nullptr) ,deleter(Del()){}

// 关键修改:接受删除器对象的构造函数
unique_ptr(T* ptr, const Del& _deleter=Del())
    : _ptr(ptr)
    , deleter(_deleter) // 初始化删除器成员
{}

// ... (拷贝构造和拷贝赋值被删除)
unique_ptr(const unique_ptr<T, Del>&) = delete;
unique_ptr& operator=(const unique_ptr<T, Del>&) = delete;

// ... (移动构造和移动赋值实现)
unique_ptr(unique_ptr<T, Del>&& other)
{
    _ptr = other._ptr;
    deleter = other.deleter; 
    other._ptr = nullptr;
}
unique_ptr& operator=(unique_ptr<T, Del>&& 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>)。该特化版本将第一个模板参数特化为 TT 表示一个元素类型为 T 的无边界数组类型。因此,其默认删除器的实现为调用 delete 而非 delete,以正确释放动态分配的数组。

shared_ptr

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

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

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

接下来的关键点在于如何实现引用计数。如果引用计数被设计为 shared_ptr 的非静态成员变量,那么每个实例化的 shared_ptr 对象内部都将持有一个引用计数的副本。

共享行为主要通过拷贝构造函数和赋值运算符重载函数实现。对于无参构造函数(构造空智能指针对象,不拥有任何动态资源),其引用计数副本应初始化为 0。对于带参构造函数(接收一个堆内存地址及可选的删除器,该地址指向新创建的堆对象或堆数组),其引用计数副本应初始化为 1。

当通过拷贝构造或赋值操作将源对象(source)共享的资源赋予目标对象(destination)时,意味着多了一个智能指针对象共享该资源,引用计数应当递增。由于拷贝构造函数的参数是源对象的引用,在完成源对象到目标对象的浅拷贝(主要是复制原生指针)后,需要将源对象和目标对象内部的引用计数副本都加一。

然而,上述实现方式存在一个致命缺陷。假设仅有两个智能指针对象(A 和 B)通过拷贝或赋值共享同一份资源,此时双方的引用计数副本均为 2(正确)。但当第三个对象(C)通过拷贝或赋值加入共享时,操作会使其中两个对象(例如 A 和 C)的引用计数副本加一(变为 3),而另一个对象(B)的引用计数副本无法同步更新(仍为 2)。这导致不同对象持有的引用计数副本值不一致。

更严重的问题出现在析构过程中。析构函数的行为是:先将自身持有的引用计数副本减一,然后判断结果是否为零。若不为零,表明仍有其他智能指针共享该资源,故不释放资源;若为零,则释放资源。由于每个对象仅持有自己的引用计数副本,当一个共享对象(例如 A)被销毁时,它只会将其自身的引用计数副本减一(例如从 3 减为 2),而其他共享对象(B 和 C)持有的引用计数副本保持不变(例如 B 仍为 2,C 仍为 3)。对于任何非独占(引用计数初始值 ≥ 2)的动态资源,每个共享对象的析构操作都只会将其自身的引用计数副本减一,最终结果始终 ≥ 1(因为初始值 ≥ 2,且每次只减 1),永远无法归零。这导致析构函数永远无法满足释放条件(引用计数副本降为零),从而造成内存泄漏。


而如果引用计数被设计为静态成员变量,由于静态成员变量属于类本身而非类的各个实例对象,那么在拷贝构造函数或赋值运算符重载函数中,完成源对象到目标对象的指针赋值后,只需对同一个静态引用计数变量进行递增操作。

此时,所有共享同一份动态资源的智能指针对象访问的都是同一个引用计数变量,因此引用计数的更新是全局一致的,解决了非静态成员变量方案中引用计数副本不一致的问题。在析构函数中,对静态引用计数进行递减操作,也会影响所有共享该资源的智能指针对象持有的(同一个)引用计数值。理论上,这避免了引用计数无法归零的情况。

然而,静态成员变量的方案实际上不可行。其核心缺陷在于:同一个类模板实例化产生的所有智能指针对象共享唯一一个静态引用计数变量。但问题在于,这些由同一个类模板实例化生成的对象,并不一定共享同一份动态资源;它们可能分别管理着多份不同的动态资源。

因此,单一静态引用计数变量记录的是所有由该类模板实例化产生的管理动态资源的智能指针对象的总数量(无论它们管理的是否为同一资源),而非每一份特定动态资源所对应的共享智能指针对象的数量。

这将导致严重问题:当管理某一特定动态资源的所有共享智能指针对象都已被销毁时,该资源理应立即被释放。但由于静态引用计数值反映的是所有现存智能指针对象的总数(不为零),该资源无法被释放。只有当最后一个由该类模板实例化产生的智能指针对象被销毁(无论它管理的是哪份资源)时,静态引用计数才会降为零。此时,析构函数会释放该最后一个对象所管理的资源。而在此之前,其他早已失去所有智能指针管理的动态资源(其对应的共享对象已全部销毁)却因引用计数未归零而无法被释放,从而造成内存泄漏


因此,引用计数的正确实现方式应为:使用一个非静态成员变量,但该成员变量本身是一个指向堆内存的指针(即堆变量)。这样,每一份独立的动态资源都会在堆上分配一个对应的整型变量,该变量的值即为共享该动态资源的智能指针对象的数量。

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

  1. 拷贝构造函数:

    • 首先,将源对象所持有的指向动态资源的原生指针赋值给目标对象。
    • 关键点在于,智能指针类内部维护一个指向整型(int*)的指针成员变量,该指针指向堆上分配的引用计数变量。
    • 在拷贝构造函数中,直接将源对象持有的指向引用计数变量的指针也赋值给目标对象对应的指针成员。
    • 这个过程的效果是:所有共享同一份动态资源的智能指针对象,其指向引用计数变量的指针都指向同一个堆内存地址。这意味着它们访问和操作的是同一份引用计数。
  2. 赋值运算符重载函数:

    • 其核心逻辑与拷贝构造函数基本一致。
    • 区别在于:赋值操作需要先处理目标对象当前已拥有的动态资源(如果有的话)。这通常涉及减少该旧资源引用计数,并在计数归零时释放该资源。
    • 完成旧资源的处理后,后续步骤(复制源对象的资源指针和引用计数指针,并增加引用计数)与拷贝构造函数完全相同。

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

  • 所有共享同一份动态资源的智能指针对象持有指向同一个引用计数变量的指针。
  • 当某个共享对象的析构函数被调用时:
    • 它首先将共享的引用计数值递减(--*ref_count)。
    • 然后检查递减后的引用计数值:
      • 如果结果为 0,表明当前被销毁的对象是最后一个拥有该动态资源的智能指针对象,因此析构函数负责释放该动态资源(通过删除器)并释放引用计数变量本身所占用的堆内存。
      • 如果结果不为 0,表明仍有其他智能指针对象共享着该资源,因此析构函数不释放该动态资源。

构造函数

在阐述原理之后,我们将探讨 shared_ptr 的模拟实现。shared_ptr 类内部封装三个成员变量:指向动态资源的指针 (_ptr)、指向引用计数的指针 (count) 以及删除器 (deleter)。与 unique_ptr 不同,shared_ptr 的类模板不维护额外的模板参数来指定可调用对象的类型。这意味着我们无法通过显式实例化类模板来为 shared_ptr 提供删除器的类型信息。那么,如何获取可调用对象的类型信息呢?

此时,包装器 (std::function) 便可发挥作用。我们知道 std::function 能够封装符合特定函数声明的可调用对象,包括函数指针、仿函数对象(函数对象)或 lambda 表达式生成的匿名对象。这些可调用对象(全局函数、静态函数、仿函数的 operator() 或 lambda 的 operator())通常具有一致的函数签名:返回值类型为 void,参数类型为 T*

cpp 复制代码
template<typename T>
class shared_ptr
{
 public:
    ...............
private:
    T* _ptr;
	int* count;
	std::function<void(T*)> deleter ;
}

因此,我们可以在 shared_ptr 类中定义一个 std::function 对象,其封装的可调用对象类型为 void(T*)shared_ptr 的构造函数有两个版本:默认构造函数和带参构造函数。

  • 默认构造函数: 创建一个空的 shared_ptr 对象,将其内部的资源指针 (_ptr) 和引用计数指针 (count) 均初始化为 nullptr
  • 带参构造函数: 接收两个参数:
    1. 第一个参数 (ptr):指向新创建的堆对象或堆数组的指针。
    2. 第二个参数 (_deleter):自定义删除器,其类型由 std::function<void(T*)> 包装器接收。此参数提供缺省值,即一个封装了 delete 运算符的仿函数对象(例如 del<T> 的实例),用于释放单个堆对象。
cpp 复制代码
shared_ptr()
    :_ptr(nullptr)
    , count(nullptr)
{
}

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

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

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

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

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

  1. 释放目标对象(左值,即 *this)当前持有的资源:

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

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

自赋值处理:

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

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

  1. 避免直接自赋值导致资源被过早释放。
  2. 避免间接共享情况下执行不必要的释放检查(小优化)。
  3. 仅在目标对象持有不同资源时才执行释放逻辑。
cpp 复制代码
shared_ptr& operator=(const shared_ptr<T>& 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<T>&& other)): 其实现相对简单。它将源对象 (other) 的资源所有权(资源指针 _ptr、引用计数指针 count 和删除器 deleter)转移给新构造的目标对象。随后,将源对象的 _ptrcount 置为 nullptr,使其处于有效但无资源的状态。

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

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

自赋值处理:

  • 如果源对象是目标对象自身通过 std::move 得到的右值引用(即 this == &other),且目标对象独占资源(引用计数为 1),递减操作会将计数减至 0,导致资源被错误释放。后续接管操作会试图转移已被释放的资源(悬空指针)。
  • 优化策略: 在释放目标对象资源前(步骤 1),检查目标对象 (this) 和源对象 (other) 是否指向不同的动态资源 (if (_ptr != other._ptr))。如果指向相同的资源(即移动自赋值),则跳过资源释放步骤(步骤 1)。这样可以避免移动自赋值导致资源被错误释放。
cpp 复制代码
// 移动构造函数
shared_ptr(shared_ptr<T>&& other) 
    : _ptr(other._ptr)
    , count(other.count)
    , deleter(other.deleter)
{
    other._ptr = nullptr;
    other.count = nullptr;
    // other.deleter 通常保持原样或使用默认,因其类型已知且无资源关联
}

// 移动赋值运算符
shared_ptr& operator=(shared_ptr<T>&& 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;

}
析构函数

析构函数的逻辑如下:

  1. 检查引用计数指针 (count) 是否非空。若非空,则递减引用计数 ((*count)--)。
  2. 再次检查引用计数指针 (count) 是否非空 且 递减后的引用计数是否为 0
    • 若两个条件均满足,表明当前对象是最后一个持有该资源的 shared_ptr。此时需调用删除器 (deleter(_ptr)) 释放动态资源,并释放引用计数对象 (delete count)。
cpp 复制代码
~shared_ptr()
{
    if (count) { // 检查引用计数指针是否有效
        (*count)--; // 递减引用计数
        if (*count == 0) { // 检查引用计数是否归零
            deleter(_ptr); // 调用删除器释放资源
            delete count; // 释放引用计数对象
        }
    }
}
循环引用

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

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

cpp 复制代码
class MyClass 
{
public:
    shared_ptr<MyClass> next; // 成员变量声明
    int a;
};

int main()
{
    MyClass* node1 = new MyClass; 
    MyClass* node2 = new MyClass; 

shared_ptr<MyClass> ptr1(node1);
shared_ptr<MyClass> ptr2(node2);

ptr1->next = ptr2; 
ptr2->next = ptr1; 

}

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

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

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

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

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

这种情况如同两只蚱蜢被绑在同一根绳子上,相互牵制,无法解脱。

因此,要解决上述循环引用问题,必须引入weak_ptr,这也正是前文所埋下的伏笔。

weak_ptr

那么所谓的weak_ptr,它虽然也是一个智能指针,但是它不会单独出现,而是专门应用于上文所说的循环引用的场景中登场。那么weak_ptrunique_ptr以及shared_ptr的不同点就是weak_ptr是不具备其所指向的动态资源的所有权(ownership)的。但是weak_ptr可以指向一个动态资源(通常是由shared_ptr管理的资源)。而此时weak_ptr对于其指向的动态资源所承担的角色就是一个观察者(Observer)。所谓观察者,就是可以访问其指向的动态资源(通过lock()方法获取一个临时的shared_ptr),但是不会参与动态资源的管理(不控制其生命周期)。也就是意味着weak_ptr被销毁,那么其不会影响到其指向的动态资源(不会导致引用计数减少或资源释放)。

那么weak_ptr对应的类模版的实现中,其同样会维护多个成员变量(具体实现可能不同,但核心信息需要保存),通常分别是指向动态资源的指针(_ptr)、指向控制块(包含强引用计数和弱引用计数)的指针(count)以及删除器(deleter)。

那么之所以会封装(或需要存储)这三个成员变量的原因,就是因为weak_ptr需要能够观察shared_ptr所指向的对象,并且weak_ptr可以转化为shared_ptr(通过lock()方法),那么其内部必然要存储这些必要的信息(以便在需要时安全地提升为shared_ptr并管理资源)。

那么接下来我们就来认识一下weak_ptr的各个成员函数:

构造函数

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

cpp 复制代码
weak_ptr()
    : _ptr(nullptr)   // 指向动态资源的指针置空
    , count(nullptr)  // 指向控制块(包含引用计数信息)的指针置空
    , deleter(nullptr) // 指向删除器的指针置空(或理解为控制块指针置空)
{

}

拷贝构造函数

那么这里的拷贝构造函数有两个版本:

  1. 接收一个weak_ptr对象的拷贝构造函数:就是直接进行浅拷贝(Shallow Copy),将源对象(other)的成员变量赋值给目标对象(this)对应的成员变量。
  2. 接收一个shared_ptr对象的拷贝构造函数:也是进行浅拷贝,将源shared_ptr对象(other)的成员变量赋值给目标weak_ptr对象的成员变量。但是这里由于shared_ptrweak_ptr是不同类,所以这里需要在shared_ptr类内部添加一个友元声明(friend class weak_ptr<T>;),允许weak_ptr访问shared_ptr的私有(private)成员变量(_ptr, count, deleter)。最关键的就是这里weak_ptr接收一个shared_ptr对象,指向shared_ptr对象所指向的空间,那么其不会增加强引用计数(use_count),但可能会增加弱引用计数(weak_count)(注意:弱引用计数用于跟踪有多少weak_ptr观察着该资源,与资源释放无关,仅用于控制块的生命周期管理)。
cpp 复制代码
weak_ptr(const weak_ptr<T>& other) 
{
    _ptr = other._ptr;
    count = other.count;    
    deleter = other.deleter;
}

weak_ptr(const shared_ptr<T>& other) 
{
    _ptr = other._ptr;
    count = other.count;    // 拷贝控制块指针(包含强/弱引用计数)
    deleter = other.deleter;
}
移动构造以及移动赋值函数

那么对于移动构造,其实现原理很简单,就是将源对象(other)的成员变量移动(转移所有权)给目标对象(this)对应的成员变量,并且将源对象的成员变量设置为空(nullptr)。而对于移动赋值函数,则需要首先判断一下自赋值(if (this != &other)),那么注意这里确实不要释放其指向的资源,因为这里weak_ptr没有管理权(不持有所有权,只观察)。

cpp 复制代码
weak_ptr(weak_ptr<T>&& other) 
{
    _ptr = other._ptr;
    count = other.count;
    deleter = other.deleter;
    other._ptr = nullptr;
    other.count = nullptr;
    other.deleter = nullptr;
}

weak_ptr<T>& operator=(weak_ptr<T>&& 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对象的状态,也就是weak_ptr是否是一个空对象或者其观察的资源是否已被释放。通过判断指向控制块的指针(count)是否为空以及强引用计数是否大于0来判断。如果count为空或强引用计数为0(资源已被释放),就返回一个空的shared_ptr对象。如果资源仍然存在(强引用计数>0),那么就创建一个局部的shared_ptr对象(temp),并且完成对成员变量(_ptr, count, deleter)的赋值,然后增加强引用计数(use_count),最后传值返回即可。

cpp 复制代码
shared_ptr<T> lock() 
{
    if (count) { 
        shared_ptr<T> temp;
        temp._ptr = _ptr;
        temp.count = count;      // 共享同一个控制块
        temp.deleter = deleter;  // 共享删除器(或通过控制块获取)
        return temp;
    }
    return shared_ptr<T>(); // 返回空的shared_ptr
}
析构函数

那么weak_ptr由于只是观察者,没有管理权,那么这里在析构时直接将其指向动态资源的指针(_ptr)以及指向控制块的指针(count)和删除器指针(deleter)设置为空(nullptr)即可。

cpp 复制代码
~weak_ptr()
{
    // 重置成员指针
    _ptr = nullptr;
    count = nullptr;
    deleter = nullptr;
}

源码

my_memory.h:

cpp 复制代码
pragma once
#include<functional>
namespace wz {
	template<typename T>
	class del
	{
	public:
		void operator()(T* ptr)
		{
			delete ptr;
		}
	};
	template<typename T, typename Del = del<T>>
	class unique_ptr
	{
	public:
		unique_ptr()
			:_ptr(nullptr)
			,deleter(Del())
		{

		}
		unique_ptr(T* ptr, const Del& _deleter=Del())
			:_ptr(ptr)
			, deleter(_deleter)
		{

		}
		unique_ptr(const unique_ptr<T, Del>&) = delete;
		unique_ptr& operator=(const unique_ptr<T, Del>&) = delete;
		unique_ptr(unique_ptr<T,Del>&& other)
		{
			_ptr = other._ptr;
			deleter = other.deleter;
			other._ptr = nullptr;
		}
		unique_ptr& operator=(unique_ptr<T, Del>&& other)
		{
			if (this != &other)
			{
				reset();
				_ptr = other._ptr;
				deleter = other.deleter;
				other._ptr = nullptr;
			}
				return *this;
		}
		T& operator*()
		{
			return *_ptr;
		}
		const T& operator*() const
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		const T* operator->() const
		{
			return _ptr;
		}
		void reset()
		{
			deleter(_ptr);
			_ptr = nullptr;
		}
		T* get()
		{
			return _ptr;
		}
		~unique_ptr()
		{
			deleter(_ptr);
		}
	private:
		T* _ptr;
		Del deleter;
	};
	template<typename T>
	class shared_ptr
	{
	public:
		template<typename U>
		friend class weak_ptr;
		shared_ptr()
			:_ptr(nullptr)
			, count(nullptr)
		{

		}
		shared_ptr(T* ptr, std::function<void(T*)>_deleter=del<T>())
			: _ptr(ptr)
			, count(new int(1))
			, deleter(_deleter)
		{

		}
		shared_ptr(const shared_ptr<T>& other)
		{
			_ptr = other._ptr;
			count = other.count;
			deleter = other.deleter;
			if (other.count != nullptr)
			{
				(*count)++;
			}
		}
		shared_ptr(shared_ptr<T>&& other)
		{
			_ptr = other._ptr;
			count = other.count;
			deleter = other.deleter;
			other._ptr = nullptr;
			other.count = nullptr;
		}
		shared_ptr& operator=(const shared_ptr<T>& 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<T>&& 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*() const
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		const T* operator->() const
		{
			return _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<void(T*)> deleter ;
	};
	template<typename T>
	class weak_ptr
	{
	public:
		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<T>& 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<T>& other)
		{
			_ptr = other._ptr;
			count = other.count;
			deleter = other.deleter;
			return *this;
		}
		weak_ptr& operator=(weak_ptr<T>&& 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<T> lock()
		{
			if (count) {
				shared_ptr<T> temp;
				temp._ptr = _ptr;
				temp.count = count;
				temp.deleter = deleter;
				(*temp.count)++;
				return temp;
			}
				return shared_ptr<T>();
		}
		~weak_ptr()
		{
			_ptr = nullptr;
			count = nullptr;
		}
	private:
		T* _ptr;
		int* count;
		std::function<void(T*)> deleter;
	};
}

main.cpp:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include "my_memory.h" 
#include <iostream>
#include <cstdio>
#include <vector>

// 测试辅助类
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<TestResource> empty;
    if (empty.get() == nullptr) std::cout << "Empty unique_ptr created\n";

    // 测试基本功能
    {
        wz::unique_ptr<TestResource> p1(new TestResource(1));
        p1->print();
        (*p1).print();
    } // 应自动销毁

    // 测试移动语义
    {
        wz::unique_ptr<TestResource> p2(new TestResource(2));
        wz::unique_ptr<TestResource> p3 = std::move(p2);
        if (p2.get() == nullptr) std::cout << "Resource moved to p3\n";
        p3->print();
    }

    // 测试自定义删除器
    {
        wz::unique_ptr<TestResource, CustomDeleter> p4(new TestResource(4));
        p4->print();
    }

    // 测试reset功能
    {
        wz::unique_ptr<TestResource> p5(new TestResource(5));
        p5.reset();
        if (p5.get() == nullptr) std::cout << "Resource reset successfully\n";
    }

    // 测试文件资源管理
    {
        wz::unique_ptr<FILE, FileDeleter> 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<TestResource> p1(new TestResource(10));
        p1->print();
        std::cout << "Instance count: " << TestResource::instanceCount << "\n";
    }

    // 测试拷贝构造
    {
        wz::shared_ptr<TestResource> p2(new TestResource(20));
        std::cout << "Instance count after p2 creation: " << TestResource::instanceCount << "\n";

        {
            wz::shared_ptr<TestResource> 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<TestResource> p4(new TestResource(40));
        wz::shared_ptr<TestResource> p5;
        p5 = p4;
        p5->print();
        std::cout << "Instance count after assignment: " << TestResource::instanceCount << "\n";
    }

    // 测试移动语义
    {
        wz::shared_ptr<TestResource> p6(new TestResource(60));
        wz::shared_ptr<TestResource> 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<TestResource> p8(new TestResource(80), [](TestResource* p) {
            std::cout << "Lambda deleter for resource " << p->id << "\n";
            delete p;
            });
        p8->print();
    }

    // 测试循环引用解决方案
    struct Node {
        wz::shared_ptr<Node> next;
        wz::weak_ptr<Node> prev;
        int id;

        Node(int id) : id(id) {
            std::cout << "Node " << id << " created\n";
        }

        ~Node() {
            std::cout << "Node " << id << " destroyed\n";
        }
    };

    {
        wz::shared_ptr<Node> node1(new Node(1));
        wz::shared_ptr<Node> 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<TestResource> weak;
    {
        wz::shared_ptr<TestResource> 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";
    }

    // 测试reset
    {
        wz::shared_ptr<TestResource> shared(new TestResource(200));
        wz::weak_ptr<TestResource> 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<TestResource> shared(new TestResource(300));
        wz::weak_ptr<TestResource> weak3(shared);
        wz::weak_ptr<TestResource> 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;
}

运行截图:

结语

那么这就是本期关于智能指针的全部内容啦,那么下一期我会更新c++类型的转化以及特殊类的设计,那么我会持续更新,希望你能够多多关注,如果本文有帮组到你,还请三连加关注,你的支持就是我创作最大的动力!

相关推荐
凤年徐2 小时前
【C++】string的模拟实现
c语言·开发语言·c++
牟同學2 小时前
从赌场到AI:期望值如何用C++改变世界?
c++·人工智能·概率论
QMCY_jason2 小时前
ubuntu 24.04 FFmpeg编译 带Nvidia 加速记录
linux·ubuntu·ffmpeg
敲代码的嘎仔2 小时前
JavaWeb零基础学习Day2——JS & Vue
java·开发语言·前端·javascript·数据结构·学习·算法
吃鱼吃鱼吃不动了2 小时前
什么是负载均衡?
开发语言·php
matlab的学徒3 小时前
Kubernetes(K8S)全面解析:核心概念、架构与实践指南
linux·容器·架构·kubernetes
夜晚中的人海3 小时前
【C++】智能指针介绍
android·java·c++
yacolex3 小时前
3.3_数据结构和算法复习-栈
数据结构·算法
Fcy6483 小时前
初识Linux和Linux基础指令详细解析及shell的运行原理
linux·服务器·ubuntu·centos