Bootstrap

C++智能指针 `auto_ptr`、`unique_ptr`、`shared_ptr`、`weak_ptr`、scoped_ptr


以下是对 C++ 中几种常见智能指针( auto_ptrunique_ptrshared_ptrweak_ptr、scoped_ptr)的详细讲解,包括它们的特点、使用场景以及部分关键源码逻辑(不同编译器的实现细节可能略有差异,这里以常见的概念性实现思路来讲解):

1. auto_ptr(已弃用,C++11 起不推荐使用)

特点与使用场景
  • auto_ptr 是 C++ 早期引入的一种智能指针,用于解决普通指针在对象生命周期管理方面的问题,它尝试实现自动的资源(通常是动态分配的堆内存)释放。其基本思想是,当 auto_ptr 对象离开其作用域时,它所管理的对象会自动被删除,避免了内存泄漏。不过,它有一些缺陷导致后来被弃用,比如不支持拷贝语义的正确实现(拷贝构造和赋值操作会转移对象的所有权,而不是进行常规的深拷贝),容易引发悬空指针等问题。
简单示例代码
#include <iostream>
#include <memory>

int main() {
    std::auto_ptr<int> ptr(new int(10));
    std::cout << *ptr << std::endl;
    // 当 ptr 离开作用域时,它所指向的 int 对象会被自动 delete
    return 0;
}
关键源码逻辑简析(简化示意)

以下是一种简单的模拟 auto_ptr 部分关键功能实现的源码思路(并非完整标准库实现):

template<typename T>
class auto_ptr {
public:
    explicit auto_ptr(T* p = nullptr) : m_ptr(p) {}

    // 析构函数,用于释放资源
    ~auto_ptr() {
        delete m_ptr;
    }

    // 重载解引用操作符,使得可以像普通指针一样使用
    T& operator*() const {
        return *m_ptr;
    }

    // 重载箭头操作符,方便访问对象成员
    T* operator->() const {
        return m_ptr;
    }

    // 拷贝构造函数,转移所有权
    auto_ptr(auto_ptr& other) : m_ptr(other.release()) {}

    // 赋值操作符,转移所有权
    auto_ptr& operator=(auto_ptr& other) {
        if (this!= &other) {
            delete m_ptr;
            m_ptr = other.release();
        }
        return *this;
    }

    // 释放当前管理的指针并返回,转移所有权
    T* release() {
        T* tmp = m_ptr;
        m_ptr = nullptr;
        return tmp;
    }

private:
    T* m_ptr;
};

在这个简化的代码中:

  • 构造函数接受一个原始指针,并将其保存在 m_ptr 成员变量中,用于后续管理。
  • 析构函数中通过 delete m_ptr 实现了当 auto_ptr 对象生命周期结束时自动释放其所指向的对象内存。
  • 解引用操作符 operator* 和箭头操作符 operator-> 的重载使得 auto_ptr 可以像普通指针一样方便地操作所指向的对象。
  • 拷贝构造函数和赋值操作符的实现是 auto_ptr 的关键也是问题所在,它们通过调用 release 函数将被拷贝或赋值对象的指针所有权转移过来,原对象就不再管理该指针(置为 nullptr),这就导致了原对象成为悬空指针,不符合常规的拷贝语义,容易引发错误,所以后来被弃用。

2. unique_ptr

特点与使用场景
  • unique_ptr 是 C++11 引入的独占式智能指针,它保证同一时刻只有一个 unique_ptr 实例可以指向特定的对象,即独占所管理对象的所有权。它不允许拷贝构造和拷贝赋值(体现独占性),但支持移动构造和移动赋值,通过移动语义可以将对象的所有权从一个 unique_ptr 转移到另一个,常用于资源独占且不需要共享所有权的场景,比如管理独占的文件句柄、数据库连接等,能有效避免资源的多重释放和悬空指针等问题。
简单示例代码
#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
    // 以下代码会编译错误,因为 unique_ptr 不支持拷贝构造
    // std::unique_ptr<int> ptr2 = ptr1;
    std::unique_ptr<int> ptr3 = std::move(ptr1);  // 通过移动语义转移所有权
    std::cout << *ptr3 << std::endl;
    return 0;
}
关键源码逻辑简析(简化示意)

下面是一个简化的 unique_ptr 部分关键功能源码实现思路(大致体现核心逻辑):

