《Effective Modern C++》学习笔记之条款十九:使用std::shared_ptr管理具备共享所有权的资源

std::shared_ptr通过引用计数的方式实现资源共享,当计数为0时,资源将被释放,和std::unique_ptr不同的是其不能处理数组,只能处理单个对象。

std::shared_ptr的尺寸是裸指针的两倍,因为其内部除了包含了一个指涉到该资源的裸指针,还包含一个指涉到该资源的一个控制块的裸指针,该控制块为动态分配在堆上,内容包含:引用计数、弱计数、自定义删除器、自定义分配器等。控制块的生成遵循以下几个规则:

  1. std::make_shared(参见条款21)总是创建一个控制块。std::make_shared会生产出一个新对象,所以此时新对象一定没有控制块的存在
  2. 从具备专属所有权的指针(std::auto_ptr、std::unique_ptr)出发构造的std::shared_ptr时,会创建一个控制块。因为std::auto_ptr或std::unique_ptr不存在控制块,转换为std::shared_ptr后,需要新建
  3. 当std::shared_ptr构造函数使用裸指针作为实参构造时,会创建一个控制块。因为裸指针也不含有控制块,所以我们不能使用同一个裸指针构造两个std::shared_ptr,否则将导致程序崩溃.可以使用new代替。
  4. 当std::shared_ptr被std::shared_ptr或std::weak_ptr作为实参构造时,不会创建控制块,因为传入的std::shared_ptr或std::weak_ptr已经含有控制块

我们在对std::shared_ptr的引用计数进行操作时,其成本是很高的,因为这个是原子操作。所以当我们对一个std::shared_ptr进行移动构造时,实际上是非常迅速的,因为这个操作仅仅是将源std::shared_ptr置空,其引用计数不变。

与std::unique_ptr类似,std::shared_ptr也可以自定义析构器,且析构器的大小不会影响std::shared_ptr的大小,因为函数是分配在堆上的,方式上与std::unique_ptr差别也比较大的,代码示例如下:

//自定义析构器myDelete1 
auto myDelete1 = [](Widget* w){
    cout<<"myDelete1"<<endl;
};

//自定义析构器myDelete2
auto myDelete2 = [](Widget* w){
    cout<<"myDelete2"<<endl;
};

//pw1的自定义析构器为myDelete1
std::shared_ptr<Widget> pw1(new Widget,myDelete1);

//pw2的自定义析构器为myDelete2
std::shared_ptr<Widget> pw2(new Widget,myDelete2);

//虽然pw1与pw2的自定义析构函数不一样看,但都是std::shared_ptr<Widget>类型,可以相互转换
//而如果是std::unique_ptr则不行,因为其类型不同
pw1 = pw2;

下面我们再来考虑一种场景,如果类Bad被std::share_ptr管理,且在类Bad的成员函数里需要把当前类对象作为share_ptr参数传给其他对象进行初始化时,将会发生什么呢?代码如下:

#include <memory>
#include <iostream>
 
class Bad
{
public:
	std::shared_ptr<Bad> getptr() {
            //裸指针初始化,重新生成一个控制块
	    return std::shared_ptr<Bad>(this);
	}
	~Bad() { std::cout << "Bad::~Bad() called" << std::endl; }
};
 
int main()
{
	// bp1有自己的控制块,指针指向 new Bad()的返回值,即对象的this指针
	std::shared_ptr<Bad> bp1(new Bad());
    
        // bp2也有自己的控制块,指针也指向 new Bad()返回的this指针
	std::shared_ptr<Bad> bp2 = bp1->getptr();

	// 打印bp1和bp2的引用计数
	std::cout << "bp1.use_count() = " << bp1.use_count() << std::endl;
	std::cout << "bp2.use_count() = " << bp2.use_count() << std::endl;
}  // this对象一次申请,两次释放,导致程序崩溃

所以为了处理这个场景,C++11为我们新增了一个类模板:std::enable_shared_from_this<T>,这个模板类提供了一个成员函数share_from_this(),该函数的主要作用是查询当前对象的控制块,如果存在则创建一个指涉到该控制块的std::share_ptr对象,否则,将抛出异常,具体行为未定义,所以为了保证不抛出异常,请在使用该函数前,确保分配的对象已经与一个std::shared_ptr相关联。上面的代码也可以改成这样:

#include <memory>
#include <iostream>

//继承于enable_shared_from_this
struct Good : std::enable_shared_from_this<Good> // 注意:继承
{
public:
	std::shared_ptr<Good> getptr() {
                //使用shared_from_this(),确保接收方的控制块与源对象是同一个
		return shared_from_this();
	}
	~Good() { std::cout << "Good::~Good() called" << std::endl; }
};

int main()
{
	// 大括号用于限制作用域,这样智能指针就能在system("pause")之前析构
	{
                //先让gp1与std::shared_ptr关联,生成控制块
		std::shared_ptr<Good> gp1(new Good());

                //gp2和gp1使用的是同一个控制块,所以计数+1
		std::shared_ptr<Good> gp2 = gp1->getptr();

		// gp1和gp2的引用计数都为2
		std::cout << "gp1.use_count() = " << gp1.use_count() << std::endl;
		std::cout << "gp2.use_count() = " << gp2.use_count() << std::endl;
	}
	system("pause");
}

要点速记

  • std::shared_ptr提供方便的手段,实现了任意资源在共享所有权语义下进行生命周期管理的垃圾回收
  • 与std::unique_ptr相比,std::shared_ptr的尺寸通常是裸指针的两倍,还好带了控制块的开销,并要求原子化的引用技术操作
  • 默认的资源析构提供delete运算符进行,但同时支持定制析构器,析构器的i性别对std::shared_ptr的型别没有影响
  • 避免使用裸指针型别的变量来创建std::shared_ptr指针

猜你喜欢

转载自blog.csdn.net/Chiang2018/article/details/114446472