智能指针

首先理解什么是RAII,RAII(Resource Acquisition Is Initialization)是C++中用来管理资源的生命周期的一种技术。
在 RAII 中,资源的获取和释放是在对象的构造函数和析构函数中完成的。当对象被创建时,它的构造函数被调用,从而获取资源;当对象超出作用域时,其析构函数被调用,从而释放资源。这样就确保了资源在对象的生命周期内始终是有效的,即使在发生异常或其他错误的情况下也能正确地释放资源

不带引用计数的智能指针

下面实现一个非常简单的智能指针

#include <iostream>
#include <stdexcept>

template <typename T>
class SmartPointer
{
public:
    SmartPointer(T *ptr) : _ptr(ptr) {}
    ~SmartPointer()
    {
        delete _ptr;
    }
    T *operator->() { return _ptr; }
    T &operator*() { return *_ptr; }

private:
    T *_ptr;
};

class Bar
{
public:
    ~Bar() { std::cout << "destructor" << std::endl; }
    void func() { std::cout << "func()" << std::endl; }
};

int main()
{
    {
        SmartPointer<Bar> f(new Bar());
        f->func();
        (*f).func();
    }
}

代码的运行结果如下:
func()
func()
destructor

可以看到,这个智能指针非常简单,就是在析构函数中正确释放资源,然后提供了->*运算符重载函数。
智能指针就是对普通指针进行了一层封装,然后提供了对应的运算符重载函数,使其接口用起来像普通指针,同时在智能指针对象出作用域时会自动调用析构函数。
只要保证析构函数中,正确释放了资源,那么就能保证资源在出作用域时自动释放。

一般不将智能指针创建在堆上,创建在堆上SmartPointer<Bar> *f = new SmartPointer<Bar>(new Bar());,仍需要手动delete,才能访问智能指针的析构函数从而释放资源。这种做法与直接使用裸指针无异。

带引用计数的智能指针

上面的版本,不带引用计数,所以有一个很严重的问题。

int main()
{
    {
        SmartPointer<Bar> sp(new Bar());
        SmartPointer<Bar> sp2(sp);  // 拷贝构造
    }
}

上面这个代码片段的执行结果为:
destructor
destructor
free(): double free detected in tcache 2
Aborted (core dumped)

可以发现,程序对同一个内存执行了两次delete。这是因为,SmartPointer<Bar> sp2(sp);执行的是浅拷贝,故两个智能指针对象中存放的是同一个内存地址,所以会有double free。
那么,能不能把这个浅拷贝改成深拷贝呢?
答案显然是不能的,改成深拷贝意味着每次执行拷贝都会重新开辟内存空间,然后拷贝数据,但本来的意图可能只是需要传递一个指针而已,也就是两个指针指向同一个内存。

那么,如何解决这个问题呢?从是否共享所有权的角度来分类,有两种办法:

  1. 不共享所有权
    • 拒绝拷贝的发生,直接把拷贝(移动)构造,拷贝(移动)赋值函数删除,对应的是std::scoped_ptr
    • 每次拷贝都是移动,都把资源的所有权转移出去,也就是只有最后一个智能指针拥有所有权,其它智能指针中都置为nullptr,对应的是std::auto_ptr
    • 允许移动构造、移动赋值操作,不允许拷贝构造、拷贝赋值,也就是c++11中的std::unique_ptr,通过std::unique_ptr<T> uptr2(std::move(uptr1))来创建新对象。

std::scoped_ptr只能用于单个对象,不能拷贝移动,std::auto_ptr自动转移所有权,容易出错,比如再次使用交出所有权的对象,这俩个都不推荐使用。

std::unique_ptr可以移动构造新对象,而且是显式的发生所有权的转移的。

  1. 共享所有权
    利用引用计数,通过引用计数是否为0,来判断是否真的执行delete操作。当发生拷贝时,引用计数加一,当有一个智能指针出作用域,对应的引用计数减一,当引用计数减为0时,才真正执行delete操作。
    这样就能保证,不发生double free,同时最后一个出作用域的智能指针能够正确释放资源。

尝试实现一个带引用技术的智能指针

#include <iostream>
#include <stdexcept>

template <typename T>
class RefCnt
{

public:
    RefCnt(T *ptr) : ptr(ptr)
    {
        if (ptr)
            cnt = 1;
    }

    void increRef()
    {
        cnt++;
    }

    int decreRef()
    {
        return --cnt;
    }

private:
    T *ptr;
    int cnt; // 不是线程安全的
};

template <typename T>
class SharedPtr
{
public:
    SharedPtr(T *ptr) : ptr(ptr)
    {
        rc = new RefCnt<T>(ptr);
    }

    SharedPtr(const SharedPtr &sp) : ptr(sp.ptr), rc(sp.rc)
    {
        if (ptr)
            rc->increRef();
    }