template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
public:
    explicit unique_ptr(T* p = nullptr) : m_ptr(p) {}

    ~unique_ptr() {
        m_deleter(m_ptr);  // 通过删除器(默认为 std::default_delete)释放资源
    }

    T& operator*() const {
        return *m_ptr;
    }

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

    // 移动构造函数,转移所有权
    unique_ptr(unique_ptr&& other) noexcept : m_ptr(other.release()) {}

    // 移动赋值操作符,转移所有权
    unique_ptr& operator=(unique_ptr&& other) noexcept {
        if (this!= &other) {
            m_deleter(m_ptr);
            m_ptr = other.release();
        }
        return *this;
    }

    // 释放当前管理的指针并返回,转移所有权
    T* release() {
        T* tmp = m_ptr;
        m_ptr = nullptr;
        return tmp;
    }

    // 获取当前管理的指针
    T* get() const {
        return m_ptr;
    }

private:
    T* m_ptr;
    Deleter m_deleter;
};

在这个模拟源码中:

  • 构造函数接受一个指针并存储在 m_ptr 成员变量中,同时可以指定一个删除器类型(默认是 std::default_delete,用于处理对象的释放逻辑,比如针对数组类型的 unique_ptr 可以自定义合适的删除器来实现正确的 delete[] 操作)。
  • 析构函数通过调用 m_deleter(m_ptr) 来释放资源,这使得可以灵活定制释放策略。
  • 移动构造函数和移动赋值操作符的实现中,通过调用 release 函数将源 unique_ptr 的指针所有权转移到当前对象,保证了同一时刻只有一个 unique_ptr 拥有该指针,符合独占所有权的设计理念,并且使用 noexcept 关键字标记,表示这些操作不会抛出异常,优化了编译器在一些场景下的处理(比如在容器移动元素时)。

3. shared_ptr

特点与使用场景
  • shared_ptr 是 C++11 引入的共享式智能指针,多个 shared_ptr 实例可以同时指向同一个对象,它们通过引用计数机制来管理对象的生命周期。每当一个新的 shared_ptr 指向该对象时,引用计数加1;当一个 shared_ptr 不再指向该对象(比如离开作用域或者被重新赋值等情况)时,引用计数减1,当引用计数减到0时,说明没有任何 shared_ptr 指向该对象了,此时才会自动释放对象所占用的资源。常用于需要在多个地方共享对象所有权的场景,比如在不同模块间共享数据结构等,但要注意避免循环引用问题(后面结合 weak_ptr 讲解如何解决)。
简单示例代码
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
    std::shared_ptr<int> ptr2 = ptr1;  // 引用计数加1,现在两个 shared_ptr 指向同一个对象
    std::cout << *ptr1 << std::endl;
    std::cout << ptr1.use_count() << std::endl;  // 输出引用计数,此时应为 2
    return 0;
}
关键源码逻辑简析(简化示意)

以下是一个简化的展示 shared_ptr 核心逻辑的源码思路(并非完整标准库实现):

template<typename T>
class shared_ptr {
public:
    // 构造函数
    explicit shared_ptr(T* p = nullptr) : m_ptr(p), m_count(new size_t(1)) {}

    ~shared_ptr() {
        release();
    }

    T& operator*() const {
        return *m_ptr;
    }

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

    // 拷贝构造函数,增加引用计数
    shared_ptr(const shared_ptr& other) : m_ptr(other.m_ptr), m_count(other.m_count) {
        ++(*m_count);
    }

    // 赋值操作符,先处理原对象引用计数,再增加新对象引用计数
    shared_ptr& operator=(const shared_ptr& other) {
        if (this!= &other) {
            release();
            m_ptr = other.m_ptr;
            m_count = other.m_count;
            ++(*m_count);
        }
        return *this;
    }

    // 获取引用计数
    size_t use_count() const {
        return *m_count;
    }

private:
    void release() {
        if (m_ptr && --(*m_count) == 0) {
            delete m_ptr;
            delete m_count;
        }
    }

    T* m_ptr;
    size_t* m_count;
};

在这个简化源码中:

  • 构造函数接受一个指针并初始化为 m_ptr,同时创建一个新的 size_t 类型变量(通过 new 分配内存)用于存储引用计数,初始值设为1,表示当前只有一个 shared_ptr 指向该对象。
  • 拷贝构造函数和赋值操作符的实现中,都会将新的 shared_ptr 指向原对象所指向的指针 m_ptr,并且对引用计数 m_count 进行操作,使其加1,表示多了一个 shared_ptr 共享该对象,体现了共享所有权的特点。
  • 析构函数中调用 release 函数,在 release 函数里先判断如果指针不为空且引用计数减到0了,就释放指针所指向的对象内存以及存储引用计数的内存(通过 delete),确保资源在没有任何 shared_ptr 指向时能正确释放。

