C++ 智能指针
在介绍智能指针之前,先来看原始指针的一些不便之处:
-
它的声明不能指示所指到底是单个对象还是数组。
-
它的声明没有告诉你用完后是否应该销毁它,即指针是否拥有所指之物。
-
如果你决定你应该销毁指针所指对象,没人告诉你该用
delete
还是其他析构机制(比如将指针传给专门的销毁函数)。 -
如果你发现该用
delete
。第一点说了可能不知道该用单个对象形式(“delete
”)还是数组形式(“delete[]
”)。如果用错了结果是未定义的。 -
假设你确定了指针所指,知道销毁机制,也很难确定你在所有执行路径上都执行了恰为一次销毁操作(包括异常产生后的路径)。少一条路径就会产生资源泄漏,销毁多次还会导致未定义行为。
-
一般来说没有办法告诉你指针是否变成了悬空指针(dangling pointers),即内存中不再存在指针所指之物。在对象销毁后指针仍指向它们就会产生悬空指针。
Item 18: Use std::unique_ptr for exclusive-ownership resource management(对于独占资源使用std::unique_ptr)
1. 性能
可以合理假设,默认情况下,std::unique_ptr
大小等同于原始指针,而且对于大多数操作(包括取消引用),他们执行的指令完全相同。这意味着你甚至可以在内存和时间都比较紧张的情况下使用它。如果原始指针够小够快,那么std::unique_ptr
一样可以。
2. 用法
2.1 拷贝与移动
std::unique_ptr
体现了专有所有权(exclusive ownership)语义。
-
移动一个
std::unique_ptr
将所有权从源指针转移到目的指针。(源指针被设为null。) -
拷贝一个
std::unique_ptr
是不允许的,因为如果你能拷贝一个std::unique_ptr
,你会得到指向相同内容的两个std::unique_ptr
,每个都认为自己拥有(并且应当最后销毁)资源,销毁时就会出现重复销毁。
因此,std::unique_ptr
是一种只可移动类型(move-only type)。
2.2 构造方式
// 1.
std::unique_ptr<int> sp1(new int(123));
// 2.
std::unique_ptr<int> sp2;
sp2.reset(new int(123));
// 3.
std::unique_ptr<int> sp3 = std::make_unique<int>(123);
/* 尝试将原始指针(比如new创建)赋值给std::unique_ptr通不过编译,因为是一种从原始指针到智能指针的隐式转换。
这种隐式转换会出问题,所以 C++11的智能指针禁止这个行为。这就是通过reset来让up接管通过new创建的对象的所有权的原因。*/
unique_ptr<int> up = nullptr;
int* ip = new int();
// up = ip; // 报错: no operator "=" matches these operands
up.reset(ip);
C++ 14 加入了
std::make_unique
,而C++ 11没有
2.3 释放
默认情况下,资源析构通过对std::unique_ptr
里原始指针调用delete
来实现。
但是在构造过程中,std::unique_ptr
对象可以被设置为使用(对资源的)自定义删除器:当资源需要销毁时可调用的任意函数(或者函数对象,包括lambda表达式): std::unique_ptr<T, DeletorFuncPtr>
#include <iostream>
#include <memory>
class Socket
{
public:
Socket() {}
~Socket() {}
//关闭资源句柄
void close()
{
...
}
};
int main()
{
auto deletor = [](Socket* pSocket) {
//关闭句柄
pSocket->close();
delete pSocket;
};
std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor); // 返回类型大小是Socket*的大小
return 0;
}
注意:
上面谈到,当使用默认删除器时(如delete
),你可以合理假设std::unique_ptr
对象和原始指针大小相同。当自定义删除器时,情况可能不再如此。函数指针形式的删除器,通常会使std::unique_ptr
的从一个字(word)大小增加到两个。对于函数对象形式的删除器来说,变化的大小取决于函数对象中存储的状态多少,无状态函数(stateless function)对象(比如不捕获变量的lambda表达式)对大小没有影响,这意味当自定义删除器可以实现为函数或者lambda时,尽量使用lambda:
#include <iostream>
#include <memory>
class Socket
{
public:
Socket() {}//std::cout << "gouzao" << std::endl;}
~Socket() {}//std::cout << "xigou" << std::endl;}
//关闭资源句柄
void close()
{
}
};
void deletor2(Socket* pSocket) {
//关闭句柄
pSocket->close();
delete pSocket;
}
int main()
{
Socket* pSocket = new Socket();
auto deletor = [](Socket* pSocket) {
//关闭句柄
pSocket->close();
delete pSocket;
};
std::unique_ptr<Socket, decltype(deletor)> spSocket1(new Socket(), deletor); // 返回类型大小是Socket*的大小
std::unique_ptr<Socket, void(*)(Socket*)> spSocket2(new Socket(), deletor); // 返回类型大小是Socket*的指针加至少一个函数指针大小
auto spSocket3 = std::make_shared<Socket>();
std::shared_ptr<Socket> spSocket4(new Socket(),deletor);
std::weak_ptr<Socket> sp5(spSocket3);
std::cout << "原始指针大小: " << sizeof(pSocket) << std::endl;
std::cout << "unique_ptr with lambda 大小:" << sizeof(spSocket1) << std::endl;
std::cout << "unique_ptr with funcp 大小: " << sizeof(spSocket2) << std::endl;
std::cout << "shared_ptr 大小:" << sizeof(spSocket3) << std::endl;
std::cout << "shared_ptr 大小:" << sizeof(spSocket4) << std::endl;
std::cout << "weak_ptr 大小:" << sizeof(sp5) << std::endl;
}
}
mingyu@ndsl84:~/cudalearn$ ./a.out
原始指针大小: 8
unique_ptr with lambda 大小:8
unique_ptr with funcp 大小: 16
shared_ptr 大小:16
shared_ptr 大小:16
weak_ptr 大小:16
2.4 两种形式
std::unique_ptr
有两种形式:
- 一种用于单个对象(std::unique_ptr
) - 一种用于数组(std::unique_ptr<T[]>)
结果就是,指向哪种形式没有歧义。std::unique_ptr
的API设计会自动匹配你的用法,比如operator[]
就是数组对象,解引用操作符(operator*和operator->)就是单个对象专有。
你应该对数组的
std::unique_ptr
的存在兴趣泛泛,因为std::array,std::vector,std::string
这些更好用的数据容器应该取代原始数组。std::unique_ptr<T[]>
有用的唯一情况是你使用类似C的API返回一个指向堆数组的原始指针,而你想接管这个数组的所有权。
2.5 转换
std::unique_ptr
是C++11中表示专有所有权的方法,但是其最吸引人的功能之一是它可以轻松高效的转换为std::shared_ptr
:
// std::unique_ptr 禁止复制语义,但存在特例,即可以通过一个函数返回一个std::unique_ptr
#include <memory>
std::unique_ptr<int> func(int val)
{
std::unique_ptr<int> up(new int(val));
return up;
}
int main()
{
std::shared_ptr<int> sp1 = func(123); // 将返回的unique_ptr 转换成 shared_ptr, 这种可以
// 但是下面这种转换不允许的
std::unique_ptr<int> a (new int());
std::shared_ptr<int> b = a; // no suitable user-defined conversion from "std::unique_ptr<int, std::default_delete<int>>" to "std::shared_ptr<int>" exists
return 0;
}
由下文的介绍可以了解到: 类似std::unique_ptr,std::shared_ptr使用delete作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于std::unique_ptr。对于std::unique_ptr来说,删除器类型是智能指针类型的一部分。对于std::shared_ptr则不是:
auto loggingDel = [](Widget *pw) //自定义删除器
{ //(和条款18一样)
makeLogEntry(pw);
delete pw;
};
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel); //删除器类型是指针类型的一部分
std::shared_ptr<Widget> spw(new Widget, loggingDel); //删除器类型不是指针类型的一部分
3. unique_ptr 的总结
std::unique_ptr
是轻量级、快速的、只可移动(move-only)的管理专有所有权语义资源的智能指针- 默认情况,资源销毁通过
delete
实现,但是支持自定义删除器。有状态的删除器和函数指针会增加std::unique_ptr
对象的大小 - 将
std::unique_ptr
转化为std::shared_ptr
非常简单
Item 19: Use std::shared_ptr for shared-ownership resource management(对于共享资源使用std::shared_ptr)
1. 性能
std::shared_ptr
通过引用计数(reference count)来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少std::shared_ptr
指向该资源。
std::shared_ptr
构造函数递增引用计数值(注意是通常——因为支持移动构造函数,不需要修改引用计数值),析构函数递减值,拷贝赋值运算符做前面这两个工作。(如果sp1和sp2是std::shared_ptr
并且指向不同对象,赋值“sp1 = sp2;”会使sp1指向sp2指向的对象。直接效果就是sp1引用计数减一,sp2引用计数加一。)如果std::shared_ptr
在计数值递减后发现引用计数值为零,没有其他std::shared_ptr
指向该资源,它就会销毁资源。
引用计数暗示着性能问题:
-
std::shared_ptr
大小是原始指针的两倍(可见Item18中的2.3的例子),因为它内部包含一个指向资源的原始指针,还包含一个指向资源的引用计数值的原始指针。(这种实现法并不是标准要求的,但是我(指原书作者Scott Meyers)熟悉的所有标准库都这样实现。) -
引用计数的内存必须动态分配。 概念上,引用计数与所指对象关联起来,但是实际上被指向的对象不知道这件事情(译注:不知道有一个关联到自己的计数值)。因此它们没有办法存放一个引用计数值。(一个好消息是任何对象——甚至是内置类型的——都可以由
std::shared_ptr
管理。)Item21会解释使用std::make_shared
创建std::shared_ptr
可以避免引用计数的动态分配(因此,直接使用new需要为目标对象进行一次内分配,为控制块再进行一次内分配;而使用std::make_shared
只有一次分配,因为std::make_shared
分配一块内存,同时容纳了目标对象和控制块。),但是还存在一些std::make_shared
不能使用的场景,这时候引用计数就会动态分配。 -
递增递减引用计数必须是原子性的,因为多个reader、writer可能在不同的线程。比如,指向某种资源的
std::shared_ptr
可能在一个线程执行析构(于是递减指向的对象的引用计数),在另一个不同的线程,std::shared_ptr
指向相同的对象,但是执行的却是拷贝操作(因此递增了同一个引用计数)。原子操作通常比非原子操作要慢,所以即使引用计数通常只有一个word大小,你也应该假定读写它们是存在开销的。
2. 用法
2.1 拷贝与移动
std::shared_ptr
持有的资源可以在多个 std::shared_ptr
之间共享
-
std::shared_ptr
构造函数递增引用计数值(注意是通常——因为支持移动构造函数,不需要修改引用计数值),析构函数递减值,拷贝赋值运算符做前面这两个工作。 -
从一个
std::shared_ptr
移动构造新std::shared_ptr
会将原来的std::shared_ptr
设置为null
,那意味着老的std::shared_ptr
不再指向资源,同时新的std::shared_ptr
指向资源。这样的结果就是不需要修改引用计数值。因此移动std::shared_ptr
会比拷贝它要快:拷贝要求递增引用计数值,移动不需要。移动赋值运算符同理,所以移动构造比拷贝构造快,移动赋值运算符也比拷贝赋值运算符快。
2.2 构造方式
与std::unique_ptr
类似
2.3 析构
类似std::unique_ptr
(参见Item18),std::shared_ptr
使用delete
作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于std::unique_ptr
。对于std::unique_ptr
来说,删除器类型是智能指针类型的一部分。对于std::shared_ptr则不是:
auto loggingDel = [](Widget *pw) //自定义删除器
{ //(和条款18一样)
makeLogEntry(pw);
delete pw;
};
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel); //删除器类型是指针类型的一部分
std::shared_ptr<Widget> spw(new Widget, loggingDel); //删除器类型不是指针类型的一部分
因此,std::shared_ptr
的设计更为灵活。考虑有两个std::shared_ptr<Widget>
,每个自带不同的删除器(比如通过lambda表达式自定义删除器):
auto customDeleter1 = [](Widget *pw) { … }; //自定义删除器,
auto customDeleter2 = [](Widget *pw) { … }; //每种类型不同
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);
因为pw1和pw2有相同的类型(虽然他们的删除其类型不同,但对于shared_ptr而言,删除器类型并不是指针类型的一部分),所以它们都可以放到存放那个类型的对象的容器中:
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };
它们也能相互赋值,也可以传入一个形参为std::shared_ptr<Widget>
的函数。但是自定义删除器类型不同的std::unique_ptr
就不行,因为std::unique_ptr
把删除器视作类型的一部分。
另一个不同于std::unique_ptr
的地方是,指定自定义删除器不会改变std::shared_ptr
对象的大小。不管删除器是什么,一个std::shared_ptr
对象都是两个指针大小。这是个好消息,但是它应该让你隐隐约约不安。自定义删除器可以是函数对象,函数对象可以包含任意多的数据。它意味着函数对象是任意大的。std::shared_ptr
怎么能引用一个任意大的删除器而不使用更多的内存?
它不能。它必须使用更多的内存。然而,那部分内存不是std::shared_ptr
对象的一部分。那部分在堆上面,或者std::shared_ptr
创建者利用std::shared_ptr
对自定义分配器的支持能力,那部分内存随便在哪都行。
2.4 内部实现
前面提到了std::shared_ptr
对象包含了所指对象的引用计数的指针。没错,但是有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做控制块(control block)。
每个std::shared_ptr
管理的对象都有个相应的控制块。控制块除了包含引用计数值外还有一个自定义删除器的拷贝,当然前提是存在自定义删除器。如果用户还指定了自定义分配器,控制块也会包含一个分配器的拷贝。控制块可能还包含一些额外的数据,正如Item21提到的,一个次级引用计数weak count
,但是目前我们先忽略它。我们可以想象std::shared_ptr
对象在内存中是这样:
当指向对象的std::shared_ptr
一创建,对象的控制块就建立了。至少我们期望是如此。通常,对于一个创建指向对象的std::shared_ptr
的函数来说不可能知道是否有其他std::shared_ptr
早已指向那个对象,所以控制块的创建会遵循下面几条规则:
-
std::make_shared(参见Item21)总是创建一个控制块。 它创建一个要指向的新对象,所以可以肯定std::make_shared调用时对象不存在其他控制块。
-
当从独占指针(即std::unique_ptr)上构造出std::shared_ptr时会创建控制块。独占指针没有使用控制块,所以指针指向的对象没有关联控制块。(作为构造的一部分,std::shared_ptr侵占独占指针所指向的对象的独占权,所以独占指针被设置为null)
-
当从原始指针上构造出std::shared_ptr时会创建控制块。如果你想从一个早已存在控制块的对象上创建std::shared_ptr,你将假定传递一个std::shared_ptr或者std::weak_ptr(参见Item20)作为构造函数实参,而不是原始指针。用std::shared_ptr或者std::weak_ptr作为构造函数实参创建std::shared_ptr不会创建新控制块,因为它可以依赖传递来的智能指针指向控制块。
从原始指针上构造超过一个std::shared_ptr就会让你走上未定义行为的快车道,因为指向的对象有多个控制块关联(但是所有的shared_ptr都指向同一个T Object)。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数一次)。
一个尤其令人意外的地方是使用this指针作为std::shared_ptr构造函数实参的时候可能导致创建多个控制块。https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/4.SmartPointers/item19.md
3. shared_ptr的总结
-
std::shared_ptr
为有共享所有权的任意资源提供一种自动垃圾回收的便捷方式。 -
较之于
std::unique_ptr
,std::shared_ptr
对象通常大两倍,控制块会产生开销,需要原子性的引用计数修改操作。 -
默认资源销毁是通过
delete
,但是也支持自定义删除器。删除器的类型是什么对于std::shared_ptr
的类型没有影响。 -
避免从原始指针变量上创建
std::shared_ptr
, 通常替代方案是使用std::make_shared
,不过当我们要使用自定义删除器,用std::make_shared
就没办法做到
Item 20: Use std::weak_ptr for std::shared_ptr-like pointers that can dangle(当std::shared_ptr可能悬空时使用std::weak_ptr)
https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/4.SmartPointers/item20.md
自相矛盾的是,如果有一个像std::shared_ptr
(见Item19)的但是不参与资源所有权共享的指针是很方便的。换句话说,是一个类似std::shared_ptr
但不影响对象引用计数的指针。这种类型的智能指针必须要解决一个在std::shared_ptr
中存在的问题:即std::shared_ptr
可能指向已经销毁的对象。一个真正的智能指针应该跟踪所指对象,在悬空时知晓,悬空(dangle)就是指针指向的对象不再存在。这就是对std::weak_ptr
最精确的描述。
请记住:
-
用std::weak_ptr替代可能会悬空的std::shared_ptr。
-
std::weak_ptr的潜在使用场景包括:缓存、观察者列表、打破std::shared_ptr环状结构。
std::enable_shared_from_this 作用
下面记录自己写的一些demo,详细的回答可以看 https://www.zhihu.com/question/30957800 中@孙梓轩的回答
- 下面情况如果不使用
shared_from_this()
,会导致对同一个对象创建了两个控制块,且每个控制块的引用计数都为1,会造成对象管理不正确
#include <memory>
#include <iostream>
#include <assert.h>
using namespace std;
class T : public enable_shared_from_this<T> {
public:
T() { cout << "T()" << endl;}
~T() {cout << "~T()" << endl;}
typedef std::shared_ptr<T> ptr;
T::ptr GetThis() {
return std::shared_ptr<T>(this); // 从裸指针创建shared_ptr也会创建一个控制块
// return this->shared_from_this();
}
private:
};
int main()
{
// std::shared_ptr<T> t = std::make_shared<T>();
std::shared_ptr<T> t(new T()); // 不论是make_shared 还是 从裸指针方式创建shared_ptr 都会创建一个控制块
std::shared_ptr<T> ptr = t->GetThis();
cout << "count = " << ptr.use_count() << endl;
cout << "count = " << t.use_count() << endl;
t.reset();
cout << "count = " << ptr.use_count() << endl;
cout << "count = " << t.use_count() << endl;
ptr.reset();
cout << "count = " << ptr.use_count() << endl;
cout << "count = " << t.use_count() << endl;
}
mingyu@host-H3C-UniServer-R4900-G5:~/cpp-projects/demo$ ./a.out
T()
count = 1
count = 1
~T()
count = 1
count = 0
~T()
munmap_chunk(): invalid pointer
已放弃 (核心已转储)
- 使用shared_from_this; C++解决方案是通过继承一个类,这个类本质上会给被管理的object上加一个指向计数器的weak ptr,于是就可以正确地增加引用计数而不是搞出2个独立的计数器。因此引出的一个问题就是如果要调用
shared_from_this()
必须调用对象至少要被一个std::shared_ptr对象管理,否则会报错:std::bad_weak_ptr。
#include <memory>
#include <iostream>
#include <assert.h>
using namespace std;
class T : public enable_shared_from_this<T> {
public:
T() { cout << "T()" << endl;}
~T() {cout << "~T()" << endl;}
typedef std::shared_ptr<T> ptr;
T::ptr GetThis() {
// return std::shared_ptr<T>(this);
return this->shared_from_this();
}
private:
};
int main()
{
// std::shared_ptr<T> t = std::make_shared<T>();
std::shared_ptr<T> t(new T());
std::shared_ptr<T> ptr = t->GetThis();
cout << "count = " << ptr.use_count() << endl;
cout << "count = " << t.use_count() << endl;
t.reset();
cout << "count = " << ptr.use_count() << endl;
cout << "count = " << t.use_count() << endl;
ptr.reset();
cout << "count = " << ptr.use_count() << endl;
cout << "count = " << t.use_count() << endl;
}
mingyu@host-H3C-UniServer-R4900-G5:~/cpp-projects/demo$ ./test_sptr
T()
count = 2
count = 2
count = 1
count = 0
~T()
count = 0
count = 0
- 使用
shared_from_this
, 但是对象没有至少被一个shared_ptr管理
#include <memory>
#include <iostream>
using namespace std;
class T : public enable_shared_from_this<T> {
public:
T() { cout << "T()" << endl;}
~T() {cout << "~T()" << endl;}
typedef std::shared_ptr<T> ptr;
T::ptr GetThis() {
// return std::shared_ptr<T>(this);
return this->shared_from_this();
}
private:
};
int main()
{
T t;
auto sp = t.GetThis();
}
mingyu@host-H3C-UniServer-R4900-G5:~/cpp-projects/demo$ ./test_wptr
T()
terminate called after throwing an instance of 'std::bad_weak_ptr'
what(): bad_weak_ptr
已放弃 (核心已转储)
参考:
https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/4.SmartPointers/item18.md
《Effective Modern Cpp》