前言

之前学习muduo网络库的时候,看到作者陈硕用到了enable_shared_from_thisshared_from_this,一直对此概念是一个模糊的认识,隐约记着这个机制是在计数器智能指针传递时才会用到的,今天对该机制进行梳理总结一下吧。

如果不熟悉C++带引用计数的智能指针shared_ptrweak_ptr,可参考这篇文章:深入掌握智能指针

这篇文章主要介绍C++11提供的智能指针相关的enable_shared_from_thisshared_from_this机制。

问题代码

我们先给出两个智能指针的应用场景代码,这些代码都有问题,仔细思考下问题原因。

代码清单1

#include <iostream>
#include <memory>
using namespace std;
// 智能指针测试类
class A {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int *m_ptr;
};
int main() {
A *p = new A(); // 裸指针指向堆上的对象 shared_ptr<A> ptr1(p); // 用shared_ptr智能指针管理指针p指向的对象
shared_ptr<A> ptr2(p); // 用shared_ptr智能指针管理指针p指向的对象
// 下面两次打印都是1,因此同一个new A()被析构两次,逻辑错误
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl; return 0;
}

代码打印结果如下:

A()
1
1
~A()
~A()
free(): double free detected in tcache 2
Aborted (core dumped)

main函数中,虽然用了两个智能指针shared_ptr,但是它们管理的都是同一个资源,资源的引用计数应该是2,为什么打印出来是1呢?导致出main函数把A对象析构了两次,不正确!如果你有这样的疑问,说明对于shared_ptr的底层原理还没有完全搞清楚。

代码清单2

#include <iostream>
using namespace std;
// 智能指针测试类
class A {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
} // A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针。
shared_ptr<A> getSharedPtr() {
/*注意:不能直接返回this,在多线程环境下,根本无法获知this指针指向
的对象的生存状态,通过shared_ptr和weak_ptr可以解决多线程访问共享
对象的线程安全问题,参考我的另一篇介绍智能指针的博客*/
return shared_ptr<A>(this);
}
private:
int *m_ptr;
};
int main() {
shared_ptr<A> ptr1(new A());
shared_ptr<A> ptr2 = ptr1->getSharedPtr(); /* 按原先的想法,上面两个智能指针管理的是同一个A对象资源,但是这里打印都是1
导致出main函数A对象析构两次,析构逻辑有问题*/
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl; return 0;
}

代码运行结果打印如下:

A()
1
1
~A()
~A()
free(): double free detected in tcache 2
Aborted (core dumped)

代码同样有错误,A对象被析构了两次,而且看似两个shared_ptr指向了同一个A对象资源,但是资源计数并没有记录成2,还是1,不正确。

shared_ptr原理分析

如果你能够理解上面代码的问题所在,那么直接跳到下一节看上面错误代码的解决方案;如果不明白问题的所在,通过下面的源码介绍,仔细理解shared_ptr的实现原理。

源码上shared_ptr的定义如下:

template<class _Ty>
class shared_ptr
: public _Ptr_base<_Ty>

shared_ptr是从_Ptr_base继承而来的,作为派生类,shared_ptr本身没有提供任何成员变量,但是它从基类_Ptr_base继承来了如下成员变量(只罗列部分源码):

template<class _Ty>
class _Ptr_base
{ // base class for shared_ptr and weak_ptr
protected:
void _Decref()
{ // decrement reference count
if (_Rep)
{
_Rep->_Decref();
}
} void _Decwref()
{ // decrement weak reference count
if (_Rep)
{
_Rep->_Decwref();
}
}
private:
// _Ptr_base的两个成员变量,这里只罗列了_Ptr_base的部分代码
element_type * _Ptr{nullptr}; // 指向资源的指针
_Ref_count_base * _Rep{nullptr}; // 指向资源引用计数的指针
};

_Ref_count_base记录资源的类是怎么定义的呢?如下(只罗列部分源码):

class __declspec(novtable) _Ref_count_base
{ // common code for reference counting
private:
/**
* _Uses记录了资源的引用计数,也就是引用资源的shared_ptr的个数;
* _Weaks记录了weak_ptr的个数,相当于资源观察者的个数,都是定义成基于CAS操作的原子类型,增减引用计数时时线程安全的操作
**/
_Atomic_counter_t _Uses;
_Atomic_counter_t _Weaks;
}

也就是说,当我们定义一个shared_ptr<int> ptr(new int)的智能指针对象时,该智能指针对象本身的内存是8个字节,如下图所示:

那么把智能指针管理的外部资源以及引用计数资源都画出来的话,就是如下图的展示:

当你做这样的代码操作时:

shared_ptr<int> ptr1(new int);
shared_ptr<int> ptr2(ptr1);
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;

这段代码没有任何问题,ptr1和ptr2管理了同一个资源,引用计数打印出来的都是2,出函数作用域依次析构,最终new int资源只释放一次,逻辑正确!这是因为shared_ptr ptr2(ptr1)调用了shared_ptr的拷贝构造函数(源码可以自己查看下),只是做了资源的引用计数的改变,没有额外分配其它资源,如下图所示:

注意:两个shared_ptr对象引用的是同一个引用计数对象_Ref_count_base,依次析构的时候,最终资源new int只释放一次,正确

但是当你做如下代码操作时:

int *p = new int;
shared_ptr<int> ptr1(p);
shared_ptr<int> ptr2(p);
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;

这段代码就有问题了,因为shared_ptr<int> ptr1(p)shared_ptr<int> ptr2(p)都调用了shared_ptr的构造函数,在它的构造函数中,都重新开辟了引用计数的资源,导致ptr1ptr2都记录了一次new int的引用计数,都是1,析构的时候它俩都去释放内存资源,导致释放逻辑错误,如下图所示:

注意:两个shared_ptr对象都开辟了自己的引用计数对象_Ref_count_base,都记录new int资源的引用计数为1,析构的时候引用计数减到0,都认为自己该释放new int资源,错误!

上面两个代码段,分别是shared_ptr的构造函数和拷贝构造函数做的事情,导致虽然都是指向同一个new int资源,但是对于引用计数对象的管理方式,这两个函数是不一样的,构造函数是新分配引用计数对象,拷贝构造函数只做引用计数增减。

相信说到这里,大家知道最开始的两个代码清单上的代码为什么出错了吧,因为每次调用的都是shared_ptr的构造函数,虽然大家管理的资源都是一样的,_Ptr都是指向同一个堆内存,但是_Rep却指向了不同的引用计数对象,并且都记录引用计数是1,出作用域都去析构,使得同一块内存被析构多次,导致问题发生!

问题修改

代码清单1修改

那么清单1的代码修改很简单,就是在产生同一资源的多个shared_ptr的时候,通过拷贝构造函数或者赋值operator=函数进行,不要重新构造,避免产生多个引用计数对象,代码修改如下:

int main() {
A *p = new A(); // 裸指针指向堆上的对象 shared_ptr<A> ptr1(p); // 用shared_ptr智能指针管理指针p指向的对象
shared_ptr<A> ptr2(ptr1); // 用ptr1拷贝构造ptr2
// 下面两次打印都是2,最终随着ptr1和ptr2析构,资源只释放一次,正确!
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl; return 0;
}

代码清单2修改 enable_shared_from_this和shared_from_this

那么清单2代码怎么修改呢?注意我们有时候想在类里面提供一些方法,返回当前对象的一个shared_ptr强智能指针,做参数传递使用(多线程编程中经常会用到)。

首先肯定不能像上面代码清单2那样写return shared_ptr<A> (this),这会调用shared_ptr智能指针的构造函数,对this指针指向的对象,又建立了一份引用计数对象,加上main函数中的shared_ptr<A> ptr1(new A());已经对这个A对象建立的引用计数对象,又成了两个引用计数对象,对同一个资源都记录了引用计数,为1,最终两次析构对象释放内存,错误!

那如果一个类要提供一个函数接口,返回一个指向当前对象的shared_ptr智能指针怎么办?方法就是继承enable_shared_from_this类,然后通过调用从基类继承来的shared_from_this()方法返回指向同一个资源对象的智能指针shared_ptr

修改如下:

#include <iostream>
#include <memory>
using namespace std;
// 智能指针测试类,继承enable_shared_from_this类
class A : public enable_shared_from_this<A> {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
} // A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针
shared_ptr<A> getSharedPtr() {
/*通过调用基类的shared_from_this方法得到一个指向当前对象的智能指针*/
return shared_from_this();
}
private:
int *m_ptr;
};

一个类继承enable_shared_from_this会怎么样?看看enable_shared_from_this基类的成员变量有什么,如下:

template<class _Ty>
class enable_shared_from_this
{ // provide member functions that create shared_ptr to this
public:
using _Esft_type = enable_shared_from_this; _NODISCARD shared_ptr<_Ty> shared_from_this()
{ // return shared_ptr
return (shared_ptr<_Ty>(_Wptr));
}
// 成员变量是一个指向资源的弱智能指针
mutable weak_ptr<_Ty> _Wptr;
};

