RAII与智能指针

说在前面:本文主要介绍C++中的RAII理念以及智能指针的使用,并举出了相应的例子进行举例说明。

一. 什么是RAII?

        RAII(Resource Acquisition Is Initialization),也就是资源获取即初始化,是一种在C++中管理资源(如内存、文件句柄、网络连接等)的编程技术。智能指针是RAII原则的一个应用示例,用于自动管理动态分配的内存,以防止内存泄漏和其他资源管理错误。

        RAII的核心理念是在对象的生命周期中获取和释放资源。资源在对象被创建时获取(初始化),并在对象被销毁时释放。这种方法利用了C++的作用域规则,确保即使在出现错误或异常时,资源也能被正确清理。

        RAII的主要优点包括:

  1. 异常安全:资源在对象析构时自动释放,即使发生异常也能保证资源被释放。
  2. 内存管理:避免内存泄漏,因为每个资源都与一个对象的生命周期绑定。
  3. 代码清晰:管理资源的逻辑被封装在对象中,使得资源管理更加直观和一致。

二. 什么是智能指针?

        智能指针是一种实现RAII的类,用于自动管理动态分配的内存。C++标准库提供了几种智能指针,主要包括std::unique_ptrstd::shared_ptrstd::weak_ptr

        智能指针的使用减少了直接使用裸指针(raw pointers)时可能出现的内存管理错误,如忘记释放内存或尝试释放已经释放的内存。通过智能指针,RAII确保资源在正确的时间自动释放,从而提高代码的可靠性和清晰度。

三. 什么是shared_ptr?

        std::shared_ptr 是 C++ 标准库中的一部分,它是一个智能指针,用于自动管理一个引用计数的对象。当最后一个拥有该对象的 shared_ptr 被销毁或被赋予新的对象时,它指向的对象会被删除。

        以下是 std::shared_ptr 的一个基本实现示例(不代表标准库的代码),仅仅是为了举例说明。

template <typename T>
class SharedPtr {
private:
    T* ptr; // 指向管理的对象
    int* count; // 引用计数

public:
    // 构造函数
    SharedPtr(T* p = nullptr) : ptr(p), count(new int(1)) {
        if (ptr == nullptr) {
            *count = 0;
        }
    }

    // 拷贝构造函数
    SharedPtr(const SharedPtr<T>& sp) : ptr(sp.ptr), count(sp.count) {
        (*count)++;
    }

    // 赋值操作符
    SharedPtr<T>& operator=(const SharedPtr<T>& sp) {
        if (this != &sp) {
            // 减少原对象的引用计数
            if (ptr) {
                (*count)--;
                if (*count == 0) {
                    delete ptr;
                    delete count;
                }
            }
            // 赋值新对象
            ptr = sp.ptr;
            count = sp.count;
            (*count)++;
        }
        return *this;
    }

    // 析构函数
    ~SharedPtr() {
        (*count)--;
        if (*count == 0) {
            delete ptr;
            delete count;
        }
    }

    // 重载解引用和指针访问操作符
    T& operator*() const { return *ptr; }
    T* operator->() const { return ptr; }

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

        需要注意的是这里的引用计数是指针类型,因为引用计数必须在智能指针的所有实例之间共享。当创建智能指针的新副本(如通过拷贝构造函数或赋值操作)时,所有副本都需要访问并修改同一个引用计数。使用指向共享计数的指针是实现这种共享的一种简单且有效的方法。

        还有一点需要注意,std::shared_ptr 通过引用计数来管理对象的生命周期。如果两个或多个 shared_ptr 对象相互引用,形成一个闭环,它们的引用计数将永远不会达到零,从而导致内存泄漏。这种情况通常通过使用 std::weak_ptr 来打破循环来解决。接下来我们用代码说明这一点:

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destructed\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B destructed\n"; }
};

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

    a->b_ptr = b;
    b->a_ptr = a;

    return 0; // A 和 B 的析构函数永远不会被调用
}

        这个例子中,两个类 AB 互相持有对方的 std::shared_ptr。当在 main 函数中创建 AB 的实例并相互赋值时,形成了一个循环引用。这意味着,即使 main 函数结束,ab 的作用域结束,它们的引用计数也不会降到零,因为它们相互引用。结果是,AB 的析构函数永远不会被调用,导致内存泄漏。如何解决,我们会在weak_ptr中介绍。

四. 什么是unique_ptr?

        std::unique_ptr 是 C++ 标准库中的一种智能指针,它提供了对动态分配内存的独占所有权。这意味着在任何时刻,最多只有一个 std::unique_ptr 实例可以指向一个特定的内存资源。当该 std::unique_ptr 实例被销毁或指向另一个资源时,它原先指向的资源也会被自动释放。std::unique_ptr 是一种轻量级的智能指针,相比于 std::shared_ptr,它具有更少的内存和性能开销。这里我们也给出unique_ptr的代码(仅供参考):

template <typename T>
class UniquePtr {
private:
    T* ptr; // 原始指针

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

    // 禁止拷贝构造
    UniquePtr(const UniquePtr&) = delete;

    // 禁止拷贝赋值
    UniquePtr& operator=(const UniquePtr&) = delete;

    // 移动构造函数
    UniquePtr(UniquePtr&& other) : ptr(nullptr) {
        std::swap(ptr, other.ptr);
    }

    // 移动赋值运算符
    UniquePtr& operator=(UniquePtr&& other) {
        if (this != &other) {
            delete ptr; // 释放当前资源
            ptr = nullptr;
            std::swap(ptr, other.ptr);
        }
        return *this;
    }

    // 解引用操作符
    T& operator*() const { return *ptr; }

    // 指针访问操作符
    T* operator->() const { return ptr; }

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