    SharedPtr &operator=(const SharedPtr &sp)
    {
        if (this == &sp)
            return *this;

        if (0 == rc->decreRef())
        {
            delete ptr;
        }

        ptr = sp.ptr;
        rc = sp.rc;
        rc->increRef();
        return *this;
    }

    ~SharedPtr()
    {
        if (0 == rc->decreRef())
        {
            delete ptr;
            delete rc;
        }
    }

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

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

private:
    T *ptr;
    RefCnt<T> *rc;
};

int main()
{
    SharedPtr<int> sp1(new int(10));
    SharedPtr<int> sp2(sp1);
    SharedPtr<int> sp3(nullptr);
    sp3 = sp2;
    *sp1 = 42;
    std::cout << *sp2 << " " << *sp3 << std::endl;
}

shared_ptr,weak_ptr

shared_ptr: 强智能指针 可以改变资源的引用计数
weak_ptr:弱智能指针 不会改变资源的引用计数(一般用于解决强智能指针的循环引用问题,以及多线程环境下访问共享对象的线程安全问题)

shared_ptr

  • shared_ptr 用于共享资源的所有权,可以有多个 shared_ptr 指向相同的内存资源。
  • 每当有一个新的 shared_ptr 指向对象时,该对象的引用计数会增加。(通过将已有的shared_ptr拷贝或者赋值给新的shared_ptr对象)
  • 当引用计数为零时,即没有任何 shared_ptr 指向对象时,对象会被自动销毁。

weak_ptr

  • weak_ptr 可以由 shared_ptr构造而来,如std::weak_ptr<int> weak = shared;
  • weak_ptr 用于解决 shared_ptr 的循环引用问题。它不会增加引用计数,因此不会影响对象的生命周期。
  • weak_ptr 允许你观察一个 shared_ptr 指向的对象,但不拥有它。如果所有指向对象的 shared_ptr 都被销毁,weak_ptr 会自动失效,避免悬挂指针问题。
  • weak_ptr 可以通过 lock() 方法转换为一个 shared_ptr,如果被观察的对象仍然存在,就返回一个有效的 shared_ptr;如果对象已被销毁,则返回一个空的 shared_ptr

weak_ptr可以用于在多线程环境下,对不拥有所有权的资源的安全访问,通过这种弱引用,能够保证,不影响被观察的对象的生命周期,同时又能观察到该目标。
而如果用shared_ptr则会修改目标对象的生命周期。

#include <chrono>
#include <iostream>
#include <memory>
#include <thread>

void worker(std::weak_ptr<int> weak) {
  std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟一段时间后观察
  if (auto shared = weak.lock()) {
    std::cout << "shared_ptr is still valid. Value: " << *shared << std::endl;
  } else {
    std::cout << "shared_ptr has been released." << std::endl;
  }
}

int main() {
  std::shared_ptr<int> shared = std::make_shared<int>(42);

  std::thread observer(worker, std::weak_ptr<int>(shared));

  std::this_thread::sleep_for(std::chrono::seconds(3));
  shared.reset();

  observer.join();
  return 0;
}

weak_ptr还能用于解决shared_ptr导致的循环应用问题

#include <iostream>
#include <memory>

class B; // 前置声明

class A {
public:
    A() { std::cout << "A constructor\n"; }
    ~A() { std::cout << "A destructor\n"; }
    void setB(std::shared_ptr<B> b) { m_b = b; }

private:
    std::shared_ptr<B> m_b;
};

class B {
public:
    B() { std::cout << "B constructor\n"; }
    ~B() { std::cout << "B destructor\n"; }
    void setA(std::weak_ptr<A> a) { m_a = a; }

private:
    std::weak_ptr<A> m_a;
};

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

    // 创建循环引用
    a->setB(b);
    b->setA(a);

    // 释放智能指针
    a.reset();
    b.reset();

    // 此时 A 和 B 对象会正确地被销毁,避免了循环引用导致的内存泄漏
    return 0;
}

自定义删除器

使用 std::shared_ptrstd::unique_ptr 来管理动态分配的内存时,可以使用自定义的删除器(deleter)来指定如何释放资源,这在编写各种线程池、连接池、文件资源管理(使用自定义删除器来调用 fclose 关闭文件)时比较常用。


void custom_deleter(int* ptr) {
    std::cout << "Custom deleter called\n";
    delete ptr;
}

int main() {
    int* raw_ptr = new int(42);
    // 当然,除了使用函数指针,还可以使用函数对象或者lambda来实现。
    std::shared_ptr<int> ptr(raw_ptr, custom_deleter);

    return 0;
}
posted @ 2024-04-24 22:55  EricLing0529  阅读(6)  评论(0编辑  收藏  举报