unique_ptr和shared_ptr及weak_ptr的使用

1. unique_ptr

1.1 初始化方式

  1. 直接初始化
    1
    unique<T> myPtr(new T);  //ok。但不能通过隐式转换来构造,如unique<T> myPtr = new T()。因为unique_ptr构造函数被声明为explicit
  2. 移动构造
    1
    unique<T> myOtherPtr = std::move(myPtr);但不允许复制构造,如unique<T> myOther = myPtr; 因为unique是个只移动类型。
  3. 通过make_unique构造
    1
    unique<T> myPtr = std::make_unique<T>(); //C++14支持的语法。但是make_都不支持添加删除器,或者初始化列表。
  4. 通过reset重置
    1
    如std::unique_ptr up; up.reset(new T());

1.2 指定删除器

  1. unique_ptr<T,D> u1(p,d);删除器是unique_ptr类型的组成部分,可是普通函数指针或lambda表达式。注意,当指定删除器时需要同时指定其类型,即D不可省略。

  2. 使用默认的deleter时,unique_ptr对象和原始指针的大小是一样的。当自定义deleter时,如果deleter是函数指针,则unique_ptr对象的大小为8字节。对于函数对象的deleter,unique_ptr对象的大小依赖于存储状态的多少,无状态的函数对象(如不捕获变量的lambda表达式),其大小为4字节。

1.3 源码分析

  1. unique_ptr的构造函数被声明为explicit,禁止隐式类型转换的行为。原因如下:
    1
    2
    ①可减少误将智能指针指向栈对象的情况。如unique_ptr<int> ui = &i;其中的i为栈变量。
    ②可避免将一个普通指针传递给形参为智能指针的函数。假设,如果允许将裸指针传给void foo(std::unique_ptr<T>)函数,则在函数结束后会因形参超出作用域,裸指针将被delete的误操作。
  2. unique_ptr的拷贝构造和拷贝赋值均被声明为delete。因此无法实施拷贝和赋值操作,但可以移动构造和移动赋值。
  3. 删除器是unique_ptr类型的一部分。默认为std::default_delete,内部是通过调用delete来实现。
  4. unique_ptr可以指向数组,并重载了operator []运算符。如unique_ptr<int[]> ptr(new int[10]); ptr[9]=9;但建议使用使作std::array、std::vector或std::string来代替这种原始数组。

1.4 常用操作

  1. get():返回unique_ptr中保存的裸指针
  2. reset():重置unique_ptr。
  3. release():放弃对指针的控制权,返回裸指针,并将unique_ptr自身置空。通常用来初始化另一个智能指针。
  4. swap(q):交换两个智能指针所指向的对象。

1.5 基本用法示例

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include <iostream>
#include <vector>
#include <memory> //for smart pointer

using namespace std;

class Widget {};

//返回值RVO优化:
unique_ptr<int> func()
{
unique_ptr<int> up(new int(100));
return up; //up是个左值,调用拷贝构造给返回值? No。
//C++标准要求当RVO被允许时,要么消除拷贝,要么隐式地把std::move用在要返回的局部
//对象上去。这里编译器会直接在返回值位置创建up对象。因此根本不会发生拷贝构造,
//unique_ptr本身也不能被拷贝构造。

//return unique_ptr<int>(new int(100)); //右值,被移动构造。
}

void foo(std::unique_ptr<int> ptr)
{
}

void myDeleter(int* p)
{
cout << "invoke deleter(void* p)"<< endl;
delete p;
}

int main()
{
//1. unique_ptr的初始化
//1.1 通过裸指针创建unique_ptr(由于unique_ptr的构造函数是explicit的,必须使用直接初始化,不能做隐式类型转换)
std::unique_ptr<Widget> ptr1(new Widget); //ok; 直接初始化
//std::unique_ptr<Widget> ptr1 = new Widget(); //error。不能隐式将Widget*转换为unqiue_ptr<Widget>类型。

std::unique_ptr<int[]> ptr2(new int[10]); //指向数组

//1.2 通过移动构造
//std::unique_ptr<Widget> ptr3 = ptr1; //error,unique_ptr是独占型,不能复制构造
std::unique_ptr<Widget> ptr3 = std::move(ptr1); //ok,unique_ptr是个只移动类型,可以移动构造
auto ptr4 = std::move(ptr3); //ok, ptr4为unique_ptr<Widget>类型

//1.3 通过std::make_unique来创建
auto ptr5 = std::make_unique<int>(10);

//auto ptr6 = std::make_unique<vector<int>>({1,2,3,4,5}); //error,make_unique不支持初始化列表
auto initList = { 1,2,3,4,5 };
auto ptr6 = std::make_unique<vector<int>>(initList);

//2. 传参和返回值
int* px = new int(0);
//foo(px); //error,px无法隐式转为unique_ptr。可防止foo函数执行完毕后,px会自动释放。
//foo(ptr5); //error,智能指针不能被拷贝。因此,可以将foo的形参声明为引用,以避免所有权转移
foo(std::move(ptr5)); //ok,通过移动构造

auto ptr7 = func(); //移动构造

//3.常用操作
std::unique_ptr<Widget> upw1; //空的unique_ptr
upw1.reset(new Widget);
std::unique_ptr<Widget> upw2(new Widget);

cout <<"before swap..." << endl;
cout << "upw1.get() = " << hex << upw1.get() << endl;

cout << "upw2.get() = " << hex << upw2.get() << endl;

cout << "after swap..." << endl;
upw1.swap(upw2); //交换指针所指的对象
cout << "upw1.get() = " << hex << upw1.get() << endl;
cout << "upw2.get() = " << hex << upw2.get() << endl;

//upw1.release(); //release放弃了控制权不会释放内存,丢失了指针
Widget* pw = upw1.release();//放弃对指针的控制
delete pw; //需手动删除

if (upw1) { //unique_ptr重载了operator bool()
cout << "upw1 owns resourse" << endl;
}else {
cout << "upw1 lost resourse" << endl;
}

upw1.reset(upw2.release()); //转移所有权
cout << "upw1.get() = " << hex << upw1.get() << endl;
cout << "upw2.get() = " << hex << upw2.get() << endl;

//upw1 = nullptr; //释放upw1指向的对象,并将upw1置空
//upw1.reset(nullptr);

//4.unique_ptr的大小
std::unique_ptr<int,decltype(&myDeleter)> upd1(new int(0), myDeleter); //自定义删除器
auto del = [](auto* p) {delete p; };
std::unique_ptr<int, decltype(del)> upd2(new int(0), del);
cout << sizeof(upw1) << endl; //4字节,默认删除器
cout << sizeof(upd1) << endl; //8字节
cout << sizeof(upd2) << endl; //4字节

return 0;
}

