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核心模块,拦截器和用户实现部分,其中 ...
随机推荐
- window环境下npm install node-sass报错
最近准备想用vue-cli初始化一个项目,需要sass-loader编译: 发现window下npm install node-sass和sass-loader一直报错, window 命令行中提示我 ...
- 使用vue2.x+webpack+vuex+sass+axios+elementUI等快速搭建前端项目框架
一.本文将分享如何快速搭起基于webpack+vue的前端项目框架,利用vue的自己的脚手架工具vue-cli搭建起基本的环境配置,再通过npm包管理工具引入相应的依赖来完善项目的各种依赖框架.下面是 ...
- eclipse中Maven工程使用Tomcat7以上插件
Maven中使用tomcat:run命令默认是使用Tomcat6的版本, 现在要用到Tomcat7以上的版本,在eclipse的Maven工程中配置如下 第一步:在项目的pom里面加入如下配置: 官网 ...
- iis7 部署网站 403错误
C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -i 403 - 禁止访问: 访问被拒绝. 您无权使用所提供的凭据查看此 ...
- 常用接口简析2---IComparable和IComparer接口的简析
常用接口的解析(链接) 1.IEnumerable深入解析 2.IEnumerable.IEnumerator接口解析 3.IList.IList接口解析 默认情况下,对象的Equals(object ...
- 安装apache
1.安装依赖软件 pcre gcc expat apr(Apache portable Run-time libraries,Apache可移植运行库) apr-util [root@localhos ...
- 在azure中建立DDC集群
在Azure上建立DDC集群 建立3台虚拟机作为UCP集群,3台虚拟机作为DTR集群. 资源组:HYD-DockerDateCenter 虚拟机size:DS3 4核14G 系统:ubuntu14.0 ...
- 将百度的ECharts整合到阿里的Weex中。
由于公司的业务,之前PC版产品中,大量的使用了百度的ECharts库.所以现在要做移动端,在大概熟悉了Weex基本语法和搭建环境后,就着手研究如何将这两个好东西糅合起来. 首先,按照Weex官方教程, ...
- POJ2251-Dungeon Master
题意:给出一三维空间的地牢,要求求出由字符'S'到字符'E'的最短路径移动方向可以是上,下,左,右,前,后,六个方向,每移动一次就耗费一分钟,要求输出最快的走出时间.不同L层的地图,相同RC坐标处是连 ...
- 洛谷教主花园dp
洛谷-教主的花园-动态规划 题目描述 教主有着一个环形的花园,他想在花园周围均匀地种上n棵树,但是教主花园的土壤很特别,每个位置适合种的树都不一样,一些树可能会因为不适合这个位置的土壤而损失观赏价 ...