4. weak_ptr

特点与使用场景
  • weak_ptr 是配合 shared_ptr 使用的一种智能指针,它主要用于解决 shared_ptr 可能出现的循环引用问题。weak_ptr 指向一个由 shared_ptr 管理的对象,但它不会增加对象的引用计数,也就是不会影响对象的生命周期。当需要访问对象时,可以通过 weak_ptr 来尝试获取一个有效的 shared_ptr(通过 lock 函数),如果对象还存在(引用计数大于0),就能获取到 shared_ptr 进而访问对象,否则获取到的 shared_ptr 为空。常用于观察者模式等场景中,避免对象之间因为相互持有 shared_ptr 而导致无法正常释放的情况。
简单示例代码
#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> ptr_b;
    ~A() {
        std::cout << "A的析构函数被调用" << std::endl;
    }
};

class B {
public:
    std::shared_ptr<A> ptr_a;
    ~B() {
        std::cout << "B的析构函数被调用" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->ptr_b = b;
    b->ptr_a = a;
    // 此时存在循环引用,A和B对象的引用计数都不会减到0,不会自动析构
    // 通过 weak_ptr 来打破循环引用
    std::weak_ptr<A> weak_a = a;
    std::weak_ptr<B> weak_b = b;
    a.reset();
    b.reset();
    if (std::shared_ptr<A> shared_a = weak_a.lock()) {
        std::cout << "A对象仍然存在" << std::endl;
    } else {
        std::cout << "A对象已不存在" << std::endl;
    }
    if (std::shared_ptr<B> shared_b = weak_b.lock()) {
        std::cout << "B对象仍然存在" << std::endl;
    } else {
        std::cout << "B对象已不存在" << std::endl;
    }
    return 0;
}
关键源码逻辑简析(简化示意)

以下是一个简单的 weak_ptr 部分关键功能源码思路(大致体现原理):

template<typename T>
class weak_ptr {
public:
    weak_ptr() : m_ptr(nullptr), m_count(nullptr) {}

    weak_ptr(const shared_ptr<T>& other) : m_ptr(other.m_ptr), m_count(other.m_count) {}

    // 通过 lock 函数尝试获取 shared_ptr,如果对象存在则返回有效的 shared_ptr,否则返回空的 shared_ptr
    std::shared_ptr<T> lock() const {
        return m_count && *m_count > 0? std::shared_ptr<T>(*this) : std::shared_ptr<T>();
    }

private:
    T* m_ptr;
    std::shared_ptr<size_t> m_count;  // 指向 shared_ptr 中引用计数的指针,用于判断对象是否存在
};

在这个简化源码中:

  • 构造函数可以接受一个 shared_ptr 来初始化,获取其指向的对象指针 m_ptr 和引用计数指针 m_count,但它本身不会改变引用计数的值,体现了不影响对象生命周期的特点。
  • lock 函数是 weak_ptr 的关键操作,它通过判断引用计数是否大于0(通过 m_count 指针所指向的值来判断),如果大于0,就利用自身的信息(指针和引用计数指针)构造并返回一个有效的 shared_ptr,这样就可以通过返回的 shared_ptr 安全地访问对象;如果引用计数为0,说明对象已经被释放了,就返回一个空的 shared_ptr,避免了访问已不存在的对象。

5. scoped_ptr

它是 boost 库(一个很常用的C++ 第三方库,为C++ 标准库提供了许多补充和扩展功能)中提供的一种智能指针,不过在C++11 及之后,std::unique_ptr 基本涵盖了它的功能并成为了标准库的一部分,以下是对 scoped_ptr 的详细介绍:

1. 基本概念与特点

