C++读书笔记

超精简Effective Modern C++ 第四章 智能指针

简介

裸指针的缺陷:

  • 未指明是指针还是数组
  • 裸指针无法提示在使用完之后是否需要析构
  • 即使知道析构函数所指的函数也不知道如何析构合适,是delete还是传入专门的用于析构的函数
  • 即使知道了应该使用delete,但是不知道使用delete还是delete[]。一旦用错会导致未定义行为
  • 即使知道如何析构,但是需要保证所有路径只执行一次,如果未执行则发生泄漏,执行多次则发生未定义行为
  • 没办法检测出裸指针是否空悬

4种智能指针:

auto_ptr:已经弃用,后面全部都使用unique。使用了复制操作符实现了移动语义,导致复制的时候会被置空。并且无法在容器中使用

unique_ptr:可以做auto_ptr 的所有事情。并且去除了限制。

shared_ptr:引用计数

weak_ptr:防止循环引用,用于有可能空悬的指针

条款18. 使用std::unique_ptr管理具备专属权的资源

  • unique_ptr是只移类型的只能指针,对于托管资源采用的是专属所有权。
  • 一般来说unique_ptr的析构都使用默认的delete。不过也可以指定自定义的删除器,不过这会造成指针尺寸变大。
auto del = [](Object* obj)
{
	//do something..
	delete obj;
};

// 使用时可以使用以下类型
// std::unique_ptr<Object, decltype(del)>
  • unique_ptr可以直接转换为shared_ptr,不过反过来不行。
  • 裸指针不允许直接赋值给unique_ptr,会出现比较大的问题
  • 不要对数组使用unique_ptr,而是使用适合的stl容器

条款19.使用std::shared_ptr管理具备共享所有权的资源

性能影响

  • std::shared_ptr的尺寸是裸指针的两倍(包含一个控制块的指针)
  • 引用计数的内存是动态分配的
  • 引用计数的递增递减是原子操作

自定义删除器

shared_ptr也可以自定义删除行为,但是与unique_ptr相比,自定义删除行为并不属于类的一部分,所以更具弹性。

auto loggingDel = [](Widget* pw){
	// do something...	
	delete pw;
};

std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);

// 模板无需传入类型
std::shared_ptr<Widget> spw(new Widget, loggingDel);

自定义析构器不会改变shared_ptr的大小,因为引用计数、弱计数、自定义删除器、分配器等是保存在控制块上,而shared_ptr只拿了其指针。

控制块何时创建

  • std::make_shared总是创建一个控制块。
  • 从专属所有权指针(unique_ptr或auto_ptr)转换为shared_ptr时会创建一个控制块
  • 如果从裸指针创建shared_ptr时(虽然一般我们不建议这么做),会创建一个控制块

多控制块控制单一裸指针陷阱

如果一个指针同时创建了两个shared_ptr,那么有两个控制块同时控制一个裸指针,会导致两次析构,引向了未定义行为的结果。

所以一般建议这样做:

std::shared<Widget> spw1(new Widget, loggingDel);

还有一个容易出现的问题是容器中使用了shared_ptr

// 和直接用裸指针创建shared_ptr没有什么区别
processedWidgets.emplace_back(this);

我们可以使用shared_from_this()的接口来避免这个问题。

通过继承enable_shared_from_this<T>,来帮助用户使用shared_ptr

条款20.对于类似std::shared_ptr但是有可能空悬的指针使用std::weak_ptr

通过lock方法我们可以通过weak_ptr判断指针是否失效。

互相引用的指针

  • 直接使用裸指针,容易产生未定义行为
  • 使用shared_ptr,循环引用,资源无法回收
  • weak_ptr,可以防止以上的两种情况

可能的使用场景

本地缓存、观察者列表,避免循环引用

如果是父子类的情况,实际上子类的声明周期一般都小于父类,所以子类直接使用裸指针也还行。

条款21.优先选用std::make_unique和std::make_shared,不是直接用new

make函数的优点

  • shared_ptr可以避免两次分配,而只需要一次动态分配
  • 可以统一申请对象的写法,目标代码也更小
  • 可以避免在构造函数调用之前发生异常导致的泄漏

应该使用构造函数而不是make函数的情况

  • 需要定制删除器
  • 希望直接传递初始化列表

make_shared不适合的地方

  • 自定义内存管理的类
  • 内存紧张的系统,非常大的对象。有可能导致内存释放的延迟。

条款22.使用Pimpl习惯用法时,将特殊成员函数定义到实现文件中

所谓Pimple就是在类中定义一个额外的实现类,然后实际使用的对外接口只取这个实现类的指针。

// widget.h
class Widget{
	public:
	Widget();

	**~Widget();//必须定义,否则无法编译通过,因为unique_ptr的删除器是类的一部分,必须可达
	//声明析构之后会组织移动语义生成,故需要主动声明移动语义
	Widget(Widget&& rhs);
	Widget& operator=(Widget&& rhs);**

	private:
	struct Impl;
	std::unique_ptr<Impl> pImpl;
}

实现文件如下

#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>

struct Widget::Impl{
	std::string name;
	std::vector<double> data;
	Gadget g1,g2,g3;
}

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

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

以上要求仅针对unique_ptr

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注