也就是说,如果一个类继承了enable_shared_from_this,那么它产生的对象就会从基类enable_shared_from_this继承一个成员变量_Wptr,当定义第一个智能指针对象的时候shared_ptr<A> ptr1(new A()),调用shared_ptr的普通构造函数,就会初始化A对象的成员变量_Wptr,作为观察A对象资源的一个弱智能指针观察者(在shared_ptr的构造函数中实现,有兴趣可以自己调试跟踪源码实现)。

然后代码如下调用shared_ptr<A> ptr2 = ptr1->getSharedPtr()getSharedPtr函数内部调用shared_from_this()函数返回指向该对象的智能指针,这个函数怎么实现的呢,看源码:

shared_ptr<_Ty> shared_from_this()
{ // return shared_ptr
return (shared_ptr<_Ty>(_Wptr));
}

shared_ptr<_Ty>(_Wptr),说明通过当前A对象的成员变量_Wptr构造一个shared_ptr出来,看看shared_ptr相应的构造函数:

shared_ptr(const weak_ptr<_Ty2>& _Other)
{ // construct shared_ptr object that owns resource *_Other
if (!this->_Construct_from_weak(_Other)) // 从弱智能指针提升一个强智能指针
{
_THROW(bad_weak_ptr{});
}
}

接着看上面调用的_Construct_from_weak方法的实现如下:

template<class _Ty2>
bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other)
{ // implement shared_ptr's ctor from weak_ptr, and weak_ptr::lock()
// if通过判断资源的引用计数是否还在,判定对象的存活状态,对象存活,提升成功;
// 对象析构,提升失败!之前的博客内容讲过这些知识,可以去参考!
if (_Other._Rep && _Other._Rep->_Incref_nz())
{
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
return (true);
} return (false);
}

综上所说,所有过程都没有再使用shared_ptr的普通构造函数,没有在产生额外的引用计数对象,不会存在把一个内存资源,进行多次计数的过程;更关键的是,通过weak_ptrshared_ptr的提升,还可以在多线程环境中判断对象是否存活或者已经析构释放,在多线程环境中是很安全的,通过this裸指针进行构造shared_ptr,不仅仅资源会多次释放,而且在多线程环境中也不确定this指向的对象是否还存活。

最终代码清单2修改如下:

#include <iostream>
#include <memory>
using namespace std;
// 智能指针测试类,继承enable_shared_from_this类
class A : public enable_shared_from_this<A> {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
} // A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针
shared_ptr<A> getSharedPtr() {
/*通过调用基类的shared_from_this方法得到一个指向当前对象的智能指针*/
return shared_from_this();
}
private:
int *m_ptr;
};
int main() {
shared_ptr<A> ptr1(new A());
shared_ptr<A> ptr2 = ptr1->getSharedPtr(); // 引用计数打印为2
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl; return 0;
}

代码打印结果如下:

A()
2
2
~A()

打印完全正确,A对象构造一次,析构一次,引用计数为2。

总结

以上就是对enable_shared_from_this和shared_from_this机制的介绍。这东西主要解决了该问题:当返回某对象时,由于智能指针调用常规的构造函数导致引用计数类的多次构造,从而导致在释放内存时,多个智能指针对同一块内存进行多次释放,出现Core dump。使用该机制则可返回指向某对象的智能指针,这样就调用的是智能指针的拷贝构造函数而非常规的构造函数,使得引用计数类不会被多次构造,避免出现同一内存多次释放的情况。