1.6 使用场景

1.6.1 作为工厂函数的返回类型

  1. 工厂函数负责在堆上创建对象,但是调用工厂函数的用户才会真正去使用这个对象,并且要负责这个对象生命周期的管理。所以使用unique_ptr是最好的选择。

  2. unique_ptr转为shared_ptr很容易,作为工厂函数本身并不知道用户希望所创建的对象的所有权是专有的还是共享的,返回unique_ptr时调用者可以按照需要做变换。

1.6.2 PImpl机制:(Pointer to Implemention)

  1. 操作方法

  (1)将曾经放在主类中的数据成员放到实现类中去,然后通过指针间接地访问那些数据成员。此时主类中存在只有声明而没有定义的类型(也叫非完整类型),如Widget::Impl。

  (2)在实现类中,动态分配和归还原那些原本应在主类中定义的那数据成员对象。即将这个数据成员放到实现类中定义(动态分配其内存)

  2. 注意事项

  (1)PImpl机制通过降低类的客户和类实现者之间的依赖性,减少了构建遍数。

  (2)对于采用std::unique_ptr来实现的PImpl指针,须在类的头文件中声明特殊成员函数,但在实现文件中实现它们(注意,不能直接在头文件中实现,具体原因见《编程实验》中的说明)。如,必须同时声明并实现类的析构函数。再由于自定义了析构函数,编译器不再提供默认的移动构造和移动赋值函数,如果需要这些函数,则也必须在头文件中声明,并在实现类中去实现。

  (3)上述建议仅适用于std::unique_ptr,但并不适用于std::shared_ptr。因为删除器在unique_ptr中是其类型的一部分,而在shared_ptr中则不是。声明对象时,unique_ptr支持T是个非完整类型,但在析构时T必须己经是个完整的类型。unique_ptr析构时会先判断T是否为完整类型再调用delete删除其所指对象,但shared_ptr则不会。

16.3 场景实验

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
43
44
45
46
47
48
49
50
51
#ifndef  _WIDGET_H_
#define _WIDGET_H_
#include <memory>

//1.传统的做法
//问题:数据成员会导致Widget.h文件必须include <string>
// <vector>和gadget.h。当客户包含Widget.h里,会增加编译时间,而且
// 如果其中的某个头文件(如Gadget.h)发生改变,则Widget的客户必须重新编译!
//class Widget
//{
// std::string name;
// std::vector<double> data;
// Gadget g1, g2, g3;// //自定义类型,位于gadget.h。
//public:
// Widget();
//};

//2. 采用PImpl手法
class Widget
{
//声明实现结构体以及指向它的指针
struct Impl; //注意只有声明,没实现。是个非完整类型。
std::unique_ptr<Impl> pImpl; //使用智能指针而非裸指针。这里声明一个指针非完整类型的指针。注意针对非完整
//类型,可以做的事情极其有限。由于unique_ptr中会将删除器作为其类型的一部分
//因此,但unique_ptr析构被调用时,当delete其所指对象时,会先判断T是否是个完
//整类型。如果不是,则会报错。因此必须在pImpl被析构前,确保Impl被定义(即是个完整类型)
//因此,使用unique_ptr<非完整类型时>,必须为该类同时定义析构函数!具体原因见后面的分析。

//std::shared_ptr<Impl> pImpl; //由于删除器不是shared_ptr类型的组成部分。当pImpl被析构时,不会判断T是否为完整类型。
//因此,不要求Widget必须自定义析构函数。

public:
Widget();
~Widget(); //Impl是个非完整类型,这里必须声明析构函数,并在Widget.cpp中实现它。
//注意,不能在该文件中实现,因为此时unique_ptr看到的Impl是个非完整类型,unique_ptr内部要求delete前,其
//其指向的必须是个完整类的指针。

//移动构造和移动赋值(由于自定义了析构函数,所以编译器不再提供默认的移动构造和移动赋值函数,这里需手动填加)
Widget(Widget&& rhs); //只能声明,须放在.cpp中去实现。编译器会在move构造函数内抛出异常的事件中生成析构pImpl代码,
//而此处Impl为非完整类型。
Widget& operator=(Widget&& rhs); //只能声明,须放在.cpp中去实现。因为移动赋值pImpl时,需要先析构pImpl所指对象,但
//此时仍为非完整类型。

//让Widget支持复制操作。注意unique_ptr不可复制
Widget(const Widget& rhs); //仅声明
Widget& operator=(const Widget& rhs); //仅声明
};