    // 释放资源并重置指针
    void reset(T* p = nullptr) {
        delete ptr;
        ptr = p;
    }

    // 析构函数
    ~UniquePtr() {
        delete ptr;
    }
};

        需要注意的是UniquePtr 禁止了拷贝构造和拷贝赋值,以确保对象的独占所有权。通过实现移动语义,它允许所有权的转移。

五. 什么是weak_ptr?

  std::weak_ptr 是 C++ 标准库中的一种智能指针,它设计用来观察但不拥有 std::shared_ptr 管理的对象。它与 std::shared_ptr 协同工作,提供了一种访问由 shared_ptr 所拥有的对象的方式,同时不影响该对象的引用计数。这种特性使得 std::weak_ptr 非常适合解决 std::shared_ptr 相互引用(即循环引用)时可能导致的内存泄漏问题。std::weak_ptr 不会增加或减少它所观察的对象的引用计数。这意味着它不会阻止 std::shared_ptr 所管理的对象被销毁。当两个或多个 std::shared_ptr 实例相互引用时,可能导致循环引用,从而引起内存泄漏。std::weak_ptr 可以被用来打破这种循环,因为它不会增加引用计数。

        这里给出上面循环引用通过weak_ptr解决后的代码:

#include <iostream>
#include <memory>

class B;

class A {
public:
    std::weak_ptr<B> b_ptr; // 改为 weak_ptr
    ~A() { std::cout << "A destructed\n"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 改为 weak_ptr
    ~B() { std::cout << "B destructed\n"; }
};

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

    a->b_ptr = b;
    b->a_ptr = a;

    return 0; // A 和 B 的析构函数将被正确调用
}

        在这个修正后的版本中,AB 类使用 std::weak_ptr 相互引用,这样即使它们相互引用,也不会阻止它们的析构函数被调用和资源被释放。当 main 函数结束时,ab 的作用域结束,它们的引用计数降到零,从而允许 AB 被正确销毁。

六. 如何通过weak_ptr访问资源?

       要通过 std::weak_ptr 访问资源,你需要先将它转换为 std::shared_ptr。由于 std::weak_ptr 本身不拥有资源,它仅提供对 std::shared_ptr 所管理对象的非拥有性引用,所以直接访问资源的能力有限。转换为 std::shared_ptr 确保了在访问资源时该资源仍然存在。

        这种转换通常使用 std::weak_ptr 的成员函数 lock() 来实现。lock() 方法检查 std::weak_ptr 所关联的 std::shared_ptr 是否仍存在(即是否还有其他 std::shared_ptr 实例指向该资源),如果存在,则返回一个指向该资源的有效 std::shared_ptr 实例;如果不存在(即资源已被释放),则返回一个空的 std::shared_ptr 实例。

下面是一个使用 std::weak_ptr 访问资源的示例:

#include <iostream>
#include <memory>

class MyClass {
public:
    void show() { std::cout << "MyClass::show()\n"; }
};

int main() {
    std::shared_ptr<MyClass> sp = std::make_shared<MyClass>(); // 创建 shared_ptr
    std::weak_ptr<MyClass> wp = sp; // 创建 weak_ptr,观察 sp 指向的对象

    // 使用 lock() 方法获取 shared_ptr
    std::shared_ptr<MyClass> sp2 = wp.lock();
    if (sp2) { // 检查得到的 shared_ptr 是否有效
        sp2->show(); // 访问资源
    }

    sp.reset(); // 释放原始 shared_ptr 指向的对象

    sp2 = wp.lock(); // 再次尝试获取 shared_ptr
    if (!sp2) {
        std::cout << "Resource is no longer available\n";
    }

    return 0;
}

        在这个例子中,我们首先创建了一个指向 MyClassstd::shared_ptrsp),然后创建了一个 std::weak_ptrwp),它观察 sp 指向的对象。通过调用 wp.lock(),我们试图获取一个新的 std::shared_ptrsp2)来访问和操作对象。当原始的 std::shared_ptrsp)被重置或销毁,导致没有 std::shared_ptr 指向对象时,wp.lock() 将返回一个空的 std::shared_ptr,这表示资源不再可用。这样的机制确保了即使原始的 std::shared_ptr 不存在时,也能安全地检查和访问资源。

七. shared_ptr有哪些创建方式?

1. 使用 std::make_sharedstd::make_shared 是创建 std::shared_ptr 的推荐方式。它在一个单独的动态内存分配中同时构造对象和管理它的控制块(包含引用计数等信息)。

std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(args...);

2. 直接构造: 可以直接构造 std::shared_ptr,但这种方式会导致两次动态内存分配——一次用于对象,另一次用于控制块。

std::shared_ptr<MyClass> ptr(new MyClass(args...));

3. 从 std::unique_ptr 转换: 你可以将一个 std::unique_ptr 转换为 std::shared_ptr。这会将所有权从 std::unique_ptr 转移到 std::shared_ptr

std::unique_ptr<MyClass> uniquePtr(new MyClass(args...));
std::shared_ptr<MyClass> ptr = std::move(uniquePtr);

4. 从另一个 std::shared_ptrstd::weak_ptr 复制/赋值: 通过复制或赋值另一个 std::shared_ptrstd::weak_ptr 也可以创建 std::shared_ptr。这会增加与源智能指针关联的对象的引用计数。

std::shared_ptr<MyClass> original = std::make_shared<MyClass>(args...);
std::shared_ptr<MyClass> ptr = original; // 复制构造

std::weak_ptr<MyClass> weakPtr = original;
std::shared_ptr<MyClass> ptrFromWeak = weakPtr.lock(); // 从 weak_ptr 构造

八. 总结

        本篇博客我们主要介绍了RAII与智能指针的一些基本概念,并通过理论和代码结合的方式说明了它们的基本用法。

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值