智能指针大揭秘:从 auto_ptr 到 unique_ptr & shared_ptr 的进化之路

智能指针大揭秘:从 auto_ptr 到 unique_ptr  & shared_ptr 的进化之路

1、引言

C++ 编程中,内存管理历来是复杂且容易出错的部分。手动管理动态分配的内存不仅会导致内存泄漏,还会引发悬空指针和双重释放等问题。如何有效地管理动态内存,避免内存泄漏和未定义行为,往往是困扰初学者和资深开发者的难题。为了解决这些问题,C++ 逐步引入了智能指针。

智能指针的核心思想是利用 RAII(Resource Acquisition Is Initialization)模式来管理资源的生命周期,确保动态分配的对象在不再使用时自动释放。使开发者能更加专注于业务逻辑,而非担心内存的分配与释放。智能指针包括了 std::unique_ptrstd::shared_ptrstd::weak_ptr 三种主要类型,它们分别解决了不同场景下的内存管理问题。接下来我们将逐步深入,探讨智能指针的演变历程、实现细节以及它们的应用场景。

  

2、智能指针的演变历程

2.1、手动内存管理时代

在早期的 CC++ 编程中,动态内存的分配和释放依赖开发者手动管理,通常通过 newdelete 来进行操作。例如:

int* ptr = new int(10);
// ...
delete ptr;

尽管这种方式看似简单,但它对开发者提出了严格的要求,即确保每次分配的内存都被正确释放。如果程序执行路径出现异常或者疏忽,未能释放的内存将造成内存泄漏。此外,重复释放同一个指针也会导致程序崩溃,增加了调试的复杂性。

  

2.2、智能指针的基本概念

智能指针的概念源自于传统指针的不足。普通指针指向堆上的动态内存,但需要手动管理对象的释放。如果开发者忘记释放内存或者多次释放内存,便会导致内存泄漏和未定义行为。而智能指针则将内存管理交给 C++ 的自动化机制来处理。

RAII (Resource Acquisition Is Initialization) 是一种利用对象生命周期来控制程序资源,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这样做法有两大好处:

  1. 不需要显示的释放资源
  2. 采用这种方式,对象所需的资源在其生命周期内始终保持有效
namespace Lenyiin
{
    template <class T>
    class Smart_ptr
    {
    public:
        Smart_ptr(T *ptr)
            : _ptr(ptr)
        {
            std::cout << "create: " << _ptr << std::endl;
        }

        ~Smart_ptr()
        {
            if (_ptr)
            {
                std::cout << "delete: " << _ptr << std::endl;
                delete _ptr;
            }
        }

        T& operator*()
        {
            return *_ptr;
        }

        T* operator->()
        {
            return _ptr;
        }

    private:
        T* _ptr;
    };
}

通过使用类的特性,可以让指针在生命周期内和原生指针一样,在生命周期外通过析构函数自动释放。

int main()
{
    // 智能指针 意义
    // 无论是在函数正常结束, 还是抛异常, 都会导致 sp 对象的生命周期到了以后, 调用析构函数
    std::cout << "---- 智能指针之前 ----" << std::endl;
    {
        Smart_ptr<int> sp1(new int);
        *sp1 = 10;

        Smart_ptr<std::pair<int, int>> sp2(new std::pair<int, int>);
        sp2->first = 20;
        sp2->second = 30;

    }
    std::cout << "---- 智能指针之后 ----" << std::endl;

    return 0;
}

运行结果:

但是单纯的利用对象生命周期来控制程序资源,会产生严重的问题。

int main()
{
    Smart_ptr<int> sp1(new int);
    Smart_ptr<int> sp2 = sp1; // error 浅拷贝, 重复析构, 析构两次, 导致同一块内存被释放两次

    return 0;
}

可能有人会有疑问,为什么智能指针不像 string、vector 等容器一样进行深拷贝?这是因为:智能指针只是托管资源空间,可以访问空间,模拟的是原生指针的行为,只是比原生指针多的是,可以在生命周期结束时自动释放资源。

由这个问题,衍生出了三种解决的方法

  1. 管理权转移 auto_ptr
  2. 防拷贝 unique_ptr
  3. 引用计数 shared_ptr

  

2.3、C++98 和 auto_ptr 的引入

为了解决内存管理问题,C++98 引入了 auto_ptr,这是 C++ 最早期的智能指针,旨在简化动态内存的管理。

auto_ptr 的语法与其他智能指针类似,它封装了一个普通的指针,并在 auto_ptr 对象的生命周期结束时自动调用 delete,释放动态分配的内存。然而,auto_ptr 存在一些严重的缺陷,例如它使用复制语义来转移所有权,这在共享资源的场景下极不安全。