#endif // ! _WIDGET_H_

Widget.h
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
#include "Widget.h"

//将对string和vector和Gadget头文件的依赖从Wigdget.h转移动Wigdget.cpp文件中。如此,Widget类的使用者
//只需依赖Widget.h,而把复杂的依赖关系留给Widget的实现者(Widget.cpp)去处理
#include <string>
#include <vector>
class Gadget {}; //本应#include "Gardget.h",但为了简明起见,就直接在这里声明该类

//Widget::Impl的实现(包括此前在Widget中的数据成员)
struct Widget::Impl
{
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};

Widget::Widget():pImpl(std::make_unique<Impl>())
{}

//注意:析构函数必须在Widget::Impl类之后定义。因为此时调用~Widget时,会调用unique_ptr的析构函数
//而unique_ptr中会调用delete删除其指向的对象,由于~Widget定义在Widget::Impl之后,因此这时看到的
//Impl是个完整的类,delete前通过了unique_ptr内部完整类型的判断!
Widget::~Widget() {}//或Widget::~Widget = default;

Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;

//make_unique(Ts&&... params)== std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
Widget::Widget(const Widget& rhs):pImpl(std::make_unique<Impl>(*rhs.pImpl))//深拷贝!
{
}

Widget& Widget::operator=(const Widget& rhs)
{
*pImpl = *rhs.pImpl; //深拷贝!复制两个指针所指向的内容。pImpl本身是只移动类型
return *this;
}
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include <memory>
#include <functional>
#include "Widget.h"
using namespace std;

enum class InvestmentType {itSock, itBond, itRealEstate};
class Investment//投资
{
public:
virtual ~Investment() {} //声明为virtual,以便正确释放子类对象
};

class Stock : public Investment {};//股票
class Bond : public Investment {}; //债券
class RealEstate : public Investment {}; //不动产

void makeLogEntry(Investment* pInvmt) {}

//工厂函数
template<typename... Ts>
auto makeInvestment(Ts&&... params) //返回unique_ptr智能指针
{
//自定义deleter
auto delInvmt = [](Investment* pInvmt) //父类指针
{
makeLogEntry(pInvmt);
delete pInvmt; //delete父类指针,所有析构函数须声明为virtual
};

std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);

if (1/*a Stock Object should be created*/) {
pInv.reset(new Stock(std::forward<Ts>(params)...)); //原始指针无法隐式转为unique_ptr,使用reset重置所有权
}
else if (0/*a Bond Object should be created*/)
{
pInv.reset(new Bond(std::forward<Ts>(params)...));
}
else if (0/*a RealEstate should be created*/)
{
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}

return pInv;
}


int main()
{
//1. unique_ptr作为工厂函数的返回值。
std::shared_ptr<Investment> sp = makeInvestment(); //从std::unique_ptr转换到std::shared_ptr(从独占到共享的
//转换简单而高效)

//2. PImpl手法的测试
Widget w; //注意Widget的析构函数必须手动实现。否则,则当w析构时编译器会将默认的析构函数inline
//到这里来,但由于include widget.h在inline动作之前,此时编译器看到的是非完整类型的
//Impl类。因此Widget类中的unique_ptr析构时,delete前检查出是个非完整类指针,从而报错。
}

2. shared_ptr

2.1 shared_ptr 的基本用法

2.1.1 与unique_ptr的比较

比较 shared_ptr unique_ptr 备注
初始化 ①shared_ptr sp; sp.reset(new T());②shared_ptr sp(new T());③shared_ptr sp1 = sp; **//**拷贝构造④auto sp = make_shared(10); ①unique_ptr up; up.reset(new T());②unique_ptr up(new T());③unique_ptr up1 = std::move(up);**//**移动构造④auto up = make_unique(10); 两者的构造函数将声明为explicit,即不允许隐式类型转换,如shared_ptr sp = new int(10);
条件判断 如,if(sp){…} 如,if(up){…} 两都均重载operator bool()
解引用 *sp *up 解引用,获得它所指向的对象
->mem sp->mem up->mem 重载->运算符
get() sp.get() up.get() 返回智能指针中保存的裸指针,要小心使用。
p.swap(q) sp.swap(q); up.swap(q); 交换p和q指针
独有操作 ①shared_ptr p(q);//拷贝构造②p = q;//赋值③p.unique();若p.use_count()为1,返回true,否则返回false。④p.use_count()//返回强引用计数 ①up=nullptr;释放up指向的对象,并将up置空。②up.release();//up放弃对指针的控制权,返回裸指针,并将up置空③up.reset();释放up指向的对象。 up.reset(q);其中q为裸指针。令up指向q所指对象。 up.reset(nullptr);置空 注意:①unique_ptr不可拷贝和赋值,但可以被移动。②release会切断unique_ptr和它原来管理的对象间的联系。通常用来初始化另一个智能指针

