智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。c++引入了 3 种类型的智能指针:

  • std::unique_ptr :独占资源所有权的指针。
  • std::shared_ptr :共享资源所有权的指针。
  • std::weak_ptr:共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。

智能指针定义在头文件<memory>中。

unique_ptr

std::unique_ptr独占所指向的对象。对其持有的堆内存具有唯一拥有权

简单说,当我们独占资源的所有权的时候,可以使用 std::unique_ptr 对资源进行管理。 当离开 unique_ptr 对象的作用域时,会自动释放资源。这是基本的 RAII 思想。

{
    std::unique_ptr<int> uptr = std::make_unique<int>(200);
    //...
    // 离开 uptr 的作用域的时候自动释放内存
}

std::unique_ptr的初始化

std::unique_ptr<int> sp3 = std::make_unique<int>(123); // 推荐使用

std::unique_ptr<int> up1(new int(123));

std::unique_ptr<int> up2;
sp2.reset(new int(123));

std::unique_ptr 指向数组

{
    std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);
    for (int i = 0; i < 10; i++) {
        uptr[i] = i * i;
    }
    for (int i = 0; i < 10; i++) {
        std::cout << uptr[i] << std::endl;
    }
}

std::unique_ptr 禁止复制语义

为了达到唯一拥有权效果,std::unique_ptr 类的拷贝构造函数和赋值运算符(operator =)被标记为 delete。

unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

std::unique_ptr 支持移动语义

可以通过调用 release 或 reset 将指针所有权从一个(非 const)unique_ptr 转移给另一个 unique_ptr

#include <iostream>
#include <memory>
using namespace std;
int main()
{
    unique_ptr<string> p1 = std::make_unique<string>("hello");

    // 将所有权从p1转移给p2
    unique_ptr<string> p2(p1.release());  // release将p1置为空
    assert(p1 == nullptr);

    unique_ptr<string> p3;
    // 将所有权从p3转移到p2
    p3.reset(p2.release());  // reset释放了p2原来指向的内存
    assert(p2 == nullptr);
}

也可以调用 std::move 移动语义转移 unique_ptr:

{
    std::unique_ptr<int> uptr = std::make_unique<int>(200);
    std::unique_ptr<int> uptr2 = std::move(uptr);
    assert(uptr == nullptr);
}

使用自定义 deleter

unique_ptr 默认使用 delete 释放它指向的对象,我们可以重载一个 unique_ptr 中默认的删除器

template< class T, class Deleter = std::default_delete<T> > class unique_ptr;
// 指向类数组时,必须显式定义Deleter
template< class T, class Deleter > class unique_ptr<T[], Deleter>;

我们必须在尖括号中 unique_ptr 指向类型之后提供删除器类型。在创建或 reset 一个这种 unique_ptr 类型的对象时,必须提供一个指定类型的可调用对象删除器。

{
    struct FileCloser {
        void operator()(FILE* fp) const {
            if (fp != nullptr) {
                fclose(fp);
            }
        }
    };
    std::unique_ptr<FILE, FileCloser> uptr(fopen("test_file.txt", "w"));
}

std::unique_ptr 成员函数

  1. release():解除该智能指针和其所指对象的联系;返回裸指针;该智能指针被置空;但是其指向的内存没有被释放。
  2. reset()
    1. 不带参数:释放智能指针所指向的对象,并将智能指针置空。
    2. 带参数:释放智能指针所指向的对象,并将该智能指针指向新对象。
  3. swap():交换两个智能指针所指向的对象。
  4. get():返回智能指针中保存的裸指针。

shared_ptr

std::shared_ptr 事实上是对资源做引用计数:当引用计数为 0 的时候,自动释放资源。

{
    std::shared_ptr<int> sptr = std::make_shared<int>(200);
      // 此时引用计数为 1
    assert(sptr.use_count() == 1);
    {
        std::shared_ptr<int> sptr1 = sptr;
        assert(sptr.get() == sptr1.get());
        // sptr 和 sptr1 共享资源,引用计数为 2
        assert(sptr.use_count() == 2);
    }
    // sptr1 已经释放
    assert(sptr.use_count() == 1);
}
// sptr离开作用域,use_count为0,自动释放内存。

和 unique_ptr 一样,shared_ptr 也可以指向数组和自定义 deleter。

std::shared_ptr 的实现原理

一个 shared_ptr 对象的内存开销要比裸指针和无自定义 deleter 的 unique_ptr 对象略大:

int main()
{
    std::cout << sizeof(int*) << std::endl;  // 输出 8

    std::cout << sizeof(std::unique_ptr<int>) << std::endl;  // 输出 8
    std::cout << sizeof(std::unique_ptr<FILE, std::function<void(FILE*)>>) << std::endl;  // 输出 40

    std::cout << sizeof(std::shared_ptr<int>) << std::endl;  // 输出 16
    std::shared_ptr<FILE> sptr(fopen("test_file.txt", "w"), [](FILE* fp) {
        std::cout << "close " << fp << std::endl;
        fclose(fp);
    });
    std::cout << sizeof(sptr) << std::endl;  // 输出 16
}