std::auto_ptr<int> ptr1(new int(10));
std::auto_ptr<int> ptr2 = ptr1;  // ptr1 不再拥有资源

在上述代码中,ptr1 的所有权被转移到 ptr2,导致 ptr1 失去对对象的控制。这种设计使得 auto_ptr 在实际开发中较为不便,因此在 C++11 中被弃用。

  

2.4、C++11 引入 unique_ptr、shared_ptr、weak_ptr

随着 C++11 的到来,新的智能指针 unique_ptrshared_ptrweak_ptr 被引入,取代了 auto_ptr。它们通过现代 C++ 的移动语义、引用计数等机制,解决了 auto_ptr 的缺陷,并提供了更加安全、灵活的内存管理方式。

unique_ptr 提供了独占所有权,避免了不必要的资源共享问题;shared_ptr 支持多对象共享同一资源,并通过引用计数控制资源释放;而 weak_ptr 则帮助开发者避免循环引用的问题。

  

3、auto_ptr :所有权转移智能指针

auto_ptr 是 C++98 标准中引入的智能指针,旨在通过 RAII 自动管理动态分配的内存。尽管它为 C++ 提供了自动化的内存管理机制,但由于其设计中的一些缺陷,后来被 unique_ptr 所取代,并最终在 C++11 中被标记为废弃(deprecated),在 C++17 中被完全移除。

虽然 auto_ptr 已经被遗弃了,为了帮助理解 auto_ptr 的历史、设计、缺陷,以及为什么最终被废弃,本章我们将对其进行详细的讨论。

3.1、auto_ptr 的引入

auto_ptrC++98 标准库中的一种智能指针,设计的目的是帮助程序员自动释放动态分配的内存,避免常见的内存泄漏问题。在早期的 C++ 中,手动管理内存(通过 newdelete)常常会引发严重的内存管理问题,特别是在异常处理和多分支逻辑中,程序员容易忘记释放内存。

3.1.1、auto_ptr 的使用

auto_ptr 的语法与其他智能指针类似,它封装了一个普通的指针,并在 auto_ptr 对象的生命周期结束时自动调用 delete,释放动态分配的内存。

简单的 auto_ptr 使用示例:

#include <memory>
#include <iostream>

int main() {
    std::auto_ptr<int> ptr(new int(10));  // 分配内存并初始化
    std::cout << *ptr << std::endl;       // 输出: 10
    // 离开作用域时,ptr 自动释放所指向的内存
    return 0;
}

在上面的例子中,auto_ptr 封装了一个动态分配的 int,当 ptr 离开作用域时,它会自动释放内存,不需要手动调用 delete

3.1.2 auto_ptr 的工作原理

auto_ptr 的基本原理是通过 RAII(Resource Acquisition Is Initialization)来管理资源。它在构造时接管动态分配的内存,并在析构时自动调用 delete。这样可以确保即使发生异常,内存也能被正确释放,减少内存泄漏的风险。

  

3.2、auto_ptr 的缺陷

尽管 auto_ptr 的引入为自动化内存管理提供了一种解决方案,但它存在严重的缺陷,尤其是在所有权语义和复制行为上。auto_ptr 的最主要问题是它的 所有权转移语义复制行为,这导致了许多意外的错误和内存问题。

3.2.1、所有权转移(Move Semantics)

auto_ptr 的核心问题在于它的复制语义。当一个 auto_ptr 对象被复制时,所有权会被转移到新的 auto_ptr,而原来的 auto_ptr 将不再持有该指针。这种所有权的隐式转移在实际使用中引发了很多问题。

示例代码:

std::auto_ptr<int> ptr1(new int(10));
std::auto_ptr<int> ptr2 = ptr1;  // 所有权从 ptr1 转移到 ptr2

std::cout << (ptr1.get() == nullptr) << std::endl;  // 输出: 1 (ptr1 为空)
std::cout << *ptr2 << std::endl;                    // 输出: 10

在上面的代码中,ptr2 复制了 ptr1,但实际上所有权发生了转移,导致 ptr1 不再拥有该资源。虽然这在某些场景下是有用的(比如在传递所有权时),但它违反了传统 C++ 中的复制语义。通常情况下,复制一个对象意味着两个对象共享相同的值或资源,但 auto_ptr 的行为不同,这导致了难以预料的错误。

3.2.2、不适用于标准容器

由于 auto_ptr 的复制行为,它无法存储在 C++ 的标准容器中(如 std::vectorstd::list)。标准容器依赖于复制语义来管理其元素,而 auto_ptr 的所有权转移特性与此不兼容。

示例代码:

