条款18 对于独占资源使用std::unique_ptr
- 默认情况下
std::unique_ptr
大小等同于原始指针(row pointer) (使用默认删除器)。
std::unique_ptr
始终拥有其所指的内容,移动一个std::unique_ptr
将所有权从源指针转移到目的指针(源指针被设置为nullptr);拷贝一个**std::unique_ptr**
是不允许的,因为会得到指向相同内容的两个std::unique_ptr
,每个都认为自己拥有且应当最后销毁资源,就会double delete。
默认情况下,std::unique_ptr
的析构操作是通过其内部裸指针实施delete
完成的。
std::unique_ptr
一般用于返回工厂模式的对象:std::unique_ptr
可以被设置使用自定义删除器。删除器是**std::unique_ptr**
类型的一部分。
auto delInvmt = [](Investment* pInvestment) //自定义删除器
{ //(lambda表达式)
makeLogEntry(pInvestment);
delete pInvestment;
};
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)> //更改后的返回类型
makeInvestment(Ts&&... params)
{
std::unique_ptr<Investment, decltype(delInvmt)> //应返回的指针
pInv(nullptr, delInvmt);
if (/*一个Stock对象应被创建*/)
{
pInv.reset(new Stock(std::forward<Ts>(params)...));
}
else if ( /*一个Bond对象应被创建*/ )
{
pInv.reset(new Bond(std::forward<Ts>(params)...));
}
else if ( /*一个RealEstate对象应被创建*/ )
{
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}
自定义删除器的一个形参类型是原始指针Investment*
,不管是在makeInvestment
内部创建的对象的真实类型是什么,最终在 lambda 表达式中作为Investment*
对象被删除。这意味着通过基类指针删除派生类实例,基类必须有 virtual 析构函数。
当使用自定义析构器时,其类型必须被指定为std::unique_ptr
的第二个实参类型,此处是decltype(delInvmt)
。
将裸指针(如new
的结果)通过“=”赋值给std::unique_ptr
不会通过编译,因为会形成裸指针到智能指针的隐式类型转换。C++11将这种转换禁止了。所以只能使用成员函数**reset()**
。
当使用默认删除器时,可以合理假设std::unique_ptr
对象和原始指针大小相同。当自定义删除器时,函数指针形式的删除器会使std::unique_ptr
从一个字大小增加到两个。对于函数对象形式的删除器来说,变化的大小取决于函数对象中存储的状态是多少,无状态函数对象(比如不捕获变量的lambda表达式)对大小没有影响。
所以尽量使用 lambda 来作为自定义的删除器。
std::unique_ptr
可以灵活的转化为std::shared_ptr
,所以通常令工厂函数返回std::unique_ptr
。
条款19 对于共享资源使用std::shared_ptr
std::shared_ptr
通过引用计数来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少**std::shared_ptr**
指向该资源。(如果 sp1 和 sp2 是std::shared_ptr
并且指向不同的对象;赋值sp1=sp2;
会使 sp1 指向 sp2 指向的对象。直接效果就是 sp1 所指对象的引用计数减 1, sp2 所指对象的引用计数加 1 )
**std::shared_ptr**
大小是原始指针的两倍。因为内部包含一个指向资源的原始指针,和一个指向资源的引用计数值的原始指针。(这种实现并不是标准要求的,但是大多数标准库都是这么实现的)- 引用计数的内存必须动态分配。引用计数和所指对象关联起来,但是实际上被指的对象不知道这件事情。因此它们无法存放一个引用计数值。
- 递增递减引用计数必须是原子操作的。
如果使用移动方式构造新的std::shared_ptr
会将原来的std::shared_ptr
设置为null,老的std::shared_ptr
不再指向资源,新的std::shared_ptr
指向资源。这种情况下,就不需要修改引用计数值。
对于std::unique_ptr
来说,删除器类型是智能指针类型的一部分。对于std::shared_ptr
则不是:
auto loggingDel = [](Widget *pw){
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**
就可以放入同一个容器。
指定删除器不会改变std::shared_ptr
对象的大小,不管删除器是什么,一个std::shared_ptr
对象都是两个指针大小。那么删除器消耗的内存在哪呢?
堆!std::shared_ptr
对象的引用计数是一个更大的数据结构的一部分,那个数据结构是控制块。每个std::shared_ptr
管理的对象都有个相应的控制块。控制块包含引用计数值和一个自定义删除器的拷贝。控制块可能还包含一些额外数据,比如一个次级引用计数 weak count。
当指向对象的 std::shared_ptr
一创建,对象的控制块就建立了。
**std::make_shared**
总是创建一个控制块。他创建一个要指向的新对象,所以可以肯定它调用时对象不存在其他控制块。- 当从独占指针(即
**std::unique_ptr**
或者**std::auto_ptr**
)上构造出**std::shared_ptr**
时会创建控制块。独占指针没有控制块。(作为构造的一部分,std::shared_ptr
侵占独占指针所指向对象的独占权,所以独占指针被设置为 nullptr) - 当从原始指针上构造出
**std::shared_ptr**
时候会创建控制块。如果在一个已经存在控制块的对象上创建std::shared_ptr
,不会创建新的控制块。因为在构造的时候可以依赖传递来的智能指针来指向控制块。
所以当从原始指针上构造超过一个 std::shared_ptr
,就会产生未定义行为。因为指向的对象有多个控制块。多个控制块意味着多个引用计数值,意味着对象会被销毁多次(每个引用计数一次)。
std::shared_ptr
不能处理数组,因为它设计之初就是针对单个对象的。
避免使用裸指针变量创建**std::shared_ptr**
指针。
条款20 当std::shared_ptr
可能悬空时使用std::weak_ptr
**std::weak_ptr**
不能解引用,也不能测试是否为空值。因为它不是一个独立的智能指针,它建立在**std::shared_ptr**
之上,但是它不会增加所指对象的引用计数。
一般通过std::shared_ptr
创建std::weak_ptr
,初始化后二者就值向相同位置。
当std::shared_ptr
失效后,对应的std::weak_ptr
空悬,可以使用expired()成员函数来测试:
需要一个原子操作来完成std::weak_ptr
是否失效的检验,以及未失效的条件下提供对所指对象的访问。可以通过由std::weak_ptr::lock
实现,它返回一个std::shared_ptr
,如果前者已经失效,则创建出来的std::shared_ptr
为空:
std::weak_ptr
可能的用武之地包括:缓存、观察者列表,以及避免std::shared_ptr
的指针环路。
条款21 优先使用std::make_unique
和std::make_shared
而非直接使用new
拷贝初始化不允许使用 explicit 构造函数
在初始化变量时,如果使用 = 来初始化,实际上执行的是拷贝初始化,如果不使用 = ,则执行的是直接初始化。
而接收指针参数的智能指针构造函数是 explicit 的,因此不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式:
shared_ptr<int> p1 = new int(1024); //错误!
shared_ptr<int> p2(new int 1024); 正确!使用了直接初始化
make_shared
是C++11的一部分,而make_unique
是C++14才补充进来的。make_unique
只是把它的参数完美转发到创建对象的构造函数。(std::forward
)
void processWidget(std::shared_ptr<Widget> spw,int priority);
如果调用时采用new
而不是std::make_shared
,会导致潜在的资源泄露:
processWidget(std::shared_ptr<Widget>(new Widget),computePriority());
在运行时,一个函数的实参必须先被计算,再调用函数。
如果按照这个顺序执行:
- 执行
new Widget
- 执行
computePriority
- 运行
std::shared_ptr
构造函数
如果在computePriority
产生了异常,那么第一步动态分配的 Widget 就会泄露,它永远不会被第三步的std::shared_ptr
所管理了。
所以采用make_shared
可以防止这个问题。
make_shared
也比new
效率高,因为std::shared_ptr
指向一个控制块,其中包含引用计数和其他东西。这个控制块在std::shared_ptr
的构造函数中分配。因此new
需要为 Widget 进行一次内存分配,再为控制块进行一次内存分配。
而make_shared
只需要一次,分配单块内存既保存对象又保存与其相关的控制块。这种优化减小了程序的静态尺寸,因为代码只包含一次内存分配调用,同时还增加了可执行代码的运行速度,因为内存是一次性分配出来的。
make 函数不能自定义删除器。但是std::shared_ptr
和std::unique_ptr
的构造函数都支持自定义删除器。
make函数也不能使用花括号初始化。怪不得我make_shared<T>{...}
给我报错呢。
在 make 函数中,对形参的完美转发使用的是小括号而非大括号。
对于一些重载了operator new
和operator delete
的类,这些函数的存在意味着对这些类型的对象的全局内存分配和释放并不适用,所以需要自定义的operator new
和operator delete
来精确的分配、释放。所以这种类型不适合std::shared_ptr
对自定义分配(通过std::allocate_shared
)和释放(通过自定义删除器)的支持。
因为std::allocate_shared
需要的内存总大小不等于动态分配的对象大小,还需要再加上控制块大小。
所以使用make
函数去创建重载了**operator new**
和**operator delete**
的类对象不可取。
条款22 当使用Pimpl手法,在实现文件中定义特殊成员函数
Pimpl( pointer to implementation ):将类数据成员替换为一个指向包含具体实现的类的指针,并将放在主类的数据成员们放到实现类,这些数据成员通过指针间接访问。
std::unique_ptr
的默认删除器是一个函数,使用delete
销毁内置于智能指针的原始指针。但是在调用**delete**
之前,通常会使默认删除器使用C++11特性**static_assert**
来确保原始指针指向的不是一个非完整类型。
所以要确保在析构之前出现实现类的完整定义。可以在该类成员函数定义部分的析构函数实现处后加=default
。
但是如果在类中声明析构函数处直接使用=default
也没有任何鸟用:
因为此处虽然有了析构的定义,但是 row pointer 仍然是一个非完整类型,析构虽然定义但无法完成。所以必须要在实现文件中,让 **Impl**
的定义被析构函数看到(只要类型的定义可以被看到,就是完整的),在**Impl**
定义之后定义析构函数体即可。
Hint:体会分离编译。
std::shared_ptr
和std::unique_ptr
在 pImpl 指针上的表现有区别的深层原因:他们支持自定义删除器的方式不同。
- 对于
std::shared_ptr
,删除器类型不是智能指针类型的一部分,会让它生成更大的运行时数据结构,但是当编译器生成的特殊成员函数(比如构造和析构函数)被使用的时候,指向的对象不必是一个完整类型。 - 对于
std::unique_ptr
,删除器类型是智能指针类型的一部分,让编译器生成更小的运行时数据机构,但是编译器生成的特殊成员函数被调用时,必须是一个已完成类型。
// widget.h
class Widget{
public:
Widget();
~Widget();
Widget(Widget&& rhs);
Widget& operator=(Widget&& rhs);
...
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
//widget.cpp
#include"widget.h"
#include"gadget.h"
#include<vector>
#include<string>
struct Widget::Impl{
std::string name;
std::vector<double> data;
Gadget g1,g2,g3;
};
Widget::Widget() = default;
Widget::~Widget() = default;
Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;
注意,析构的定义一定要出现在移动前面,因为移动的隐含操作是析构老的对象,中间会产生析构,如果在移动时,析构不可见,那么将会报错。