C++智能指针的enable_shared_from_this和shared_from_this机制的更多相关文章

  1. 智能指针shared_ptr新特性shared_from_this及weak_ptr

    enable_shared_from_this是一个模板类,定义于头文件<memory>,其原型为: template< class T > class enable_shar ...

  2. 深入学习c++--智能指针(二) weak_ptr(打破shared_ptr循环引用)

    1. 几种智能指针 1. auto_ptr: c++11中推荐不使用他(放弃) 2. shared_ptr: 拥有共享对象所有权语义的智能指针 3. unique_ptr: 拥有独有对象所有权语义的智 ...

  3. React Native 4 for Android源码分析 一《JNI智能指针之介绍篇》

    文/ Tamic: http://blog.csdn.net/sk719887916/article/details/53455441 原文:http://blog.csdn.net/eewolf/a ...

  4. 异常处理与MiniDump详解(2) 智能指针与C++异常

    write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie 讨论新闻组及文件 一.   综述 <异常处理与MiniDump详解(1) C++异常>稍 ...

  5. 引用内部函数绑定机制,R转义字符,C++引用,别名,模板元,宏,断言,C++多线程,C++智能指针

     1.引用内部函数绑定机制 #include<iostream> #include<functional> usingnamespacestd; usingnamespac ...

  6. (转)C++11里的智能指针

    1. std::auto_ptr有些违背c++编程思想. 已经被"不建议使用了".2. 下文转自:http://blog.csdn.net/lanergaming/article/ ...

  7. 使用TR1的智能指针

    作为C++程序员,在没有智能指针,手动管理内存的蛮荒岁月里,可以说是暗无天日,痛苦异常.直到上帝说,还是要有光,于是智能指针进了标准.C++码农的日子总算好起来了. 虽然一直鄙视着没有显式指针的语言, ...

  8. Boost智能指针使用总结

    内存管理是一个比较繁琐的问题,C++中有两个实现方案: 垃圾回收机制和智能指针.垃圾回收机制因为性能等原因不被C++的大佬们推崇, 而智能指针被认为是解决C++内存问题的最优方案. 1. 智能指针定义 ...

  9. Effective Modern C++:04智能指针

    裸指针有着诸多缺点:裸指针的声明中看不出它指向的是单个对象还是数组:裸指针的声明中也无法看出使用完它指向的对象后是否需要删除,也就是声明中看不出裸指针是否拥有其指向的对象:即使知道要析构裸指针指向的对 ...

随机推荐

  1. 057_末晨曦Vue技术_处理边界情况之强制更新($forceUpdate)与通过 v-once 创建低开销的静态组件

    强制更新($forceUpdate) 点击打开视频讲解更加详细 在vue中,如果data中有基本数据类型变量:age,修改他,页面会自动更新. 但如果data中的变量为数组或对象(引用数据类型),我们 ...

  2. 以太坊 layer2: optimism 源码学习(二) 提现原理

    作者:林冠宏 / 指尖下的幽灵.转载者,请: 务必标明出处. 掘金:https://juejin.im/user/1785262612681997 博客:http://www.cnblogs.com/ ...

  3. LGV 引理——二维DAG上 n 点对不相交路径方案数

    文章目录 引入 简介 定义 引理 证明 例题 释疑 扩展 引入 有这样一个问题: 甲和乙在一张网格图上,初始位置 ( x 1 , y 1 ) , ( x 2 , y 2 ) (x_1,y_1),(x_ ...

  4. 定时器控制单只LED灯

    点击查看代码 #include <reg51.h> #define uchar unsigned char #define uint unsigned int sbit LED=P0^0; ...

  5. 06_Linux基础-NGINX和浏览器、网页的关系-云服务器ssh登陆-安装NGINX-上传网页-压缩命令-xz-gzip-bzip2-zip-tar-配置NGINX服务器支持下载功能-备份脚本

    06_Linux基础-NGINX和浏览器.网页的关系-云服务器ssh登陆-安装NGINX-上传网页-压缩命令-xz-gzip-bzip2-zip-tar-配置NGINX服务器支持下载功能-备份脚本 一 ...

  6. DispatcherServlet 分发流程

    0 太长不看版 HTTPServlet 的 Service 方法将请求按类进行分解 主要是根据HTTP方法的类型调用 doXXX 方法 GET 和 HEAD 方法需要对 if-modified-sin ...

  7. C++中的STL大法整理

    C++中的STL大法整理 由于碰到了一些不知道怎么用的STL vector vector是数组的STL,对于普通数组的优势就在于,可以动态地变化数组长度.那么面对一些数据范围非常大而又可以边读入边处理 ...

  8. 记一次 .NET 某数控机床控制程序 卡死分析

    一:背景 1. 讲故事 前段时间有位朋友微信上找到我,说它的程序出现了卡死,让我帮忙看下是怎么回事? 说来也奇怪,那段时间求助卡死类的dump特别多,被迫训练了一下对这类问题的洞察力 ,再次声明一下, ...

  9. KingbaseES R3 集群pcp_attach_node 更新show pool_nodes中节点状态

    系统环境: 操作系统: [kingbase@node2 bin]$ cat /etc/centos-release CentOS Linux release 7.2.1511 (Core) 数据库: ...

  10. C语言输入输出格式符

    C语言输入输出格式符 printf函数(格式输出函数) 1.一般格式 printf(格式控制,输出表列) 例如:printf("i=%d,ch=%c\n",i,ch); 说明: ( ...