#include <memory>
#include <vector>

int main() {
    std::vector<std::auto_ptr<int>> vec;
    vec.push_back(std::auto_ptr<int>(new int(10)));  // 错误!所有权转移破坏了容器的语义
}

在这个例子中,auto_ptr 无法在标准容器中使用,因为在 push_back 操作时会发生所有权转移,破坏了容器的元素管理机制。这也是 auto_ptr 最终被废弃的原因之一。

  

3.3、模拟实现 auto_ptr

namespace Lenyiin
{
    // C++98 auto_ptr
    // 1. 管理权转移,早起设计缺陷,一般公司都明令禁止使用它
    // 缺陷:ap2 = ap1 场景下 ap1 就悬空了,访问就会报错,如果不熟悉它的特性就会被坑
    template <class T>
    class Auto_ptr
    {
    public:
        // 默认构造
        Auto_ptr(T* ptr = nullptr)
            : _ptr(ptr)
        {
        }

        // 拷贝构造
        Auto_ptr(Auto_ptr<T>& ap)
            : _ptr(ap._ptr)
        {
            std::cout << "拷贝构造, 管理权转移" << std::endl;
            ap._ptr = nullptr;
        }

        // ap1 = ap2
        Auto_ptr<T>& operator=(const Auto_ptr<T>& ap)
        {
            if (this != &ap)
            {
                if (_ptr)
                {
                    std::cout << "赋值拷贝, delete: " << _ptr << std::endl;
                    delete _ptr;
                }

                _ptr = ap._ptr;
                ap._ptr = nullptr;
            }

            return *this;
        }

        // 析构函数
        ~Auto_ptr()
        {
            if (_ptr)
            {
                std::cout << "析构 delete: " << _ptr << std::endl;
                delete _ptr;
            }
        }

        T& operator*()
        {
            return *_ptr;
        }

        T* operator->()
        {
            return _ptr;
        }

    private:
        T* _ptr;
    };
}

  

4、unique_ptr:独占所有权智能指针

由于 auto_ptr 的设计缺陷,C++11 标准引入了 unique_ptr,并逐渐取代了 auto_ptrunique_ptr 解决了 auto_ptr 的所有权转移问题,并通过 移动语义 提供了更加安全和高效的内存管理方式。

std::unique_ptr具有独占所有权的特性。它意味着在任何时刻,只有一个 unique_ptr 能够管理某个资源,这确保了资源不会被多个对象意外共享。

4.1、unique_ptr 的优势

  • 防止内存泄漏unique_ptr 能在对象超出作用域时自动释放内存。
  • 严格的独占所有权unique_ptr 仅允许一个智能指针持有资源的所有权,任何复制行为都被禁用。只有通过 移动 才能转移所有权,这样的设计避免了 auto_ptr 的隐式所有权转移问题。
  • 支持标准容器:由于 unique_ptr 禁用了复制,标准容器可以安全地存储 unique_ptr,因为标准容器依赖于移动语义,而不是复制语义。
  • 性能优化:与 auto_ptr 不同,unique_ptr 支持更高效的内存管理,特别是在多线程环境中。
  • 线程安全:由于 unique_ptr 没有共享资源的概念,因此天然是线程安全的。

4.2、unique_ptr 的基本特性

unique_ptr 实现了独占所有权语义,任何时候只能有一个 unique_ptr 实例管理某个资源。它支持 移动语义,允许通过 移动构造移动赋值 将资源的所有权从一个 unique_ptr 转移到另一个 unique_ptr

4.2.1、unique_ptr 的构造与析构

unique_ptr 的构造函数允许传递一个原始指针来初始化它。当 unique_ptr 对象超出作用域时,它的析构函数会自动释放持有的资源(通过 deletedelete[] 调用)。

示例代码:

#include <iostream>
#include <memory>

int main() 
{
    {
        std::unique_ptr<int> p1(new int(10));  // p1 独占一个 int 资源
        std::cout << *p1 << std::endl;         // 输出 10
    }
    // p1 超出作用域,资源被自动释放
    return 0;
}

在这个示例中,unique_ptr 在超出作用域时自动调用其析构函数,释放动态分配的内存。

4.2.2、禁用复制语义

unique_ptr 禁用了复制构造函数和复制赋值运算符,因此无法通过复制操作共享所有权。这一设计确保了资源的独占所有权,避免了内存错误。

示例代码:

std::unique_ptr<int> p1(new int(42));
// std::unique_ptr<int> p2 = p1;  // 错误:不能复制 unique_ptr

尝试复制 unique_ptr 会导致编译错误,避免了隐式的所有权转移问题

4.2.3、移动语义支持

