std::shared_ptr<void>的工作原理
前戏
先抛出两个问题
- 如果
delete一个指针,但是它真实的类型和指针类型不一样会发生什么? - 是谁调用了析构函数?
下面这段代码会发生什么有趣的事情?
// delete_diff_type.cpp
#include <iostream> using namespace std; class Foo
{
public:
Foo() { cout << "Foo()" << endl; }
~Foo() { cout << "~Foo()" << endl; }
}; class FakeFoo
{
public:
FakeFoo() { cout << "FakeFoo()" << endl; }
~FakeFoo() { cout << "~FakeFoo()" << endl; }
}; int main(int argc, const char * argv[]) { void* vptr = new Foo();
delete vptr; // warning // FakeFoo* ffptr = static_cast<FakeFoo*>(new Foo()); // error
FakeFoo* ffptr = reinterpret_cast<FakeFoo*>(new Foo());
delete ffptr; Foo* fptr = new Foo();
delete fptr; return ;
} 输出:
Foo()
Foo()
~FakeFoo()
Foo()
~Foo()
看一下汇编代码可以看到main函数主要做了下面这几件事
; symbol stub for: operator new(unsigned long)
; Foo::Foo at delete_diff_type.cpp:
; symbol stub for: operator delete(void*) ; symbol stub for: operator new(unsigned long)
; Foo::Foo at delete_diff_type.cpp:
; FakeFoo::~FakeFoo at delete_diff_type.cpp:
; symbol stub for: operator delete(void*) ; symbol stub for: operator new(unsigned long)
; Foo::Foo at delete_diff_type.cpp:
; Foo::~Foo at delete_diff_type.cpp:
; symbol stub for: operator delete(void*)
从汇编中可以看出,构造造函数和析构函数是编译器根据指针的类型生成的调用代码。而且编译器是不允许没有继承关系的指针之间进行转换的,void* 是个例外,只要不作死用reinterpret_cast把指针转换成不相关的类型是不会有问题的。
所以上面两个问题大概就有答案了。delete语句会至少产生两个动作,一个是调用指针对应类型的析构函数,然后去调用operator delete释放内存。所以如果delete的指针和其指向的真实类型不一样的时候,编译器只会调用指针类型的析构函数,这也就为什么基类的析构函数需要声明称虚函数才能够保证delete基类指针的时候子类析构函数能够被正确的调用。
operator delete是都会被调用到的,所以指针指向的那块内存是能够“正常的”被释放掉用。
std::shared_ptr<void> 的行为
那么这个跟std::shared_ptr<void> 有什么关系呢?
先看一段代码
#include <iostream> using namespace std; class Foo
{
public:
Foo() { cout << "Foo()" << endl; }
~Foo() { cout << "~Foo()" << endl; }
}; int main(int argc, const char * argv[]) {
shared_ptr<void> vptr = shared_ptr<Foo>(new Foo);
return ;
}
输出:
Foo()
~Foo()
与第一段代码中类似,不过把void*换成了std::shared_ptr<void>,那么shared_ptr<void>为什么能够调用到正确的析构函数呢?一定是shared_ptr里面搞了什么鬼。
std::shared_ptr<void> 为啥能正常工作
那么就看看源代码看看到底为啥这货能够工作,下面是libcxx中shared_ptr的部分源码,把关键的部分抠出来了。
/** C1. shared_ptr 构造函数 **/ template<class _Tp>
template<class _Yp>
shared_ptr<_Tp>::shared_ptr(_Yp* __p,
typename enable_if<is_convertible<_Yp*, element_type*>::value, __nat>::type)
: __ptr_(__p)
{
unique_ptr<_Yp> __hold(__p);
typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT;
typedef __shared_ptr_pointer<_Yp*, default_delete<_Yp>, _AllocT > _CntrlBlk;
__cntrl_ = new _CntrlBlk(__p, default_delete<_Yp>(), _AllocT());
__hold.release();
__enable_weak_this(__p, __p);
} /** C2. shared_ptr 拷贝构造 **/
template<class _Tp>
inline
shared_ptr<_Tp>::shared_ptr(const shared_ptr& __r) _NOEXCEPT
: __ptr_(__r.__ptr_),
__cntrl_(__r.__cntrl_)
{
if (__cntrl_)
__cntrl_->__add_shared();
} /** C3. shared_ptr::__cntrl_ 类型 **/
__shared_weak_count* __cntrl_; /** C4. shared_ptr 析构函数 **/ template<class _Tp>
shared_ptr<_Tp>::~shared_ptr()
{
if (__cntrl_)
__cntrl_->__release_shared();
} /** C5. __shared_weak_count::__release_shared **/ bool __release_shared() _NOEXCEPT {
if (__libcpp_atomic_refcount_decrement(__shared_owners_) == -) {
__on_zero_shared();
return true;
}
return false;
} /** C6. __shared_ptr_pointer::__release_shared **/
template <class _Tp, class _Dp, class _Alloc>
/* 重点 !!! virtual !!! */
void
__shared_ptr_pointer<_Tp, _Dp, _Alloc>::__on_zero_shared() _NOEXCEPT
{
// __data__ 是 内部工具类__compressed_pair
// __data_.first().second()是 deleter
// __data_.first().first() 是 shared_ptr<T> 中T类型的指针
__data_.first().second()(__data_.first().first());
__data_.first().second().~_Dp();
} /** C7. default deleter **/ template <class _Tp>
struct _LIBCPP_TEMPLATE_VIS default_delete {
// ... 此处省略若干行
void operator()(_Tp* __ptr) const _NOEXCEPT {
delete __ptr;
}
};
不要慌,给你慢慢道来。
这个故事简单得说是这样的:
- 每一个shared_ptr 内部有一个control block,里面会存放一个要维护的指针,一个计数,一个删除器(deleter),一个分配(allocator)。这里我们要关心的是删除器。顾名思义,它是用来删除指针的。
- shared_ptr中的 有一个__cntrl_即control block。字段的类型是__shared_weak_count指针,这个类是一个非模板类。shared_ptr<T>创建的control block的类型是一个类模板template <class _Tp, class _Dp, class _Alloc> __shared_ptr_pointer继承自__shared_weak_count。 见代码C3。
- 当创建一个新的shared_ptr的时候,构建一个新的control block。见代码C1。
- 当一个shared_ptr A赋值或者拷贝构造给另一个shared_ptrB的时候(当然是在类型能够转换的前提下),B会把A的__cntrl_拷一份,同时将其引用加一。注意,这个时候拷贝的是指针,__cntrl_还是指向最初创建的那个对象。见代码C2。
- 当shared_ptr被析构或者重置的时候会调用__cntrl_->__release_shared()。见代码C4。
- __cntrl_->__release_shared()如果发现当前的计数为-1的时候,调用__on_zero_shared()。见代码C5。
- __on_zero_shared是一个虚方法,那么它就会调用到最初创建的__shared_ptr_pointer的实现。
- __shared_ptr_pointer的实现中是有完整的类型和删除器的信息的。见代码C6。
- 默认的删除器很简单的执行了delete __ptr操作,因为类型是已知的所以能够正确的调用到析构函数。见代码C7。
这种实现方式给shared_ptr带来额外好处
Effective C++ 条款07告诉我们“要为多态基类声明 virtual 析构函数”。当然我认为建议依然有效,但是用了shared_ptr以后带来的一个额外好处就是即便你的析构函数忘记写成virtual也能帮你正确的调用析构函数。
std::shared_ptr<void>的工作原理的更多相关文章
- std::shared_ptr之deleter的巧妙应用
本文由作者邹启文授权网易云社区发布. std::shared_ptr 一次创建,多处共享,通过引用计数控制生命周期. 实例 在邮箱大师PC版中,我们在实现搜索时,大致思路是这样的: 每一个账号都有一个 ...
- 虚函数列表: 取出方法 // 虚函数工作原理和(虚)继承类的内存占用大小计算 32位机器上 sizeof(void *) // 4byte
#include <iostream> using namespace std; class A { public: A(){} virtual void geta(){ cout < ...
- 巧用std::shared_ptr全局对象释放单例内存
巧用std::shared_ptr 单例的使用相对比较广泛,但是需要在程序退出前调用它的析构函数对数据进行释放,常规做法是在main函数末尾进行释放工作, 但是这样相对比较繁琐,因此便有了利用全局变量 ...
- 基于C++11实现线程池的工作原理
目录 基于C++11实现线程池的工作原理. 简介 线程池的组成 1.线程池管理器 2.工作线程 3.任务接口, 4.任务队列 线程池工作的四种情况. 1.主程序当前没有任务要执行,线程池中的任务队列为 ...
- C++虚函数工作原理
一.虚函数的工作原理 虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数.典型情况下,这一信息具有一种被称为 vptr(virtual table poi ...
- std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题
在std::shared_ptr被引入之前,C++标准库中实现的用于管理资源的智能指针只有std::auto_ptr一个而已.std::auto_ptr的作用非常有限,因为它存在被管理资源的所有权转移 ...
- C++虚函数的工作原理
静态绑定与动态绑定 讨论静态绑定与动态绑定,首先需要理解的是绑定,何为绑定?函数调用与函数本身的关联,以及成员访问与变量内存地址间的关系,称为绑定. 理解了绑定后再理解静态与动态. 静态绑定:指在程序 ...
- 《浏览器工作原理与实践》<11>this:从JavaScript执行上下文的视角讲清楚this
在上篇文章中,我们讲了词法作用域.作用域链以及闭包,接下来我们分析一下这段代码: var bar = { myName:"time.geekbang.com", printName ...
- 菜鸟学Struts2——Struts工作原理
在完成Struts2的HelloWorld后,对Struts2的工作原理进行学习.Struts2框架可以按照模块来划分为Servlet Filters,Struts核心模块,拦截器和用户实现部分,其中 ...
随机推荐
- 基于BroadReceiver实现获取短信内容
我朋友拜托我做一个能实现向指定号码发短信获取动态密码的一个小app,中间用到了基于监听系统通知的BroadReceiver 来实现获取有新短信并且获取新短信的内容.下面就是这个小app的实现监听部分的 ...
- Loadrunner web_url函数学习(转贴)
http://blog.csdn.net/dfbrt56/article/details/3291461 ----------------------------------------------- ...
- RPC是什么
RPC是什么? 通俗的讲就是,调用远程计算机上的服务,就像调用本地服务一样.通常包含传输协议和编码协议. RPC可以基于HTTP或TCP协议,但基于HTTP协议的RPC性能却不如基于TCP协议的RPC ...
- 大道至简第一章Java伪代码读后感
import.java.大道至简.*; //周爱民 import.java.读后感*; public class shawanyia { //1. 编程的精义 while(山没有消失) {if(愚公没 ...
- 【jQuery插件】使用cropper实现简单的头像裁剪并上传
插件介绍 这是一个我在写以前的项目的途中发现的一个国人写的jQuery图像裁剪插件,当时想实现用户资料的头像上传功能,并且能够预览图片,和对图片进行简单的裁剪.旋转,花了不少时间才看到了这个插件,感觉 ...
- Shell编程基础篇-下
1.1 条件表达式 1.1.1 文件判断 常用文件测试操作符 常用文件测试操作符 说明 -d文件,d的全拼为directory 文件存在且为目录则为真,即测试表达式成立 -f文件,f的全拼为file ...
- 自己动手实现mybatis动态sql
发现要坚持写博客真的是一件很困难的事情,各种原因都会导致顾不上博客.本来打算写自己动手实现orm,看看时间,还是先实现一个动态sql,下次有时间再补上orm完整的实现吧. 用过mybatis的人,估计 ...
- 实践作业2:黑盒测试实践——在被测系统上录制脚本 Day 5
1.处理了学生信息管理系统运行的小细节 2.总结了katalon的使用流程如下 a.在Katalon Studio中创建一个自动化测试项目. b.创建一个测试案例,用来写入所有必要的测试步骤 c.编写 ...
- Android Studio问题汇总
1) Android Studio重命名工程名称: 1. 关闭Android Studio 2. 修改project所在路径的文件夹名字为[NewName] 3. 修改根目录下的.iml文件名为[Ne ...
- UWP 显示图片到Image控件
要想显示图片,前提是要有一个空间的啦 <Image x:Name="imageHidden"/> 然后一个响应选择图片得事件,注意使用asynchronous方法哦 F ...