  • 独占所有权scoped_ptr 实现了对其所管理对象的独占式所有权,这意味着在同一时刻,只有一个 scoped_ptr 实例能够持有并管理特定的对象。与 std::unique_ptr 类似,这种独占性可以有效避免多个指针同时管理一个对象可能导致的资源重复释放、悬空指针等问题,保证了资源管理的清晰性和安全性。
  • 生命周期管理:它主要用于管理在堆上动态分配的对象,当 scoped_ptr 实例超出其作用域时(比如函数执行完毕、代码块结束等情况),会自动调用其析构函数,在析构函数中释放所管理的对象内存,从而防止内存泄漏。这种自动的资源释放机制遵循了RAII(Resource Acquisition Is Initialization,资源获取即初始化)的原则,使得程序员无需手动去释放对象,减少了因忘记释放内存而造成内存泄漏的风险。
  • 不可拷贝和不可赋值scoped_ptr 不允许进行拷贝构造和赋值操作,这是为了严格保证其独占性。如果允许拷贝,那就违背了只有一个指针能管理对象的初衷,容易出现混乱的资源管理情况,所以编译器会禁止对 scoped_ptr 进行这类操作,例如:
#include <boost/smart_ptr/scoped_ptr.hpp>
#include <iostream>

int main() {
    boost::scoped_ptr<int> ptr1(new int(10));
    // 以下代码会编译失败,因为 scoped_ptr 不支持拷贝构造
    // boost::scoped_ptr<int> ptr2 = ptr1;
    return 0;
}

2. 使用场景

  • 局部资源管理:在函数内部动态分配了一些资源(比如使用 new 操作符创建了对象),希望在函数结束时能自动释放这些资源,就可以使用 scoped_ptr。例如:
#include <boost/smart_ptr/scoped_ptr.hpp>
#include <iostream>

void func() {
    boost::scoped_ptr<SomeClass> local_obj(new SomeClass());
    // 使用 local_obj 对 SomeClass 对象进行操作,当函数结束时,local_obj 超出作用域,
    // 其管理的 SomeClass 对象会自动被释放,无需手动调用 delete
}
  • 类的成员变量管理:在类中,如果有某个成员变量是通过动态分配内存创建的,并且希望在类对象生命周期结束时自动释放该成员所指向的对象,也可以使用 scoped_ptr 来管理,保证资源的正确释放,避免内存泄漏风险。

3. 关键源码逻辑简析(简化示意)

以下是一个大致模拟 boost::scoped_ptr 核心功能的简化源码示例(并非完整的 boost 库实现,只是帮助理解其基本原理):

template<typename T>
class scoped_ptr {
public:
    explicit scoped_ptr(T* p = nullptr) : m_ptr(p) {}

    ~scoped_ptr() {
        delete m_ptr;
    }

    T& operator*() const {
        return *m_ptr;
    }

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

    // 禁用拷贝构造函数
    scoped_ptr(const scoped_ptr&) = delete;

    // 禁用赋值操作符
    scoped_ptr& operator=(const scoped_ptr&) = delete;

private:
    T* m_ptr;
};

在这个简化的代码中:

  • 构造函数:接受一个指向对象的指针 T* p,并将其存储在成员变量 m_ptr 中,用于后续对对象的管理。例如 scoped_ptr<int> ptr(new int(10)); 就是通过构造函数将新分配的 int 类型对象的指针交给 scoped_ptr 实例来管理。
  • 析构函数:当 scoped_ptr 实例生命周期结束时(比如离开作用域),析构函数会被自动调用,在析构函数中通过 delete m_ptr 来释放其所管理的对象内存,确保资源能正确回收,这体现了自动资源管理的功能。
  • 解引用操作符 operator* 和箭头操作符 operator-> 的重载:使得 scoped_ptr 可以像普通指针一样方便地操作所管理的对象。例如,如果 scoped_ptr<SomeClass> ptr(new SomeClass());,那么就可以通过 *ptr 来访问 SomeClass 对象的成员(前提是 SomeClass 有合适的重载了 operator* 的相关逻辑),通过 ptr->member 来访问 SomeClass 对象的成员变量或成员函数等,提高了使用的便利性。
  • 禁用拷贝构造函数和赋值操作符:通过将它们定义为 = delete,明确告知编译器不允许使用这两个操作,从语法层面上阻止了违反独占所有权规则的行为,保证了 scoped_ptr 的独占性特点。

总的来说,scoped_ptr 是一种简单而有效的智能指针,虽然在C++11 后被标准库中的 unique_ptr 所替代,但在一些旧代码或者仍然使用 boost 库的项目中还能看到它的身影,并且理解它的原理和使用方式对于掌握智能指针以及更好地进行资源管理有很大的帮助。

总之,C++ 中的这些智能指针各有特点和适用场景,合理使用它们可以帮助我们更方便、安全地管理动态分配的资源,避免内存泄漏、悬空指针等常见的内存管理问题,提升代码的健壮性和可维护性。

;