2.1.2 指定删除器

  1. shared_ptr sp1(q, deleter1);与unique_ptr不同,删除器不是shared_ptr类型的组成部分。假设,shared_ptr sp2(q,deleter2),尽管sp1和sp2有着不同的删除器,但两者的类型是一致的,都可以被放入vector<shared_ptr>类型的同一容器里。

  2. 与std::unique_ptr不同,自定义删除器不会改变std::shared_ptr的大小。其始终是祼指针大小的两倍

  3. 当使用shared_ptr管理动态数组时,需要指定删除器。因为默认删除器不支持数组对象。如shared_ptr sp(new int[10], std::default_delete<**int[]**>);

    数组删除器示例1:

    1
    2
    3
    4
    5
    6
    int main()
    {
    shared_ptr<int> ptr(new int[10], default_delete<int[]>());
    // shared_ptr<int> ptr(new int[10], [](int* p) {delete[]p; });
    return 0;
    }

    对象删除器示例2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <iostream>
    #include <memory>
    using namespace std;
    // 自定义删除器函数,释放int型内存
    void deleteIntPtr(int* p)
    {
    delete p;
    cout << "int 型内存被释放了...";
    }
    int main()
    {
    shared_ptr<int> ptr(new int(250), deleteIntPtr);
    // 删除器为 lambda 表达式
    // shared_ptr<int> ptr(new int(250), [](int* p) {delete p; });
    return 0;
    }
  4. 删除器可以是普通函数、函数对象和lambda表达式等。默认的删除器为std::default_delete,其内部是通过delete来实现功能的。

2.2 剖析 std::shared_ptr

2.2.1 std::shared_ptr 的内存模型

  1. shared_ptr包含了一个指向对象的指针和一个指向控制块的指针。每一个由std::shared_ptr管理的对象都有一个控制块,它除了包含引用计数之外,还包含了自定义删除器的副本和分配器的副本以及其他附加数据
  2. 控制块的创建规则:
  (1)std::make_shared总是创建一个控制块

  (2)从具备所有权的指针出发构造一个std::shared_ptr时,会创建一个控制块。(如std::unique_ptr转为shared_ptr时会创建控制块,因为unique_ptr本身不使用控制块,同时unique_ptr置空)

  (3)当std::shared_ptr构造函数使用裸指针作为实参时,会创建一个控制块。这意味从同一个裸指针出发来构造不止一个std::shared_ptr时会创建多重的控制块,也意味着对象会被析构多次。如果想从一个己经拥有控制块的对象出发创建一个std::shared_ptr,可以传递一个shared_ptr或weak_ptr而非裸指针作为构造函数的实参,这样则不会创建新的控制块。

  3. 引用计数(强引用计数)

  (1)shared_ptr的构造函数会使该引用计数递增,而析构函数会使该计数递减。但移动构造时表示从一个己有的shared_ptr移动构造到一个新的shared_ptr。这意味着一旦新的shared_ptr产生后,原有的shared_ptr会被置空,其结果是引用计数没有变化。

  (2)复制赋值同时执行两种操作(如sp1 和sp2是指向不同对象的shared_ptr,则sp1 = sp2时,将修改sp1使得其指向sp2所指的对象。而最初sp1所指向的对象的引用计数递减,同时sp2所指向的对象引用计数递增)

  (3)reset函数,如果不带参数时,则引用计数减1。如果不带参数时,如sp.reset(p)则sp原来指向的对象引用计数减1,同时sp指向新的对象(p)

  (4)如果实施一次递减后最后的引用计数变成0,即不再有shared_ptr指向该对象,则会被shared_ptr析构掉

  (5)引用计数的递增和递减是原子操作,即允许不同线程并发改变引用计数。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <iostream>
#include <vector>
#include <memory> // for smart pointer

using namespace std;

class Widget{};

void func(shared_ptr<Widget> sp){}

int funcException() { /*throw 1;*/ return 0; } //假设该函数会抛出异常

void demo(shared_ptr<int> sp, int f){}