无自定义 deleter 的 unique_ptr 只需要将裸指针用 RAII 的手法封装好就行,无需保存其它信息,所以它的开销和裸指针是一样的。如果有自定义 deleter,还需要保存 deleter 的信息。

shared_ptr 需要维护的信息有两部分:

  1. 指向共享资源的指针。
  2. 引用计数等共享资源的控制信息:实现上是维护一个指向控制信息的指针。

所以,shared_ptr 对象需要保存两个指针。shared_ptr 的 deleter 是保存在控制信息中,所以,是否有自定义 deleter 不影响 shared_ptr 对象的大小。

std::shared_ptr 的初始化

std::shared_ptr<int> p1;             // 不传入任何实参
std::shared_ptr<int> p2(nullptr);    // 传入空指针 nullptr

std::shared_ptr<int> p3(new int(10));

std::shared_ptr<int> p3 = std::make_shared<int>(10);

// 调用拷贝构造函数
std::shared_ptr<int> p4(p3);
std::shared_ptr<int> p4 = p3;
// 调用移动构造函数,p4就会置空
std::shared_ptr<int> p5(std::move(p4));
std::shared_ptr<int> p5 = std::move(p4);

std::shared_ptr 成员函数

  • use_count(): 返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量。
  • unique(): 判断当前 shared_ptr 对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。
  • reset():
    • 无参数:当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针。
    • 有参数:获得该存储空间的所有权,并且引用计数的初始值为 1。
  • get(): 获得 shared_ptr 对象内部包含的普通指针。
  • swap(): 交换 2 个相同类型 shared_ptr 智能指针的内容。

注意事项

  • 不要使用原始指针初始化多个 shared_ptr。
int* p1 = new int;
// 由于p1和p2是两个不同对象,但是管理的是同一个指针,这样会造成空悬指针,
std::shared_ptr<int> p2(p1);
std::shared_ptr<int> p3(p1);
  • 不允许以暴露裸漏的指针进行赋值。
//带有参数的shared_ptr构造函数是explicit类型的,所以不能像这样
std::shared_ptr<int> p1 = new int();// 不能隐式转换,类型不匹配
  • 不要用栈中的指针构造 shared_ptr 对象。
int x = 12;
std::shared_ptr<int> ptr(&x);
  • 不要使用 shared_ptr 的 get()初始化另一个 shared_ptr
Base *a = new Base();
std::shared_ptr<Base> p1(a);
//p1、p2各自保留了对一段内存的引用计数,其中有一个引用计数耗尽,资源也就释放了,会出现同一块内存重复释放的问题
std::shared_ptr<Base> p2(p1.get());
  • 容器中的 shared_ptr 记得用 erease 节省内存。

enable_shared_from_this

一个类的成员函数如何获得指向自身(this)的 shared_ptr:

class Foo {
public:
    std::shared_ptr<Foo> GetSPtr() {
        return std::shared_ptr<Foo>(this);
    }
};

auto sptr1 = std::make_shared<Foo>();
assert(sptr1.use_count() == 1);

auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 1);
assert(sptr2.use_count() == 1);

上面的代码其实会生成两个独立的 shared_ptr,他们的控制块是独立的,最终导致一个 Foo 对象会被 delete 两次。

成员函数获取 this 的 shared_ptr 的正确的做法是继承 std::enable_shared_from_this。

class Bar : public std::enable_shared_from_this<Bar> {
public:
    std::shared_ptr<Bar> GetSPtr() {
        return shared_from_this();
    }
};

auto sptr1 = std::make_shared<Bar>();
assert(sptr1.use_count() == 1);

auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 2);
assert(sptr2.use_count() == 2);

继承了 std::enable_shared_from_this 的子类,成员变量中增加了一个指向 this 的 weak_ptr。这个 weak_ptr 在第一次创建 shared_ptr 的时候会被初始化,指向 this。

继承了 std::enable_shared_from_this 的类都被强制必须通过 shared_ptr 进行管理。

weak_ptr

std::weak_ptr 要与 std::shared_ptr一起使用。

一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期:

  • 如果需要操作weak_ptr正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。
  • 当shared_ptr管理的资源被释放时,weak_ptr会自动变成 nullptr。

当 weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数。

weak_ptr的作用

weak_ptr主要针对shared_ptr的空悬指针和循环引用问题而提出:

  1. 空悬指针问题:有两个指针p1和p2,指向堆上的同一个对象Object,p1和p2位于不同的线程中。假设线程A通过p1指针将对象销毁了(尽管把p1置为了NULL),那p2就成了空悬指针。

    weak_ptr不控制对象的生命期,但是它知道对象是否还活着。如果对象还活着,那么它可以提升为有效的shared_ptr(提升操作通过lock()函数获取所管理对象的强引用指针);如果对象已经死了,提升会失败,返回一个空的shared_ptr。

  2. 循环引用问题。

weak_ptr的初始化

std::weak_ptr<int> wp1;
weak_ptr<int> wp2 (wp1);

// shared_ptr<int> sp (new int);
weak_ptr<int> wp3(sp);

weak_ptr的成员函数

  • use_count():查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
  • expired():判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。
  • lock():如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。
  • reset():将当前 weak_ptr 指针置为空指针。
  • swap():互换 2 个同类型 weak_ptr 指针的内容。