尽管 unique_ptr 禁用了复制操作,但它支持 移动语义,允许通过移动构造或移动赋值将所有权从一个 unique_ptr 转移到另一个。

示例代码:

#include <iostream>
#include <memory>

int main() 
{
    std::unique_ptr<int> p1(new int(10));
    std::unique_ptr<int> p2 = std::move(p1);  // 所有权从 p1 转移到 p2

    if (p1 == nullptr) 
    {
        std::cout << "p1 为空" << std::endl;  // p1 不再拥有资源
    }
    std::cout << *p2 << std::endl;  // 输出 10
    return 0;
}

在这个例子中,std::move(p1)p1 中的资源所有权转移给 p2,之后 p1 变为空指针,且不再拥有该资源。

#include <memory>
#include <vector>

int main() 
{
    {
        std::vector<std::unique_ptr<int>> vec;
        vec.push_back(std::make_unique<int>(10));  // 安全!使用移动语义
    }
    // 超出作用域,资源被自动释放
}

在这个例子中,unique_ptr 可以安全地存储在 std::vector 中,并且通过移动语义来传递所有权,不会导致 auto_ptr 的那种隐式所有权转移问题。

4.2.4、自定义删除器

在某些特殊场景下,资源的释放方式可能需要自定义。unique_ptr 允许通过自定义删除器来管理资源的释放。自定义删除器可以是函数指针或函数对象。

struct Deleter {
    void operator()(int* p) const 
    {
        std::cout << "Deleting int pointer: " << *p << std::endl;
        delete p;
    }
};

int main() {
    std::unique_ptr<int, Deleter> ptr(new int(100));  // 使用自定义删除器
}

自定义删除器的作用是在 unique_ptr 释放资源时调用特定的逻辑,适用于需要特殊清理操作的资源管理场景。

4.2.5、数组形式的 unique_ptr

除了管理单一对象,unique_ptr 还可以用于管理动态分配的数组。在这种情况下,需要在声明 unique_ptr 时使用数组版本的 unique_ptr,并且删除器会自动调用 delete[]

std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);  // 动态分配一个数组
arr[0] = 1;
arr[1] = 2;
std::cout << arr[0] << ", " << arr[1] << std::endl;

在数组形式的 unique_ptr 中,访问数组元素的方式与常规指针相同,但内存管理是自动化的。

4.2.6、空指针安全

与原始指针不同,unique_ptr 是空指针安全的。即使 unique_ptr 不指向任何对象,它仍然是安全的,不会导致崩溃或未定义行为。

std::unique_ptr<int> ptr;  // 空指针
if (!ptr) {
    std::cout << "Pointer is null." << std::endl;
}

这使得 unique_ptr 的使用更加健壮,减少了空指针引发的程序崩溃的风险。

  

4.3、unique_ptr 的使用场景

由于其独占所有权和自动管理资源的特性,unique_ptr 适用于不需要共享资源的场景。例如,某个类内部的资源管理可以通过 unique_ptr 来实现,以确保对象的生命周期结束时,资源被正确释放。unique_ptr 适用于以下场景:

4.3.1、动态内存管理

当需要管理动态分配的内存并确保其自动释放时,unique_ptr 是非常合适的选择。例如在函数内部分配的临时对象,需要在函数结束时释放,可以使用 unique_ptr 来确保正确管理内存。

4.3.2、管理资源句柄

除了管理动态内存外,unique_ptr 也可以用于管理其他需要手动释放的资源,如文件句柄、网络连接、锁等。通过自定义删除器(deleter),可以灵活地释放不同类型的资源。

示例代码:

#include <iostream>
#include <memory>
#include <cstdio>

struct FileDeleter {
    void operator()(FILE* file) const 
    {
        if (file) 
        {
            std::fclose(file);
            std::cout << "文件已关闭" << std::endl;
        }
    }
};

int main() {
    std::unique_ptr<FILE, FileDeleter> file(std::fopen("example.txt", "r"));
    // 当 file 超出作用域时,FileDeleter 会自动关闭文件
    return 0;
}

在这个例子中,自定义的 FileDeleter 确保在 unique_ptr 超出作用域时,自动调用 fclose 关闭文件。

4.3.3、用于标准容器中

由于 unique_ptr 支持移动语义,因此它可以安全地存储在标准容器(如 std::vector)中。相比于 auto_ptrunique_ptr 不会在存储过程中发生所有权隐式转移,确保了容器的正确性。

示例代码:

#include <iostream>
#include <memory>
#include <vector>

int main() {
    std::vector<std::unique_ptr<int>> vec;
    vec.push_back(std::make_unique<int>(10));  // 安全地存储 unique_ptr

    for (const auto& elem : vec) {
        std::cout << *elem << std::endl;  // 输出 10
    }

    return 0;
}