int main()
{
//1. 陷阱:用同一裸指针创建多个shared_ptr
//1.1 错误做法
auto pw = new Widget;
std::shared_ptr<Widget> spw1(pw); //强引用计数为1,为pw创建一个控制块
//std::shared_ptr<Widget> spw2(pw); //强引用计数为1,为pw创建另一个新的控制块,会导致多次析构

auto sp = new Widget;
func(shared_ptr<Widget>(sp)); //慎用裸指针,sp将在func结束后被释放!

//1.2 正确做法
std::shared_ptr<Widget> spw3(spw1); //ok,pw的强引用计数为2。使用与spw1同一个控制块。
std::shared_ptr<Widget> spw4(new Widget); //将new的结果直接传递给shared_ptr
std::shared_ptr<Widget> spw5 = std::make_shared<Widget>(); //强烈推荐的做法!

//2. 陷阱:在函数实参中创建shared_ptr
//2.1 shared_ptr与异常安全问题
//由于参数的计算顺序因编译器和调用约定而异。假定按如下顺序计算
//A.先前new int,然后funcException();
//B.假设恰好此时funcException产生异常。
//C.因异常出现shared_ptr还来不及创建,于是int内存泄露
demo(shared_ptr<int>(new int(100)), funcException());

//2.2 正确做法
auto p1 = std::make_shared<int>(100);
demo(p1, funcException());

//3. 陷阱:shared_ptr的循环引用(应避免)(见第22课 weak_ptr)

//4. 删除器
auto deleter1 = [](Widget* pw) {cout << "deleter1"<< endl; delete pw; };
auto deleter2 = [](Widget* pw) {cout << "deleter2"<< endl; delete pw; };

std::shared_ptr<Widget> pw1(new Widget, deleter1);
std::shared_ptr<Widget> pw2(new Widget, deleter2);

std::shared_ptr<Widget> pw3(pw1);
pw3.reset(new Widget); //deleter恢复为默认的std::default_delete

vector<std::shared_ptr<Widget>> vecs;
vecs.emplace_back(pw1);
vecs.emplace_back(pw2); //pw1和pw2虽然有不同的删除器,但类型相同,可以放入同一容器内。

//5. 其它
//5.1 shared_ptr的大小
cout << sizeof(spw1) << endl;//8
cout << sizeof(pw1) << endl; //8
//5.2 shared_ptr管理动态数组(建议用std::array、std::vector取代)
std::shared_ptr<int> pArray1(new int[10], [](int* p) {delete[] p; }); //使用delete[]
std::shared_ptr<int> pArray2(new int[10], std::default_delete<int[]>()); //使用default_delete<int[]>()
//5.3 常见操作
cout << pw1.use_count() << endl; //2

if (pw1) //pw1.use_count >= 1 ?
{
cout << "pw1.use_count >= 1" << endl;
}
else
{
cout << "pw1.use_count == 0" << endl;
}
//5.4 别名构造
int* p = new int(10);

std::shared_ptr<int> a(new int(20));
std::shared_ptr<int> b(a, p); // alias constructor: co-owns a, points to p。可用于多继承中
// a 和 b拥用相同的控制块,但两者指向的对象不同。由于两者拥用相同的
//的控制块,可认为a和b所指对象具有相同的拥有者,因此10和20两个堆对象
//拥有相同的生命期
cout << *a << endl; //20
cout << *b << endl; //10

return 0;
}

2.2.2 enable_shared_from_this 模板的分析

(一)模板分析(以boost::enable_shared_from_this为例)

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
43
44
45
46
47
48
49
50
51
52
53
54
template<class T> class enable_shared_from_this
{
protected:

enable_shared_from_this() BOOST_NOEXCEPT
{
}

enable_shared_from_this(enable_shared_from_this const &) BOOST_NOEXCEPT
{
}

enable_shared_from_this & operator=(enable_shared_from_this const &) BOOST_NOEXCEPT
{
return *this;
}

~enable_shared_from_this() BOOST_NOEXCEPT // ~weak_ptr<T> newer throws, so this call also must not throw
{
}

public:

shared_ptr<T> shared_from_this()
{
shared_ptr<T> p( weak_this_ );
BOOST_ASSERT( p.get() == this );
return p;
}

shared_ptr<T const> shared_from_this() const
{
shared_ptr<T const> p( weak_this_ );
BOOST_ASSERT( p.get() == this );
return p;
}

public: // actually private, but avoids compiler template friendship issues

// Note: invoked automatically by shared_ptr; do not call
template<class X, class Y> void _internal_accept_owner( shared_ptr<X> const * ppx, Y * py ) const
{
if( weak_this_.expired() )
{
weak_this_ = shared_ptr<T>( *ppx, py );
}
}

private:

mutable weak_ptr<T> weak_this_;
};

boost::enable_shared_from_this
  1. enable_shared_from_this模板类提供两个public属性的shared_from_this成员函数。这两个函数内部会通过weak_this_(weak_ptr类型)成员来创建shared_ptr。

  2. internal_accept_owner函数不能手动调用,这个**函数会被shared_ptr自动调用,该函数是用来初始化唯一的成员变量weak_this**。

  3. 根据对象生成顺序,先初始化基类enable_shared_from_this,再初始化派生类对象本身。这时对象己经生成,但weak_this_成员还未被初始化,最后应通过shared_ptr sp(new T())等方式调用shared_ptr构造函数(内部会调用_internal_accept_owner)来初始化weak_this_成员。而如果在调用shared_from_this函数之前weak_this_成员未被初始化,则会通过ASSERT报错提示。

