《effectivc c++》第三章关于使用智能指针的部分有很多在 c++2.0 时代已经有些过时,所以该部分的笔记综合了《effective modern c++》中智能指针的部分。

13:以对象管理资源

《effective modern c++》条款18:使用 std::unique_ptr 管理具备专属所有权的资源
《effective modern c++》条款19:使用 std::shared_ptr 管理具备共享所有权的资源

  • std::unique_ptr 是小巧、高速的、具备只移型别的智能指针,对托管资源实施专属所有权定义

当使用 unique_ptr 的时候,最多可以有一个 unique_ptr 指向一个资源。当 unique_ptr 被销毁时,资源会自动被回收。因为任何资源只能有一个 unique_ptr,所以任何对 unique_ptr 进行复制的尝试都会导致编译错误。

1
2
unique_ptr<T> myPtr(new T);       // Ok
unique_ptr<T> myOtherPtr = myPtr; // Error: Can't copy unique_ptr

但是,unique_ptr 可以用移动

1
2
unique_ptr<T> myPtr(new T);                 // Ok
unique_ptr<T> myOtherPtr = std::move(myPtr); // Ok, 资源现在储存在myOtherPtr

同样也可以执行以下操作

1
2
3
4
5
unique_ptr<T> MyFunction() {
unique_ptr<T> myPt(/* ... */);
/* ... */
return myPtr;
}

上边用法的意思是:我将一个管理资源返回给你,如果你没有明确捕获返回值(ptr=MyFunction()),资源会被清理。如果捕获了返回值,那么你现在拥有资源的独占所有权。这样就可以把 unique_ptr 视为更安全更好的 auto_ptr 的替代品。

  • unique_ptr 默认的资源析构采用 delete 运算符来实现,但可以指定自定义析构器。有状态的的删除器和采用函数指针实现的删除器会增加 std::unique_ptr 型别的对象尺寸

  • 将 std::unique_ptr 转换成 std::shared_ptr 是容易实现的

将 std::unique_ptr 类别的对象转换为 std::shared_ptr 类别

1
std::shared_ptr<Investment> sp = makeInvestment(argument);

这一特性使得 unique_ptr 非常适合作为工厂函数的返回类型。工厂函数并不知道调用者对返回的对象采取专属所有权好还是共享所有权更合适,所以通过返回一个 unique_ptr 向调用者提供了更高效的智能指针,同时调用者也可将返回值转换成其他类型的智能指针(shared_ptr)。

  • std::shared_ptr 提供方便的手段,实现了任意资源在共享所有权语义下进行生命周期管理的垃圾回收

shared_ptr 允许多个指针指向给定资源。其通过访问某资源的引用计数来确定是否自己是最后一个指涉到该资源的,引用计数用来记录指涉到该资源的 shared_ptr 数量,当最后一个指向资源的 shared_ptr 被销毁时该资源将被释放。

1
2
shared_ptr<T> myPtr(new T); // Ok
shared_ptr<T> myOtherPtr = myPtr // Ok,现在有两个知怎指向资源
  • 与 std::unique_ptr 相比,std::shared_ptr 的尺寸通常是裸指针尺寸的两倍,它还会带来控制块的开销,并要求原子化的引用计数操作

引用计数带来的一些性能影响:

  1. shared_ptr 的尺寸是裸指针的两倍。因为内部包含一个指向资源的裸指针和一个指向引用计数的裸指针。
  2. 引用计数的内存必须动态分配
  3. 引用计数的递增和递减必须是原子操作。当多线程运行时需要原子操作。

16:成对使用 new 和 delete 时要采取相同形式

  • 如果你在 new 表达式中使用[],必须在相应的 delete 表达式中也使用[]。如果你在 new 表达式中不使用[],一定不要在相应的 delete 表达式中使用[]。
1
2
3
4
5
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];

delete stringPtr1; // 删除一个对象
delete[] stringPtr2; // 删除一个由对象组成的数组

如果对 stringPtr1 使用 delete[] 操作,delete 会读取若干内存并将它解释为“数组大小”,然后开始多次调用析构函数,那无疑会带来灾难性的后果。
如果没有对 stringPtr2 使用 delete[] 操作,则会只删除数组中的第一个对象的析构函数,造成内存泄露。

17:优先使用 std::make_unique 和 std::make_shared,而非直接使用 new

《effective modern c++》条款21

原条款;以独立语句将 newed 对象置入智能指针。

  • 相比于直接使用 new 表达式,make 系列函数消除了重复代码、改进了异常安全性,并且对于 std::make_shared 和 std::allcoated_shared 而言,生成的目标代码会尺寸更小、速度更快。

std::make_unique 和 std::make_shared 是三个 make 系列函数中的两个。make 系列函数会把一个任意实参集合完美转发给动态分配内存的对象的构造函数,并返回一个指涉到该对象的智能指针。需要注意的是 make_unique 出现在 c++14。

优先选用 make 系列函数的第一个原因:

1
2
auto foo = std::make_shared<Widget>();
std::shared_ptr<Widget> foo2 (new Widget);

使用了 new 版本的将 int 重复写了两遍,而 make 函数则没有。

优先使用 make 系列函数的第二个原因与异常安全有关。
考虑以下的代码

1
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

在 processWidget 执行前,下列事件必须发生:

  • new Widget,一个 Widget 对象必须在堆上创建
  • 由 new 产生的裸指针的托管对象 std::shared_ptr 的构造函数必须执行
  • computePriority 必须运行

但是编译器不一定按照上述顺序执行代码,如果 computePriority 的运行发生在 new 和 std::shared_ptr 之间。那么第一步动态分配的 Widget 的内存将不会存储到智能指针中,也就发生了泄漏。归根结底其原因还是 std::shared_ptr(new Widget) 会分成两步走,而两步之间发生了异常就会内存泄漏,而反观 make_shared 是一步,就不会出现上述的问题。

mae_shared 的另一个优势是性能的提升。以上边的 Widget 举例,使用 new 会引发两次内存分配,因为除了要为 Widget 进行一次内存分配,还要为 shared_ptr 相关联的控制块再进行一次内存分配。而使用 make_shared 一次内存分配就够了,make_shared 会分配单块内存既保存 Widget 对象又保存其相关联的控制块。

  • 不适于使用 make 系列函数的场景包括需要定制删除器,以及期望直接传递大括号初始化物。
  • 对于 std::shared_ptr,不建议使用 make 系列函数的额外场景包括:(1)自定义内存管理的类;(2)内存紧张的系统、非常大的对象、以及存在比指涉到相同对象的 std::shared_ptr 生存期更久的 std::weak_ptr。