通过 std::make_unique 创建 unique_ptr,并将其存储在容器中,既能安全管理内存,又能避免复杂的所有权问题。

4.3.4、内存池与 unique_ptr 的结合

在高性能需求的场景中,内存池技术可以与 unique_ptr 结合使用,以减少频繁的内存分配与释放带来的开销。通过自定义分配器,我们可以将 unique_ptr 的内存管理交给内存池,从而提高性能。

4.3.5、函数返回值

当一个函数返回动态分配的对象时,使用 unique_ptr 可以避免手动管理资源,并明确表示返回值的所有权转移。

std::unique_ptr<int> createInt(int value) 
{
    return std::make_unique<int>(value);  // 返回 unique_ptr,自动管理内存
}

这种方式比返回原始指针要更加安全,因为调用者不需要显式管理资源释放。

  

4.4、unique_ptr 的基本实现

为了更好地理解 unique_ptr 的工作原理,我们可以分析其内部实现。unique_ptr 依赖于模板特性,并使用了移动语义来确保其独占所有权。

unique_ptr 是一个模板类,它封装了一个指针,并在析构时自动调用删除器来释放该指针所指向的资源。下面是一个简化的 unique_ptr 实现:

namespace Lenyiin
{
    template <class T, class Deleter = std::default_delete<T>>
    class Unique_ptr {
    private:
        T* ptr; // 原始指针
        Deleter deleter;  // 自定义删除器

    public:
        // 构造函数
        explicit Unique_ptr(T* p = nullptr)
            : ptr(p)
        {}

        // 禁用拷贝构造函数和赋值操作符
        Unique_ptr(const Unique_ptr&) = delete;
        Unique_ptr& operator=(const Unique_ptr&) = delete;

        // 支持移动构造函数和移动赋值
        Unique_ptr(Unique_ptr&& other) noexcept
            : ptr(other.ptr)
        {
            other.ptr = nullptr;
        }

        // 移动赋值运算符
        Unique_ptr& operator=(Unique_ptr&& other) noexcept
        {
            if (this != &other) {
                reset();
                ptr = other.ptr;
                other.ptr = nullptr;
            }
            return *this;
        }

        // 析构函数
        ~Unique_ptr()
        {
            deleter(ptr);  // 调用删除器释放资源
        }

        // 重载 * 和 -> 操作符
        T& operator*() const
        {
            return *ptr;
        }

        T* operator->() const
        {
            return ptr;
        }

        // 获取底层指针
        T* get() const
        {
            return ptr;
        }

        // 释放所有权
        T* release()
        {
            T* oldPtr = ptr;
            ptr = nullptr;
            return oldPtr;
        }

        // 重置指针
        void reset(T* p = nullptr)
        {
            delete ptr;
            ptr = p;
        }
    };
}

unique_ptr 的核心是原始指针 ptr 和删除器 deleter。它禁用了复制语义,通过移动语义来传递所有权。reset() 方法允许用户手动重置指针并释放旧资源,而 release() 方法则可以释放所有权,并让用户手动管理指针的生命周期。

unique_ptr 的移动语义通过移动构造函数和移动赋值操作来实现。当资源的所有权被转移时,原来的智能指针会变成空指针,而新的智能指针则接管资源。移动构造函数和移动赋值操作都会将原智能指针中的资源指针设为 nullptr,确保原智能指针不再持有资源。这样可以避免重复释放资源的风险。

  

5、shared_ptr:共享所有权智能指针

std::shared_ptrC++11 引入的另一种智能指针,允许多个 shared_ptr 对象共享同一个资源。其内部通过引用计数来管理资源的生命周期,当引用计数归零时,资源将被自动释放。简化了动态内存的管理并显著降低了内存泄漏的风险。

本章将深入探讨 shared_ptr 的使用场景、实现原理、性能特点,以及常见的陷阱和最佳实践。

  

5.1、什么是 shared_ptr

shared_ptrC++ 标准库中的一种智能指针,允许多个指针对象共同拥有动态分配的对象。它通过维护一个引用计数器来跟踪有多少个 shared_ptr 指向同一个对象。当最后一个 shared_ptr 离开作用域时,引用计数归零,自动删除指向的对象。这种机制确保了对象在不再被使用时得到正确销毁,避免了内存泄漏。

shared_ptr 的核心特性

  • 引用计数shared_ptr 使用引用计数器来记录有多少个指针拥有同一个对象。当引用计数变为零时,shared_ptr 会自动销毁该对象。
  • 共享所有权:与 unique_ptr 的独占所有权不同,shared_ptr 允许多个指针共享对同一对象的所有权。
  • 线程安全shared_ptr 的引用计数是线程安全的,确保在多线程环境下可以安全地共享同一对象。

  