** 使用说明**

  1. 基类必须为enable_shared_from_this,其中T为派生类的类名。(这种方法叫奇妙递归模板模式)
  2. 通过调用shared_from_this()成员函数获得一个和this指针指向相同对象的shared_ptr。
  3. 从内部实现看,shared_from_this会查询当前对象的控制块,并创建一个指向该控制块的新shared_ptr。这样的设计就要求当前对象己有一个与其关联的控制块。为了实现这一点,就必须有一个己经存在指向当前对象的std::shared_ptr,如果不存在,则通常shared_from_this会抛出异常。
  4. 从enable_shared_from_this继承的类,在构造函数中不能调用shared_from_this()获取智能指针,因为构造过程中weak_this_成员还未被初始化,所以会报异常 what(): bad_weak_ptr

安全地从this指针创建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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#include <iostream>
#include <vector>
#include <memory>

using namespace std;

//1. 从this指针创建shared_ptr
//1.1 错误的做法
class Test1
{
public:
//析构函数
~Test1() { cout <<"~Test1()" << endl; }

//获取指向当前对象的指针
std::shared_ptr<Test1> getObject()
{
shared_ptr<Test1> pTest(this); //危险! 直接从this指针创建,会为this对象创建新的控制块!
//从而可能导致this所指对象被多次析构
return pTest;
}
};

//1.2 正确的做法
class Test2 : public std::enable_shared_from_this<Test2> //继承! 注意Test2为基类的模板参数 (递归模板模式)
{
public:
//析构函数
~Test2() { cout << "~Test2()" << endl; }

std::shared_ptr<Test2> getObject()
{
return shared_from_this(); //调用enable_shared_from_this模板的成员函数,获取this对象的shared_ptr
}
};

//2. shared_from_this函数的正确调用
//2.1 一般做法
class Test3 : public std::enable_shared_from_this<Test3>
{
public:
//构造函数中不能使用shared_from_this
Test3()
{
//std::shared_ptr<Test3> sp = shared_from_this(); //error,此时基类(enable_shared_from_this<Test3>)
//虽己构造完,但shared_ptr的构造函数还没被调用,weak_this_指针
//未被初始化,因此调用shared_from_this会抛出异常
}

//调用process之前,必须确保shared_ptr的构造函数己被执行(即weak_this_被初始化)
void process()
{
std::shared_ptr<Test3> sp = shared_from_this();
}
};

//2.2 改进做法:利用工厂函数来提供shared_ptr
class Test4 : public std::enable_shared_from_this<Test4>
{
Test4() {} //构造函数设为private
public:

//提供工厂函数
template<typename... Ts>
static std::shared_ptr<Test4> create(Ts&& ... params)
{
std::shared_ptr<Test4> ret(new Test4(params...));
return ret;
}

void process()
{
std::shared_ptr<Test4> sp = shared_from_this();
}
};

//3. enable_shared_from_this的应用举例
class Widget;
std::vector<std::shared_ptr<Widget>> processWidgets; //记录己被处理过的Widgets

class Widget : public std::enable_shared_from_this<Widget> //需要从这里继承
{
public:
void process()
{
//错误做法:直接将this传给shared_ptr<Widget>
//processWidgets.emplace_back(this); //将处理完的Widget加入链表。
//error,这种做法本质上是用裸指针来创建shared_ptr,会为this对象创建
//新的控制块。如果外部new Widget时,也将指针交给shared_ptr管理时,会出现为同
//一个this对象创建多个控制块,从而造成this对象的多次析构!

//正确做法:(为了确保shared_from_this在shared_ptr构造函数后被调用,可以采用工厂函数的方式来创建Widget,
//具体见前面的例子)
processWidgets.emplace_back(shared_from_this()); //将指向当前对象的shared_ptr加入到链表中
}

~Widget() { cout <<"~Widget()" << endl; }
};

int main()
{
//1. 从this指针创建shared_ptr
//1.1 错误做法:对象被多次析构
{
//std::shared_ptr<Test1> pt1(new Test1());
//std::shared_ptr<Test1> pt2 = pt1->getObject();
}

//1.2 正确做法
{
std::shared_ptr<Test2> pt1(new Test2());
std::shared_ptr<Test2> pt2 = pt1->getObject();
}

//2. shared_from_this的正确调用
{
//2.1 错误方法:
Test3 t;
//t.process(); //错误,shared_ptr构造函数没有被执行

Test3* pt = new Test3();
//pt->process(); //错误,原因同上。
delete pt;

//正确做法
std::shared_ptr<Test3> spt(new Test3); //shared_ptr构造被执行,weak_this_被正确初始化
spt->process();

//2.2 工厂方法提供shared_ptr,确保shared_ptr构造函数被执行!
std::shared_ptr<Test4> spt2 = Test4::create();
spt2->process();
}

//3. enable_shared_from_this的应用举例
{
std::shared_ptr<Widget> sp(new Widget);
sp->process();
}

return 0;
}

2.3 dynamic_pointer_cast 的使用

  1. dynamic_pointer_cast用于将子类的shared_ptr对象转为父类的 shared_ptr 对象,成功转换后的对象new_base和原始父对象base共享引用计数.
  2. 成功转后base对象的引用计数加一.
  3. 转换后的new_base对象的reset将导致原始对象的引用计数减一.且new_base的内容为NULL
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
#include <iostream>
#include <stdio.h>
#include <memory>

