智能指针

引入背景

  • 悬空指针被使用:有些内存资源已经被释放,但指向它的指针并没有改变指向,并且后续还在使用
  • 二次释放:有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃)
  • 堆内存泄露(忘记释放):没有及时释放不再使用的内存资源造成内存泄漏,程序占用的内存资源越来越多;程序发生异常时内存泄露。

unique_ptr

  • 内存布局(sizeof大小为8字节)

unique_ptr内存布局

  • 基本开销成本同裸指针一致,在未自定义删除器操作时

    • 函数对象、lambda(注意捕获效应会导致lambda对象变大)、函数指针、函数适配器std::bind、可调用对象std::function
  • 函数声明

    • template<class T, class Deleter = std::default_delete> class unique_ptr;
    • template<class T, class Deleter = std::default_delete> class unique_ptr<T[], Deleter>;
      • 目的是采用函数重载方式,支持对数组类对象的相应管理
  • 特点

    • 通过RAII机制,实现异常保证
    • 拥有动态生命周期的唯一对象所有权
    • 禁用拷贝,可用移动传递所有权
  • 相关api

    • release暴露相应的裸指针,并且释放所有权
    • reset重置所需管理的所有权
      • 无参数,仅仅是置空
      • 有参数,置空后指向相应的指针
    • swap交换管理的所有权
    • get返回对象的裸指针
    • get_deleter返回对象的析构器
    • make_unique构造一个智能指针对象
  • 推荐采用make_unique

    • 避免因为异常导致的内存泄漏
  • 常见陷阱

    • 使用unique_ptr获取同一个指针的所有权,会导致双重删除
    • unique_ptr(this)会抢夺this所有权,不可取。
    • release接口并没有负责删除
    • get之后,仅是暴露了裸指针,不可转移相应的所有权
    • unique_ptr用在多态基类时,父类必须实现虚析构
  • 使用场景

    • 为动态分配实现异常安全
    • 从函数内返回动态分配的内存(工厂方法)
    • 将动态分配内存的所有权转移给函数
    • 对象中保存多态子对象
    • 容器中保存指针
  • 传参方式

    • 推荐:不涉及所有权,直接传递裸指针void process(widget *);
    • 推荐:获取widget所有权,void process(unique_ptr<widget>)
    • 不推荐:改变unique指向,void process(unique_ptr<widget>&)
    • 不推荐:无意义,void process(const unique_ptr<widget>&)
  • 实战坑点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    struct Node {
    int data;
    std::unique_ptr<Node> next;

    ~Node() {
    cout << "dtor: " << data << endl;
    }
    }

    // 递归导致的嵌套析构
    struct List {
    std::unique_ptr<Node> head;

    void push(int data) {
    head = std::unique_ptr<Node>(new Node{data, std::move(head)});
    }
    ~List() {
    while (head) {
    head = std::move(head->next);
    }
    }
    }


  • 反映出autoptr的缺陷

    • 不支持对数组成员的内存管理
    • 失去所有权而导致访问越界(在函数传参的情形下)
    • 多次析构同一片空间导致内存崩溃
1
2
3
4
std::auto_ptr<Stock> ap(new Stock("hello"));
std::auto_ptr<Stock> ap2 = ap; //compile success, while unique_ptr don't

ap->foo(); //crash because ap is set to NULL

shared_ptr

  • 内存布局(sizeof大小为16字节)

sharedPtr内存布局

  • 由于引用计数块的引入,较之普通的指针,有着一定的成本开销

  • 函数声明

    • template< class T > class shared_ptr;
    • 注意此处较之unique_ptr的异同
      • 二者均可定义删除器,但shared_ptr的删除器并不内嵌在型别之中
      • 需通过构造函数进行相应的定义
      • 原因在于,为了节省shared_ptr内存开销
  • 特点

    • 通过RAII机制,实现异常保证
    • 拥有动态生命周期的共享对象所有权
      • 构造、拷贝 引用计数+1
      • 析构 引用计数-1
    • 拥有循环引用的问题
  • 相关api

    • release暴露相应的裸指针,并且释放所有权
    • reset重置所需管理的所有权
      • 无参数,仅仅是置空,释放引用计数
      • 有参数,置空后指向相应的指针,同时释放引用计数
    • swap交换管理的所有权
    • get返回对象的裸指针
    • use_count获取引用计数
    • unique引用计数是否唯一
    • make_shared构造一个智能指针对象
  • 推荐采用make_shared

    • 拥有更高的安全性
    • 在大多数的情形下拥有着更高的效率
  • 推荐的转换方式

    • static_pointer_cast
    • dynamic_pointer_cast
    • const_pointer_cast
    • reinterpret_pointer_cast
  • 差异

    • 采用非虚的父类析构器,在实现多态的时候,不会报错
    • 其原因在于,其析构的执行根据类型推断
    • unique_ptr不进行类型推断
  • 函数传参方式