5.2、shared_ptr 的创建与使用

要创建一个 shared_ptr,我们可以直接使用构造函数,或者更推荐使用 std::make_shared 函数。这种方式不仅更加简洁,还能提升性能,因为它减少了内存分配次数。

5.2.1、创建 shared_ptr

#include <memory>
#include <iostream>

int main() 
{
    // 使用 make_shared 创建
    std::shared_ptr<int> sp1 = std::make_shared<int>(10);
    std::cout << "Shared pointer value: " << *sp1 << std::endl;
    return 0;
}

在上面的例子中,std::make_shared 函数为我们创建了一个管理动态内存的 shared_ptr。它的内部机制可以更高效地分配内存,因为它将对象本身和引用计数器一起分配,而不是分开分配。

5.2.2、引用计数的示例

#include <memory>
#include <iostream>

int main() 
{
    std::shared_ptr<int> sp1 = std::make_shared<int>(10);  // 引用计数为 1
    {
        std::shared_ptr<int> sp2 = sp1;  // 引用计数为 2
        std::cout << "Reference count inside block: " << sp1.use_count() << std::endl;
    }  // 离开作用域,sp2 销毁,引用计数变为 1

    std::cout << "Reference count outside block: " << sp1.use_count() << std::endl;
    return 0;
}

在这个例子中,当 sp2 复制了 sp1 后,引用计数从 1 变为 2。当 sp2 离开作用域时,引用计数会自动减少回 1。shared_ptr 通过 use_count() 函数可以查询当前的引用计数。

  

5.3、shared_ptr 的实现原理

在本节中,我们将深入探讨如何手动实现一个简化版的 shared_ptr。通过这个实现,我们可以更好地理解 shared_ptr 的内部机制和运作原理。请注意,下面的实现只是一个简化版本,真实的标准库实现可能包含更多的细节和优化。

5.3.1、shared_ptr 的内部结构

一个典型的控制块可能包含以下成员:

  • 对象指针:指向动态分配的对象。
  • 引用计数器:跟踪 shared_ptr 的数量。
  • 互斥锁:多线程高并发时保证线程安全

控制块的设计使得 shared_ptr 可以灵活地管理资源,不论是简单的对象还是自定义的资源(如文件句柄、网络连接)。

5.3.2、 shared_ptr 的生命周期

shared_ptr 的生命周期由引用计数决定。当引用计数器归零时,shared_ptr 会自动调用对象的析构函数并释放所管理的内存资源。以下是 shared_ptr 生命周期的几个重要阶段:

  1. 创建:当 shared_ptr 创建时,控制块中的引用计数初始化为 1。
  2. 复制:当 shared_ptr 被复制时,引用计数增加。
  3. 销毁:当 shared_ptr 被销毁时,引用计数减少。当引用计数为 0 时,控制块会调用对象的析构函数。

5.3.3、shared_ptr 的实现

shared_ptr 的实现包括构造函数、析构函数、拷贝构造函数、赋值操作符等。下面是简化版的 shared_ptr 实现:

namespace Lenyiin
{
    // C++11 Shared_Ptr
    // 引用计数,可以拷贝
    // 缺陷:循环引用
    template <class T>
    class Shared_ptr
    {
    public:
        // 普通构造
        Shared_ptr(T *ptr = nullptr)
            : _ptr(ptr), _pcount(new int(1)), _pmtx(new std::mutex)
        {}

        // 拷贝构造
        Shared_ptr(const Shared_ptr<T>& sp)
            : _ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx)
        {
            add_ref_count();
        }

        // 赋值操作
        // sp1 = sp2
        Shared_ptr<T>& operator=(const Shared_ptr<T>& sp)
        {
            if (this != &sp)
            {
                // 减减引用计数,如果我是最后一个管理资源的对象,则释放资源
                release();

                // 我开始和你一起管理资源
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                _pmtx = sp._pmtx;

                add_ref_count();
            }
            return *this;
        }

        void add_ref_count()
        {
            _pmtx->lock();
            ++(*_pcount);
            _pmtx->unlock();
        }

        void release()
        {
            bool flag = false;

            _pmtx->lock();
            if (--(*_pcount) == 0)
            {
                if (_ptr)
                {
                    std::cout << "delete: " << _ptr << std::endl;
                    delete _ptr;
                    _ptr = nullptr;
                }

                delete _pcount;
                _pcount = nullptr;

                flag = true;
            }
            _pmtx->unlock();

            if (flag == true)
            {
                delete _pmtx;
                _pmtx = nullptr;
            }
        }