class base
{
public:
base (){ }
virtual ~base () { }
};

class classC : public base
{
public:
classC() {}
virtual ~classC () {}
};

void test_cast_fun(std::shared_ptr<base> new_base, std::shared_ptr<classC> &child) {
printf("new_base.count=%d, child.count=%d\n", new_base.use_count(), child.use_count());
new_base.reset();
printf("after new_base.reset new_base.count=%d, child.count=%d, new_base=%s \n", new_base.use_count(), child.use_count(), NULL==new_base?"NULL":"NOT_NULL");
}

int main(int , char *[])
{
auto c = std::make_shared<classC>();
printf("dynamic_pointer_cast 前 child.count=%d\n", c.use_count());
test_cast_fun(std::dynamic_pointer_cast<base>(c), c);
printf("执行 reset 后 child=%s, c.count=%d\n", c==NULL?"NULL":"NOT NULL", c.use_count());
return 0;
}

输出结果为

1
2
3
4
dynamic_pointer_cast 前 child.count=1
new_base.count=2, child.count=2
after new_base.reset new_base.count=0, child.count=1, new_base=NULL
执行 reset 后 child=NOT NULL, c.count=1

2.4 使用总结

  1. 尽可能避免将裸指针传递给一个std::shared_ptr的构造函数,常用的替代手法是使用std::make_shared。
  2. 如果必须将一个裸指针传递给shared_ptr的构造函数,就直接传递new运算符的结果,而非传递一个裸指针变量。如shared_ptr spw (new Widget, logginDel);
  3. 不要将this指针返回给shared_ptr。当希望将this指针托管给shared_ptr时,类需要继承自std::enable_shared_from_this,并且从shared_from_this()中获得shared_ptr指针。
  4. 自定义删除器的shared_ptr对象在销毁函数中不能再调用自己的 shared_from_this 函数, 否则会引起崩溃
  5. 不建议使用malloc()分配内存来创建shared_ptr(或任何包含构造函数的对象),因为构造函数不会被调用:malloc() 仅分配内存,不会调用对象的构造函数。因此,如果使用 malloc() 创建包含 shared_ptr 或其他 C++ 对象的结构体,这些对象将不会被正确初始化。new 操作符则确保调用了构造函数,这样对象就可以被正确初始化。
  6. 可以将shared_ptr 对象直接传递给函数参数const sahred_ptr & 来防止对象和对象内容被修改
  7. 当shared_ptr对象作为set存储内容时,可以按以下方式来实现按内容的opereator<排序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template <typename T>
    struct set_shardptr_compare {
    bool operator()(const std::shared_ptr<T>& lhs, const std::shared_ptr<T>& rhs) const {
    return *lhs < *rhs;
    }
    };

    typedef std::set<std::shared_ptr<xxx>, set_shardptr_compare<xxx>> SET_SHARE;

    SET_SHARE m_; // set存储xxx类型shared_ptr内容, 并按存储内容operator <实现排序

3. weak_ptr

3.1. weak_ptr的概况

3.1.1 weak_ptr的创建

  1. 直接初始化:weak_ptr wp(sp); //其中sp为shared_ptr类型

  2. 赋值: wp1 = sp; //其中sp为shared_ptr类型

      wp2 = wp1; //其中wp1为weak_ptr类型

3.1.2 常用操作

  1. use_count():获取当前控制块中资源的强引用计数。

  2. expired():判断所观测的资源是否失效(即己经被释放),即use_count是否为0。

  (1)shared_ptr sp1 = wp.lock();//如果wp失效,则sp为空(其中wp为weak_ptr类型)

  (2)shared_ptr sp2(wp); //如果wp失效,则抛std::bad_weak_ptr异常

  3. lock():获取所监视资源的shared_ptr,如shared_ptr sp = wp.lock(); //wp为weak_ptr类型。

  4. reset():重置weak_ptr,影响弱引用计数

3.1.3 注意事项

  1. weak_ptr不是独立的智能指针,它是shared_ptr的助手,只是监视shared_ptr管理的资源是否释放,不会影响强引用计数,不能管理资源。

  2.weak_ptr**没有重载操作符*和->**,因为它不共享指针,不能操作资源。

  3.weak_ptr主要用来代替可能空悬的shared_ptr

weak_ptr示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>

using namespace std;

int main()
{
auto sp1 = make_shared<int>(10);
weak_ptr<int> wp(sp1); //通过shared_ptr初始化
weak_ptr<int> wp1, wp2;
wp1 = sp1; //利用shared_ptr来赋值
wp2 = wp; //利用weak_ptr赋值
auto sp2 = wp2.lock(); //sp2为shared_ptr类型

sp1 = nullptr;

cout << wp2.use_count() << endl; //1,强引用计数
return 0;
}

3.2 weak_ptr的应用

