指针的艺术——四种智能指针

22 阅读6分钟

C++中的内存管理一直是开发者面临的一个重要挑战。传统指针需要手动管理内存的分配和释放,这容易导致内存泄漏和悬空指针等问题。

为了解决这些问题,C++ 引入了智能指针,使用智能指针可以自动管理内存的生命周期,简化了内存管理并提高了代码的安全性和可维护性。

什么是智能指针?

智能指针是一个对象,像指针一样工作,但同时负责管理所指向对象的生命周期。智能指针在其自身被销毁时,会自动释放所管理的内存,从而避免内存泄漏。

C++标准库提供了几种常用的智能指针类型:auto_ptr(C 11 中已弃用)、std:: unique_ptr、std::shared_ptr和std:: weak_ptr。

std::auto_ptr (C 11 中已弃用)

std::auto_ptr 是C++98标准中引入的一种智能指针,但在C++11中被弃用,并在C++17中被完全移除。

std::auto_ptr 的设计初衷是提供自动内存管理,但由于其所有权语义不够明确,容易导致意外的内存管理问题,因此被 std::unique_ptr 取代。

特点:std::auto_ptr 采用所有权模式。

  • 所有权转移:std::auto_ptr在复制或赋值时会转移所有权,这意味着源指针会变为空。这种行为可能导致意外的内存管理问题。
  • 不安全:由于所有权转移的特性,std::auto_ptr 不适合用于标准容器(如 std::vector),因为容器的复制和赋值操作会导致所有权的不确定性,可能会导致程序内存奔溃。
#include <memory>

void example() {
    std::auto_ptr<int> ptr1(new int(10));
    std::auto_ptr<int> ptr2 = ptr1; // 所有权转移,ptr1 变为空
    // 现在 ptr2 拥有对象
}

此时不会报错,ptr2 剥夺了 ptr1 的所有权,但是当程序运⾏时访问 ptr1 将会报错。所以 std::auto_ptr 的缺点是:存在不安全风险,内存奔溃。

替代方案是使用 std::unique_ptrstd::unique_ptr 提供了更明确的所有权语义,并且支持移动语义,避免了 std::auto_ptr 的缺陷。

std::unique_ptr

std::unique_ptr 是一个独占所有权的智能指针确保同一时间只有一个指针可以拥有某个对象。对于避免资源泄露来说是非常好用的。

特点:采⽤所有权模式

  • 保同一时间只有一个指针可以拥有某个对象。
  • 不可复制,但可以移动。适用于需要独占资源的场景。
#include <memory>

void example() {
    std::unique_ptr<int> ptr1(new int(10));
    std::unique_ptr<int> ptr2 = std::move(ptr1); // 转移所有权
    // ptr1 现在为空,ptr2 拥有对象
}

如果 std::unique_ptr<int> ptr2 = ptr1; 则会报错,避免了 ptr1 不再指向有效数据的问题。

std::shared_ptr(共享,强引用)

std::shared_ptr 是一个共享所有权的智能指针允许多个指针指向同一个对象。该对象和其相关资源会在“最后⼀个引⽤被销毁”时候释放。

我们从名字 share 就可以看出资源可以被多个指针共享,std::shared_ptr 使⽤计数机制来表明资源被⼏个指针共享。可以通过成员函数 use_count() 来查看资源的所有者个数。

在构造 std::shared_ptr 时,我们除了可以通过 new 来完成构造,还可以通过传⼊ auto_ptrunique_ptrweak_ptr 指针来完成构造。

当我们调⽤ release() 时,当前指针会释放资源所有权,计数减⼀。当计数等于 0 时,资源会被释放。

特点:共享型,强引⽤

  • 使用引用计数来管理对象的生命周期
  • 当最后一个 std::shared_ptr 被销毁时,所管理的对象才会被释放。
#include <memory>

void example() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
    std::shared_ptr<int> ptr2 = ptr1; // 共享所有权
    // 引用计数增加
}

 std::weak_ptr (弱引用)

std::weak_ptr 是一个不控制对象生命周期的智能指针,通常与 std::shared_ptr 一起使用,进⾏该对象的内存管理的就是那个强引⽤的 std::shared_ptr

std::weak_ptr 只提供了对管理对象的⼀个访问⼿段,其设计的⽬的是为配合 std::shared_ptr ⽽引⼊的⼀种智能指针,目的是来协助 std::shared_ptr ⼯作。

std::weak_ptr 只可以从⼀个 std::shared_ptr 或另⼀个 std::weak_ptr 对象构造,其构造和析构不会引起引⽤记数的增加或减少。

std::weak_ptr 是⽤来解决 std::shared_ptr 相互引⽤时产生的死锁问题。

如果说两个 std::shared_ptr 相互引⽤,那么这两个指针的引⽤计数永远不可能下降为 0,也就是资源永远不会释放。

std::weak_ptr 是对一个对象的⼀种弱引⽤,不会增加对象的引⽤计数,和 std::shared_ptr 之间可以相互转化,std::shared_ptr 可以直接赋值给 std::weak_ptrstd::weak_ptr 可以通过调⽤ lock() 函数来获得 std::shared_ptr

比如:当两个智能指针都是 std::shared_ptr 类型的时候,析构时两个资源引⽤计数会减⼀,但两者引⽤计数还是为 1,导致跳出函数时资源没有被释放(析构函数没有被调⽤)。

解决办法就是把其中⼀个智能指针改为 std::weak_ptr,这样就不会有问题了。

特点:

  • 不增加引用计数,避免循环引用。
  • 需要通过lock()方法转换为std::shared_ptr才能访问对象。
#include <memory>

void example() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(10);
    std::weak_ptr<int> weakPtr = sharedPtr; // 不影响引用计数

    if (auto ptr = weakPtr.lock()) { // 检查对象是否仍然存在
        // 使用 ptr
    }
}

智能指针的优势

  • 自动内存管理:智能指针自动管理内存的分配和释放,减少了内存泄漏的风险。
  • 异常安全:在异常发生时,智能指针会自动释放资源,确保资源不被泄漏。
  • 简化代码:通过智能指针,开发者可以专注于业务逻辑,而不必过多关注内存管理细节。

智能指针是C++现代化编程的重要工具,可以通过自动管理内存的生命周期,极大地提高了代码的安全性和可维护性。

理解并正确使用智能指针,将帮助我们编写更高效和可靠的程序。在实际应用中,我们应当根据具体场景需求,去选择合适的智能指针类型。