        // 析构函数
        ~Shared_ptr()
        {
            release();
        }

        T& operator*()
        {
            return *_ptr;
        }

        T* operator->()
        {
            return _ptr;
        }

        int use_count()
        {
            return *_pcount;
        }

        T* get_ptr() const
        {
            return _ptr;
        }

    private:
        T* _ptr;

        // 记录有多少个对象一起共享管理资源, 最后一个析构释放资源
        int* _pcount;
        // 
        std::mutex* _pmtx;
    };
}

以上代码实现了一个简化版的 shared_ptr,包括基本的引用计数管理、控制块和指针管理。这个实现演示了 shared_ptr 的核心概念和工作原理。实际的标准库实现可能会有更多的细节和优化,但本实现提供了一个理解 shared_ptr 内部机制的良好基础。

5.3.4、shared_ptr 的功能测试

为了验证我们的 shared_ptr 实现的正确性,我们可以编写一些测试代码来检查基本的功能,包括构造、拷贝、赋值和析构。

#include <cassert>

int main() {
    // 测试构造函数
    shared_ptr<int> sp1(new int(10));
    assert(*sp1 == 10);
    // 测试指针计数
    std::cout << "sp1: " << sp1.use_count() << std::endl;

    // 测试拷贝构造函数
    shared_ptr<int> sp2(sp1);
    assert(*sp2 == 10);
    assert(*sp1 == 10);
    std::cout << "sp2: " << sp2.use_count() << std::endl;

    // 测试赋值操作符
    shared_ptr<int> sp3;
    sp3 = sp1;
    assert(*sp3 == 10);
    assert(*sp1 == 10);
    std::cout << "sp3: " << sp3.use_count() << std::endl;

    return 0;
}

  

5.4、shared_ptr 的最佳实践

为了充分发挥 shared_ptr 的优势,同时避免其潜在的陷阱,以下是一些最佳实践:

5.4.1、复杂对象的生命周期管理

shared_ptr 的典型应用场景是在复杂的系统中,多个对象需要共享同一个资源时。例如,在 GUI 应用程序中,多个控件可能共享同一个数据模型。当某个控件被销毁时,数据模型应该继续存在,直到最后一个控件不再需要它。这种场景中,shared_ptr 可以确保数据模型的生命周期正确管理。

5.4.2、与 weak_ptr 结合使用

在某些场景下,可能需要打破对象之间的循环引用。例如,两个对象 AB 相互拥有对方的指针,如果都使用 shared_ptr,将导致引用计数永远不会归零,资源得不到释放。为了解决这种问题,C++ 提供了 weak_ptr(弱指针)来管理弱引用。weak_ptr 不会增加引用计数,可以用来打破循环依赖。

#include <memory>
#include <iostream>

struct B;  // 前向声明

struct A {
    std::shared_ptr<B> bptr;
    ~A() { std::cout << "A destroyed" << std::endl; }
};

struct B {
    std::weak_ptr<A> aptr;  // 使用 weak_ptr 打破循环依赖
    ~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->bptr = b;
    b->aptr = a;  // 若使用 shared_ptr,会造成循环依赖

    return 0;
}

在这个例子中,AB 相互引用。通过将 B 的指针声明为 weak_ptr,打破了循环依赖,从而允许正确释放对象。

5.4.3、使用 std::make_shared 创建 shared_ptr

std::make_shared 是创建 shared_ptr 的推荐方式。它不仅能减少内存分配次数,还能提高性能和安全性。避免直接使用 new 创建 shared_ptr

std::shared_ptr<int> sp = std::make_shared<int>(42);  // 推荐方式

5.4.4、避免不必要的复制

尽量避免在函数参数和返回值中使用 shared_ptr 的复制操作。如果需要传递或返回 shared_ptr,考虑使用 const std::shared_ptr<T>&std::shared_ptr<T>&&(右值引用)。

void process(const std::shared_ptr<int>& sp) 
{
    // 只读访问,避免不必要的复制
}

5.4.5、理解 shared_ptr 的生命周期

要清楚 shared_ptr 的生命周期规则。在 shared_ptr 的生命周期内,不应访问已被销毁的对象。对 shared_ptr 的所有权和生命周期有清晰的理解,可以避免常见的错误和资源泄漏。

5.4.6、weak_ptr 的锁定机制

weak_ptr 的一个重要特性是它不会直接访问资源,而是需要通过 lock() 方法将自身转化为 shared_ptr,以安全地访问资源。这样可以确保资源在使用时仍然有效,而当资源已经被释放时,lock() 返回的 shared_ptr 将为空。