3.2.1 缓存对象

  1. 考虑一个工厂函数loadWidget,该函数基于唯一ID来创建一些指向只读对象的智能指针。

  2. 假设该只读对象需要被频繁使用,而且经常需要从文件或数据库中加载。那么可以考虑将对象缓存起来。同时为了避免过量缓存,当不再使用时,则将该对象删除。

  3. 由于带缓存,工厂函数返回unique_ptr类型显然不合适。因为调用者和缓存管理器均需要一个指向这些对象的指针。

  4. 当用户用完工厂函数返回的对象后,该对象会被析构,此时相应的缓存条目将会空悬。因为可以考虑将工厂函数的返回值设定为shared_ptr类型,而缓存类型为weak_ptr类型

3.2.2 观察者模式

  1. 观察者模式是在subject状态发生改变时,通知观察者的一种设计模式。

  2. 在多数实现中,每个subject持有指向观察者的指针,这使得当subject状态改变时可以很容易通知观察者。

  3. subject不会控制其观察者的生存期,因此应该是持有观察者的weak_ptr指针**。同时在subject的使用某个指针时,可以先确定是否空悬。

3.2.3 解决循环引用

  1. A、B、C三个对象的数据结构中,A和C共享B的所有权,因此各持有一个指向B的std::shared_ptr;

  2. 假设有一个指针从B指回A(即上图中的红色箭****头),则该指针的类型应为weak_ptr,而不能是裸指针或shared_ptr,原因如下:

   ①假如是裸指针,当A被析构时,由于C仍指向B,所以B会被保留。但B中保存着指向A的空悬指针(野指针),而B却检测不出来,但解引用该指针时会产生未定义行为。

   ②假如是shared_ptr时。由于A和B相互保存着指向对方的shared_ptr,此时会形成循环引用,从而阻止了A和B的析构。

   ③假如是weak_ptr,这可以避免循环引用。假设A被析构,那么B的回指指针会空悬,但B可以检测到这一点,同时由于该指针是weak_ptr,不会影响A的强引用计数,因此当shared_ptr不再指向A时,不会阻止A的析构。

3.2.4监视this智能指针

见shared_ptr中的enable_shared_from_this,其中的weak_this_指针即为weak_ptr类型,用于监视this指针。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <iostream>
#include <memory> //for smart pointer
#include <unordered_map> //for unordered_map
#include <set>

using namespace std;

class Widget
{
public:
Widget(int id):ID(id){}

int ID;
};

//1. 利用weak_ptr来缓存对象
//模拟从数据库中加载,并创建shared_ptr指向widget对象
shared_ptr<Widget> loadWidget(int WidgetID)
{
return make_shared<Widget>(WidgetID);
}

//带缓存的工厂函数
std::shared_ptr<const Widget> fastloadWidget(int WidgetID) //返回shared_ptr类型
{
//缓存:weak_ptr类型
static std::unordered_map<int, std::weak_ptr<const Widget>> cache;

auto objPtr = cache[WidgetID].lock(); //objPtr的类型为shared_ptr,指向缓存的对象

if (!objPtr) { //如果对象不在缓存中. 这里省略了缓存中因失效而不断累积std::weak_ptr的处理。
objPtr = loadWidget(WidgetID);
cache[WidgetID] = objPtr;
}

return objPtr;
}

//2. 观察者模式
//2.1 观察者
class WeatherObservers //抽象观察者
{
public:
virtual void updateWeatherInfo(int num) = 0;
};
//机场:具体观察者
class Airport : public WeatherObservers
{
public:
void updateWeatherInfo(int num) override
{
std::cout <<"Airport: " << num << endl;
}
};
//学校:具体观察者
class School : public WeatherObservers
{
public:
void updateWeatherInfo(int num) override
{
std::cout << "School: " << num << endl;
}
};

//2.1 主题(气象站)
class WeatherStation
{
using ObserverPtr = std::weak_ptr<WeatherObservers>; //弱引用

//set集合中保存观察者的弱引用(以ObserverPtr为关键字,基于ownership排序)
using ObserverList = std::set<ObserverPtr, std::owner_less<ObserverPtr>>;

ObserverList obs; //保存所有观察者
public:
//注册观察者
void registerObserver(const ObserverPtr oPtr)
{
if (obs.find(oPtr) == obs.end()) {
obs.insert(oPtr);
}
}
//注销观察者
void unregisterObserver(const ObserverPtr oPtr) //oPtr为weak_ptr类型
{
if (obs.find(oPtr) != obs.end())
{
obs.erase(oPtr);
}
}

//通知各个观察者
void notifyObservers(int num)
{
std::shared_ptr<WeatherObservers> tempPtr;
for (auto& ob : obs)
{
if ((tempPtr = ob.lock())) {
tempPtr->updateWeatherInfo(num);
}
}
}
};


int main()
{
//观察者模式
WeatherStation station;
std::shared_ptr<Airport> airport(new Airport());
std::shared_ptr<School> school(new School());

station.registerObserver(airport);
station.registerObserver(school);

station.notifyObservers(1);

station.unregisterObserver(school);
station.notifyObservers(2);

return 0;
}
/*输出结果
Airport: 1
School: 1
Airport: 2
*/

参考

https://www.cnblogs.com/5iedu/p/11622401.html
https://www.cnblogs.com/5iedu/p/11623757.html
https://www.cnblogs.com/5iedu/p/11619357.html


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!