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_ptr
B的时候(当然是在类型能够转换的前提下),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核心模块,拦截器和用户实现部分,其中 ...
随机推荐
- Android Weekly Notes Issue #284
November 19th, 2017 Android Weekly Issue #284 本期内容丰富.有趣的有如何搭建真机测试平台,Proguard里面各类keep的区别,如何运行时获得泛型类型, ...
- android shape 大全 (转)
1. 各属性的配置语法 在项目 res/drawable 文件夹中创建一个以 shape 为根节点的 XML 文件,基本语法如下: <?xml version="1.0" e ...
- springboot-mybatis 批量insert
springboot mybatis 批量insert 操作 直接上代码: 1.首先要在pom.xml中导入包: 略...... 2.springboot mybatis配置: package com ...
- 【阿里聚安全·安全周刊】阿里双11技术十二讲直播预约|AWS S3配置错误曝光NSA陆军机密文件
关键词:阿里双11技术十二讲直播丨雪人计划丨亚马逊AWS S3配置错误丨2018威胁预测丨MacOS漏洞丨智能风控平台MTEE3丨黑客窃取<权利的游戏>剧本|Android 8.1 本 ...
- redis配置文件之复制
主从复制使用slaveof将Redis实例作为另一个Redis服务器的副本. 1) Redis复制是异步的,master可以配置成如果它连接的slave没有达到给定的数量,就停止接受写入.2) 如果断 ...
- opencv摄像头捕获图像
#include <iostream> #include <opencv2/opencv.hpp> using namespace cv; using namespace st ...
- 2.1synchronized同步方法
由前言: 在第一章已经出现了非线程安全的情况."非线程安全"其实会发生在多个线程同时对同一个对象中的实例变量进行访问时发生.产生的结果就是脏读(读到被修改过的数据). " ...
- 51Nod--1012最小公倍数
1012 最小公倍数LCM 基准时间限制:1 秒 空间限制:131072 KB 分值: 0 难度:基础题 收藏 关注 输入2个正整数A,B,求A与B的最小公倍数. Input 2个数A,B,中间用 ...
- mysql 索引类型
根据类型分为普通索引2种类型,hash 和b-tree 最常用 hash是按一对一索引的.速度 最快但不支持范围 比如where name = 'dd' 最快.但是使用 date >3 ...
- PhpStorm连接服务器,开始自动上传功能
连接服务器 菜单栏找到[工具/Tools]->[Deployment/部署]->[Confinguration-/配置-]. 点加号(+),添加一台服务器,填写名称,选择类型为SFTP,点 ...