std::shared_ptr<int> sptr = std::make_shared<int>(42);
std::weak_ptr<int> wptr = sptr;

if (std::shared_ptr<int> locked = wptr.lock()) {
    std::cout << "Resource is still alive: " << *locked << std::endl;
} else {
    std::cout << "Resource has been released" << std::endl;
}

上面的代码通过 lock() 方法来获取资源,并且检查资源是否已经被释放。如果资源已经被释放,locked 将为 nullptr,从而避免了访问已释放内存的风险。

  

5、智能指针总结

在 C++ 中,智能指针提供了一个管理动态内存的安全机制,避免了内存泄漏和悬挂指针问题。C++ 标准库中提供了多种智能指针,其中最常用的包括 auto_ptrunique_ptrshared_ptr。每种智能指针都有其独特的特性和适用场景。以下是对这三种智能指针的总结。

5.1、auto_ptr

auto_ptrC++98 标准引入的智能指针,用于自动管理动态分配的内存。它实现了所有权转移,但由于其不符合现代 C++ 的设计理念和安全要求,在 C++11 中被标记为废弃,并在 C++17 中被移除。

特点:

  • 所有权转移auto_ptr 支持通过拷贝构造和赋值操作符转移所有权。原智能指针在转移后变为 nullptr
  • 不安全:由于所有权转移的特性,auto_ptr 可能导致意外的资源管理错误和潜在的悬挂指针问题。
  • 废弃auto_ptr 已被 C++11 标准标记为废弃,建议使用 unique_ptrshared_ptr 替代。

5.2、unique_ptr

unique_ptrC++11 引入的智能指针,提供了独占所有权的管理机制。它保证了在任何时刻只有一个 unique_ptr 拥有对象的所有权,从而避免了资源泄漏。

特点:

  • 独占所有权unique_ptr 只允许一个 unique_ptr 拥有对象的所有权,不能被拷贝,只能通过 move 操作转移所有权。
  • 资源释放:当 unique_ptr 被销毁时,它会自动释放所管理的对象。
  • 高效unique_ptr 不需要额外的内存来存储引用计数,性能开销较小。

5.3、shared_ptr

shared_ptrC++11 引入的智能指针,允许多个 shared_ptr 实例共享同一个对象的所有权。它使用引用计数机制来管理对象的生命周期,确保在最后一个 shared_ptr 被销毁时才释放对象。

特点:

  • 共享所有权:多个 shared_ptr 可以共享同一个对象的所有权,通过引用计数来跟踪对象的生命周期。
  • 线程安全:引用计数的更新是线程安全的。
  • 性能开销:需要额外的内存来存储引用计数,性能开销较 unique_ptr 大。
  • 循环引用:需要小心处理循环引用问题,可以使用 weak_ptr 来打破循环引用。

5.4、总结

  1. auto_ptr
    • 优点:简单易用,早期 C++ 的智能指针实现。
    • 缺点:不安全,所有权转移可能导致悬挂指针。
    • 状态:已废弃,不建议使用。
  2. unique_ptr
    • 优点:独占所有权,资源管理简单高效,无需引用计数。
    • 缺点:不能被拷贝,只能通过 move 转移所有权。
    • 适用场景:需要独占所有权的场景,例如局部对象和工厂模式。
  3. shared_ptr
    • 优点:支持共享所有权,通过引用计数管理对象生命周期,线程安全。
    • 缺点:性能开销较大,可能存在循环引用问题。
    • 适用场景:需要多个对象共享同一个资源的场景,例如观察者模式和缓存系统。

智能指针是 C++ 内存管理中的重要工具,它们通过 RAII 模式和引用计数机制极大地简化了内存的分配和释放。在现代 C++ 中,unique_ptrshared_ptrweak_ptr 分别解决了独占所有权、共享所有权和循环引用等问题,成为了 C++ 编程中不可或缺的部分。

了解这些智能指针的特性和使用场景,可以帮助开发者选择合适的智能指针,提高代码的安全性和可维护性。在实际开发中,选择合适的智能指针来管理资源,可以有效地避免内存泄漏和资源管理错误。

通过本文的详细讲解和代码示例,我们不仅介绍了智能指针的基础知识,还探讨了其内部实现和自定义实现。希望读者在阅读本文后,能够对智能指针有一个更加全面和深入的理解,并能够在实际开发中灵活运用智能指针管理内存。

  

希望这篇博客对您有所帮助,也欢迎您在此基础上进行更多的探索和改进。如果您有任何问题或建议,欢迎在评论区留言,我们可以共同探讨和学习。

Comments

No comments yet. Why don’t you start the discussion?

发表回复