Wang's blog

智能指针

Published on

C++11标准废弃了原来的智能指针auto_ptr,同时引入了三个新的智能指针:unique_ptr, shared_ptr与weak_ptr。它们基于RAII原则进行内存管理。

RAII(资源获取即初始化)

C++中可以在栈上或堆上申请内存,栈上申请的内存会在超出作用域时自动释放,但是堆上申请的内存需要手动释放,否则会出现内存泄漏。在现代C++中,通常基于RAII原则编写代码,以避免出现内存泄漏。

RAII的基本原则是:尽可能在栈上申请资源,如果由于资源太大或其它原因必须在堆上申请时,则该资源需要由一个栈上的对象持有;在该对象初始化时即获取该资源,同时在该对象的析构函数中释放该资源。在超出持有资源对象的作用域时,其析构函数会被自动调用,进而释放所持有的资源,从而避免了内存泄漏。

unique_ptr

特点

unique_ptr对象持有一个底层指针,在unique_ptr对象析构时会自动释放该指针。通过禁用拷贝构造函数与拷贝赋值操作符,unique_ptr保证此底层指针只有一个持有者。如需转移底层指针的所有权,可以使用移动语义,移动后原unique_ptr失效。

在确定同时只能有一个所有者的情况下,使用unique_ptr可以在编译期间即保证此约束,从而避免运行期间出现错误,同时可以简化代码逻辑并提高运行效率。

实现

在unique_ptr类中,至少需要实现以下功能:

  • 普通构造函数:保存传入的指针
  • 析构函数:释放持有的指针
  • 拷贝构造函数/赋值操作符:禁止使用,避免多个持有者
  • 移动构造函数/赋值操作符:实现底层指针所有权的转移
  • *与->操作符:实现与普通指针相同的操作

一个简单的实现如下:

template <typename T>
class unique_ptr
{
public:
    // 普通构造函数,保存传入的指针
    unique_ptr(T *p = nullptr) : ptr_(p) {}

    // 析构函数
    ~unique_ptr()
    {
        // 底层指针非空则释放
        if (ptr_)
        {
            delete ptr_;
            ptr_ = nullptr;
        }
    }

    // 禁用拷贝构造函数与赋值操作符,保证只有一个所有者
    unique_ptr(const unique_ptr &) = delete;
    unique_ptr &operator=(const unique_ptr &) = delete;

    // 移动构造函数
    unique_ptr(unique_ptr &&p)
    {
        // 保存底层指针
        ptr_ = p.ptr_;
        // 将原unique_ptr中的底层指针置空,避免同时有两个所有者
        p.ptr_ = nullptr;
    }
    // 移动赋值操作符,与移动构造函数做法相同
    unique_ptr &operator=(unique_ptr &&p)
    {
        ptr_ = p.ptr_;
        p.ptr_ = nullptr;
        return *this;
    }

    // 实现*与->操作符,使智能指针的使用与普通指针相同
    T &operator*() const noexcept
    {
        return *ptr_;
    }
    T *operator->() const noexcept
    {
        return ptr_;
    }

    // 实现其它所需方法
    // ......

private:
    // 底层指针
    T *ptr_;
};

使用

struct A
{
    // ......
};

int main()
{
    // 建立unique_ptr对象,传入需要管理的底层指针(对传入的指针不要再做任何操作)
    std::unique_ptr<A> pa = std::unique_ptr<A>(new A);

    // 错误,禁止复制unique_ptr
    std::unique_ptr<A> pb(pa);
    std::unique_ptr<A> pb = pa;

    // 正确,可以移动所有权,移动后通过pb可以访问所管理指针,通过pa不再能访问
    std::unique_ptr<A> pb = std::move(pa);

    // 推荐使用make_unique函数建立unique_ptr(C++14添加),此方式更加简洁,效率更高,且保证异常安全
    std::unique_ptr<A> pc = std::make_unique<A>();

} // 退出作用域后,所有智能指针与底层指针都被自动释放

shared_ptr

特点

与unique_ptr不同,在shared_ptr中,一个底层指针可以有多个持有者,因此shared_ptr可以进行拷贝构造或赋值。shared_ptr通过添加一个引用计数来记录持有者的数量,当最后一个持有者对象析构时,会自动释放底层指针,从而保证没有内存泄漏。

shared_ptr适合应用在同时可能有多个持有者操作指针,但是并不知道哪个持有者最后释放的场景中。

实现

在shared_ptr类中,至少需要实现以下功能:

  • 普通构造函数:保存传入的指针,引用计数设为1
  • 析构函数:引用计数-1,如果归0,则释放所持有的指针
  • 拷贝构造函数/赋值操作符:复制底层指针,引用计数+1
  • *与->操作符:实现与普通指针相同的操作

一个简单的实现如下:

template <typename T>
class shared_ptr
{
public:
    // 普通构造函数,保存传入的指针,初始化引用计数为1
    shared_ptr(T *p = nullptr) : ptr_(p), rc_(new int(1)) {}

    // 析构函数
    ~shared_ptr()
    {
        // 释放当前资源
        release();
    }

    // 拷贝构造函数
    shared_ptr(const shared_ptr &p) : ptr_(p.ptr_), rc_(p.rc_)
    {
        // 拷贝数据后引用计数+1
        ++(*rc_);
    }
    // 拷贝赋值操作符
    shared_ptr &operator=(const shared_ptr &p)
    {
        // 如果不指向同一底层指针
        if (ptr_ != p.ptr_)
        {
            // 释放当前资源
            release();
            // 拷贝数据
            ptr_ = p.ptr_;
            rc_ = p.rc_;
            // 引用计数+1
            ++(*rc_);
        }
        return *this;
    }

    // 实现*与->操作符,使智能指针的使用与普通指针相同
    T &operator*() const noexcept
    {
        return *ptr_;
    }
    T *operator->() const noexcept
    {
        return ptr_;
    }

    // 实现其它所需方法
    // ......

private:
    // 释放当前资源
    void release()
    {
        // 引用计数-1
        --(*rc_);
        // 如果引用计数归0,释放底层指针与引用计数指针
        if (*rc_ == 0)
        {
            delete ptr_;
            delete rc_;
            ptr_ = nullptr;
            rc_ = nullptr;
        }
    }

    // 底层指针
    T *ptr_;
    // 引用计数
    int *rc_;
};

使用

struct A
{
    // ......
};

int main()
{
    // 建立shared_ptr对象,传入需要管理的底层指针(对传入的指针不要再做任何操作)
    std::shared_ptr<A> pa = std::shared_ptr<A>(new A);

    // 可以复制与赋值,引用计数增加
    std::shared_ptr<A> pb(pa);
    std::shared_ptr<A> pb = pa;

    // 同样推荐使用make_shared函数建立shared_ptr
    std::shared_ptr<A> pc = std::make_shared<A>();

} // 退出作用域后,所有智能指针被自动释放(如果没有其它持有者,则底层指针也被释放)

weak_ptr

特点

weak_ptr是弱化版本的shared_ptr,它仅用于监视shared_ptr中管理的资源是否存在,以避免出现循环引用等问题。weak_ptr并不影响引用计数,也不能通过weak_ptr直接使用底层指针。

作用

shared_ptr对象未释放前,其持有的底层指针不会释放,如果出现循环引用的情况,会造成内存泄漏。例如:

struct B;

struct A
{
    std::shared_ptr<B> b;
};

struct B
{
    std::shared_ptr<A> a;
};

int main()
{
    std::shared_ptr<A> pa = std::make_shared<A>();  // 建立A指针,引用计数为1
    std::shared_ptr<B> pb = std::make_shared<B>();  // 建立B指针,引用计数为1
    pa->b = pb;                                     // B指针引用计数为2
    pb->a = pa;                                     // A指针引用计数为2
} // 两个底层指针引用计数仍为1,因此没有释放,造成内存泄漏

上面的例子中,pa与pb相互持有对方的一个shared_ptr,因此存在循环引用。即使在主函数退出后,两个指针的引用计数仍然都为1,因此不能释放,导致内存泄漏。修改方法是将循环引用中的任意一个shared_ptr改为weak_ptr,例如:

struct B
{
    std::weak_ptr<A> a;
};

使用

int main()
{
    // weak_ptr必须使用shared_ptr初始化
    std::shared_ptr<int> sp1 = std::make_shared<int>(10);
    std::weak_ptr<int> wp = sp1;

    // 通过expired函数检查是否失效
    if (!wp.expired())
    {
        // 通过lock函数生成shared_ptr才能使用
        std::shared_ptr<int> sp2 = wp.lock();
        *sp2 = 100;
    }
}

其它

自定义删除器(deleter)

unique_ptr与shared_ptr默认使用delete语句释放指针,用户可以在建立智能指针时指定一个函数/仿函数/lambda表达式执行自定义释放操作。

std::unique_ptr<D, void (*)(D *)> p1(new D, [](D *ptr) { delete ptr; });

std::shared_ptr<D> p2(new D, [](D *ptr) { delete ptr; });

数组的智能指针

如果将数组作为底层指针传入智能指针,由于默认使用delete对底层指针进行释放,此时会产生未定义行为。可以使用自定义删除器或模板特化的方法避免此问题。

// 错误,使用默认删除器会产生未定义行为
std::shared_ptr<int> sp1(new int[10]());

// 正确,使用自定义删除器
std::shared_ptr<int> sp2(new int[10](), std::default_delete<int[]>());

// 正确,使用模板特化(C++17中添加),同时实现了[]操作符,可以将智能指针作为数组使用
std::shared_ptr<int[]> sp3(new int[10]());
sp3[0] = 10;