1
2
3
4
5
6
// 可行,推荐:成为共享所有权之一
void share(shared_ptr<Widget>)
// 可行,不推荐,可能保留引用计数
void share(const shared_ptr<Widget>&)
// 可行,不推荐,打算重新指向别的对象
void share(shared_ptr<Widget>&)
  • shared_ptr循环引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class A {
public:
std::shared_ptr<B> bptr;
~A() {
cout << "A is deleted" << endl; // 析构函数后,才去释放成员变量
}
};

class B {
public:
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl; // 析构函数后,才去释放成员变量
}
};

int main()
{
std::shared_ptr<A> pa;

{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
return 0;
}
  • 解决方案,将其中一个shared_ptr转换成为weak_ptr

  • weak_ptr简要介绍

    • 核心思想,只负责观察资源的占用情况,不拥有其所有权
    • 通过share_ptr构造weak_ptr
      • weak_ptr拥有引用计数
      • 通过expired判断是否为空
      • 通过use_count获得引用计数
      • 通过expired()判断其是否过期
      • 通过lock()获取被引用对象的shared_ptr
    • unique_ptr可转换成shared_ptr
      • 切记转换时,采用相关右值(std::move
      • 本质原因是unique_ptr不支持拷贝
    • 切记,若采用weak_ptr,需要保障引用计数块的存在
      • 即采用make_shared效率低下情形,由于采用此方案,内存整块分配后,由于weak_ptr存在,不得释放相关内存,原始对象也无法释放
  • shared_ptr局限性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <memory>
#include <iostream>

struct Good: std::enable_shared_from_this<Good> // note: public inheritance
{
std::shared_ptr<Good> getptr() {
return shared_from_this();
}
};

struct Bad
{
std::shared_ptr<Bad> getptr() {
return std::shared_ptr<Bad>(this);
}
~Bad() { std::cout << "Bad::~Bad() called\n"; }
};

int main()
{
//example 1
// Good: the two shared_ptr's share the same object
std::shared_ptr<Good> gp1 = std::make_shared<Good>();
std::shared_ptr<Good> gp2 = gp1->getptr();
std::cout << "gp2.use_count() = " << gp2.use_count() << '\n';

//example 2
// Bad: shared_from_this is called without having std::shared_ptr owning the caller
try {
Good not_so_good;
std::shared_ptr<Good> gp1 = not_so_good.getptr();
} catch(std::bad_weak_ptr& e) {
// undefined behavior (until C++17) and std::bad_weak_ptr thrown (since C++17)
std::cout << e.what() << '\n';
}

//example 3
// Bad, each shared_ptr thinks it's the only owner of the object
std::shared_ptr<Bad> bp1 = std::make_shared<Bad>();
std::shared_ptr<Bad> bp2 = bp1->getptr();
std::cout << "bp2.use_count() = " << bp2.use_count() << '\n';
} // UB: double-delete of Bad
  • 如果直接采用shared_ptr管理this,会造成double_free的错误
    • 主要问题在于,创建了两个控制块,但却管理着同一片内容
    • 较为合理的方案
    • enable_shared_from_this 基本原理
      • 用法:继承这个玩意
        • 不会初始化所需的成员
      • 然后返回,shared_from_this()
      • 注意:若T类型不含有Shared指针,则不生成weak_this指针
  • 管理内部成员的小技巧
1
2
3
4
5
6
// 栈上的对象不能用智能指针管理
shared<Widget> sp(&data);

// 堆上内存的智能指针管理
shared<Widget> sp(&spw->data); // err: 实例可能被销毁
shared<Widget> sp{spw, &spw->data}; // 成员和对象享用统一引用计数块