C++ 智能指针 shared_ptr 分析
引文:
C++对指针的管理提供了两种解决问题的思路:
1.不允许多个对象管理一个指针
2.允许多个对象管理一个指针,但仅当管理这个指针的最后一个对象析构时才调用delete
ps:这两种思路的共同点就是只允许delete一次,下面将讨论的shared_ptr就是采用思路1实现的
ps:智能指针不是指针,而是类,可以实例化为一个对象,来管理裸指针
1.shared_ptr的实现原理:
shared_ptr最本质的功能:“当多个shared_ptr管理同一个指针,仅当最后一个shared_ptr析构时,指针才被delete”,该功能是通过引用计数法实现的
引用计数法的规则:
1)所有管理同一个裸指针的shared_ptr,都共享一个引用计数器
2)每当一个shared_ptr被赋值给其他shared_ptr时,这个共享的引用计数器就加1
3)每当一个shared_ptr析构或被用于管理其他裸指针时,这个引用计数器就减1
4)如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个shared_ptr了,于是我们释放指针指向的资源
引用计数法的内部实现:
1)这个引用计数器保存在某个内部类型中,而这个内部类型对象在shared_ptr第一次构造时以指针的形式保存在shared_ptr中
2)shared_ptr重载了赋值运算符,在赋值和拷贝另一个shared_ptr时,这个指针被另一个shared_ptr共享
3)在引用计数归0时,这个内部类型指针与shared_ptr管理的资源一起释放
4)此外,为了保证线程安全,引用计数器的加1和减1都是原子操作,它保证了shared_ptr由多个线程共享时不会爆掉
2.shared_ptr的使用
#include<iostream>
#include<stdio.h>
#include<string>
#include<memory>
using namespace std; int main()
{
//初始化 方法1:
shared_ptr<string> sptr1(new string("name"));
//初始化 方法2:
shared_ptr<string> sptr2=make_shared<string>("sex");
//初始化 方法3:
int *p =new int(10);
shared_ptr<int> sptr3(p); //这种初始化的方式很危险,delete p之后,strp3也不再有效
}
相关成员函数:
1)use_count:返回引用计数的个数
2)unique:返回是否独占所有权(use_count=1)
3)swap:交换两个share_ptr对象(即交换所拥有的对象)
4)reset:放弃内部对象的所有权或拥有对象的变更,会引起原有对象引用计数的减少
5)get:返回内部对象指针
3.引用计数最大的缺点:循环引用
下面是事故现场:
class Observer; // 前向声明
class Subject
{
private: std::vector<shared_ptr<Observer>> observers;
public:
Subject() {}
addObserver(shared_ptr<Observer> ob)
{
observers.push_back(ob);
}
// 其它代码
}; class Observer
{
private:
shared_ptr<Subject> object;
public:
Observer(shared_ptr<Object> obj) : object(obj) {} // 其它代码
};
目标类subject连接这多个观察者类,当某个事件发生时,目标类可以遍历观察者数组observers,对观察者进行通知,而观察者类中也保留着目标类的shared_ptr,这样多个观察者之间可以以目标类为桥梁进行沟通,除了会发生内存泄漏外,这还是一种很不错的设计模式嘛……
这里产生内存泄漏的原因就是循环引用,循环引用指的是一个引用通过一系列的引用链,竟然引回到自身,在上面的例子中,subject->observer->subject就是这么一条环形引用链,假设我们程序中只有一个变量shared_ptr<sbuject> p,此时p指向的对象不仅通过shared_ptr引向自己,还通过它包含的observer中的object成员变量引回自己,于是它的引用计数是2,每个observer的引用计数都是1,当p析构时,它的引用计数2-1=1,大于0,其析构函数不会被调用,于是p和它包含的每个observer对象在程序结束时依然驻留在内存中,没有被delete,从而造成了内存泄漏
4.采用weak_ptr(弱引用)解决循环引用的问题:
标准库提供了std::weak_ptr,weak_ptr是shared_ptr的观察者,它与一个shared_ptr绑定,但是却不参与引用计数的计算,在需要时,它还能生成一个与它所观察的shared_ptr共享引用计数器的新的shared_ptr,总而言之,weak_ptr的作用就是:在需要时生成一个与绑定的shared_ptr共享引用计数器的新shared_ptr,在其他时候不干扰绑定的shared_ptr的引用计数
weak_ptr相关成员函数:
1)lock:获得一个和绑定的shared_ptr共享引用计数器的新的shared_ptr
2)expired:功能等价于判断use_count是否等于0,但是速度更快
继续引用上面subject和observer的例子,来解决循环引用的问题:
将上述例子中,observer中object成员的类型换成weak_ptr<subject>即可解决内存泄漏的问题,因为之前的observer中object成员的subject参与了引用计数,替换成weak_ptr<subject>之后没有参与引用计数,这样以来,p指向对象的引用计数为1,所以在p析构时,subject指针将被delete,其中包含的observer数组在析构时,内部的observer对象的引用计数也为0,所以他们也被deleete了,不存在内存泄漏的问题了
class Observer; // 前向声明
class Subject
{
private: std::vector<shared_ptr<Observer>> observers;
public:
Subject() {}
addObserver(shared_ptr<Observer> ob)
{
observers.push_back(ob);
}
// 其它代码
}; class Observer
{
private:
shared_ptr< weak_ptr<Subject> > object;
public:
Observer(shared_ptr<Object> obj) : object(obj) {} // 其它代码
};
5.错误用法1:多个无关的shared_ptr管理同一个裸指针,有可能导致二次析构
int main()
{
int *a = new int(10); shared_ptr<int> p1(a); shared_ptr<int> p2(a);
}
p1和p2管理同一个裸指针a,此时的p1和p2有着完全独立的两个引用计数器,所以p1析构的时候会将a析构一次,p2析构的时候也会将a析构一次,C++中不允许同一个东西被析构两次,这样会导致程序爆炸
为了避免这种情况,我们永远不要将new用在shared_ptr构造函数列表以外的地方,或者干脆不用new,改用make_shared
另外,即使这样,也有可能导致二次析构,比如我们采用shared_ptr的get函数获得原始裸指针来构造另一个shared_ptr
class A
{
public:
std::shared_ptr<A> getShared()
{
return std::shared_ptr<A>(this);
}
}; int main()
{
std::shared_ptr<A> pa = std::make_shared<A>();
std::shared_ptr<A> pbad = pa->getShared();
}
上面的样例中,pa和pbad各自拥有一个独立的引用计数器,也有可能会导致二次析构
总而言之:管理同一个资源的sahred_ptr,只能由同一个初始shared_ptr通过一系列赋值和拷贝构造得到,要确保其共享的是同一个引用计数器
6.错误用法2:直接用new构造多个shared_ptr作为实参,可能会导致内存泄漏
// 声明
void f(A *p1, B *p2); // 使用
f(new A, new B);
上面的代码很容易发生内存泄漏,假如new A先发生于new B,那么如果new B抛出异常,那么new A的分配将会发生泄漏
如果按照这种方式new多个share_ptr作为实参,依然会发生内存泄漏
//声明
void f(shared_ptr<A> p1,shared_ptr<B> p2); //使用
f(shared_ptr<A> (new A),shared_ptr<B>(new B));
因为shared_ptr的构造有可能发生在new A和new B之后,这里涉及到C++操作的sequence after性质,该性质保证:
1)new A发生在shared_ptr<A>构造发生之前
2)new B发生在shared_ptr<B>构造发生之前
3)两个shared_ptr的构造发生在函数f的调用之前
在满足上面三条性质的前提下,各操作的顺序可以任意执行
若不使用new而是使用make_shared来构造shared_ptr,那么就不会产生内存泄漏
//声明
void f(shared_ptr<A> p1,shared_ptr<B> p2); //使用
f(make_shared<A>(),make_shared<B>());
原因很简单,依然是sequence after性质,如果两个函数的执行顺序不确定,那么当一个函数执行时,另外一个函数不会执行,于是make_shared<A>的构造完成了,即使make_shared<B>的构造抛出了异常,那么A的资源也能够被正确的释放,和上面的情形相比较,make_shared保证了第二个new发生的时候,第一个new所分配的资源已经被shared_ptr管理起来了,所以在异常发生时,能够正确的释放资源
总结:请总是使用make_shared来生成shared_ptr
7.如果希望使用shared_ptr来管理动态数组,那么需要提供一个自定义的删除器来代替delete
#include <iostream>
#include<memory>
using namespace std; class DelTest
{
public:
DelTest(){
j= 0;
cout<<" DelTest()"<<":"<<i++<<endl;
}
~DelTest(){
i = 0;
cout<<"~ DelTest()"<<":"<<i++<<endl;
}
static int i,j;
}; int DelTest::i = 0;
int DelTest::j = 0; void noDefine()
{
cout<<"no_define start running!"<<endl;
shared_ptr<DelTest> p(new DelTest[10]); } void slefDefine()
{
cout<<"slefDefine start running!"<<endl;
shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!传入lambada表达式代替delete操作。
} int main()
{
noDefine();//!构造10次,析构1次。内存泄漏。
cout<<"--------------------"<<endl;
slefDefine();//!构造次数==析构次数 无内存泄漏
}
/*
运行结果:
no_define start running!
DelTest():0
DelTest():1
DelTest():2
DelTest():3
DelTest():4
DelTest():5
DelTest():6
DelTest():7
DelTest():8
DelTest():9
~ DelTest():0
--------------------
slefDefine start running!
DelTest():1
DelTest():2
DelTest():3
DelTest():4
DelTest():5
DelTest():6
DelTest():7
DelTest():8
DelTest():9
DelTest():10
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
*/
需要注意的是:虽然通过自定义删除器的方式shared_ptr可以管理动态数组,但是shared_ptr并不支持下标运算符的操作,而且只能指针类型不支持指针算术运算(不能取地址),因此为了访问数组中的元素,必须用get获得一个原始内置裸指针,然后用它来访问数组元素
样例如下:
#include <iostream>
#include<memory>
using namespace std; class DelTest
{
public:
DelTest(){
j= 0;
x=i;
cout<<" DelTest()"<<":"<<i++<<endl;
}
~DelTest(){
i = 0;
cout<<"~ DelTest()"<<":"<<i++<<endl;
}
static int i,j;
int x;
}; int DelTest::i = 0;
int DelTest::j = 0; void noDefine()
{
cout<<"no_define start running!"<<endl;
shared_ptr<DelTest> p(new DelTest[10]); } void slefDefine()
{
cout<<"slefDefine start running!"<<endl;
shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!传入lambada表达式代替delete操作。
cout<<p.get()[4].x<<endl; } int main()
{
noDefine();//!构造10次,析构1次。内存泄漏。
cout<<"--------------------"<<endl;
slefDefine();//!构造次数==析构次数 无内存泄漏
}
/*
运行结果:
no_define start running!
DelTest():0
DelTest():1
DelTest():2
DelTest():3
DelTest():4
DelTest():5
DelTest():6
DelTest():7
DelTest():8
DelTest():9
~ DelTest():0
--------------------
slefDefine start running!
DelTest():1
DelTest():2
DelTest():3
DelTest():4
DelTest():5
DelTest():6
DelTest():7
DelTest():8
DelTest():9
DelTest():10
5
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
*/
8.使用shared_ptr管理非常规的动态对象的时候,记得自定义删除器
某些情况下,有些动态内存也不是我们new出来的,如果要使用shared_ptr管理这种动态内存,也要自定义删除器
#include <iostream>
#include <stdio.h>
#include <memory>
using namespace std; void closePf(FILE * pf)//即可以避免异常发生后无法释放内存的问题,也避免了很多人忘记执行fclose
{
cout<<"----close pf after works!----"<<endl;
fclose(pf);
} int main()
{
shared_ptr<FILE> pf(fopen("bin2.txt", "w"),closePf);
cout<<"*****start working****"<<endl;
if(!pf)
return -1;
char *buf = "abcdefg";
fwrite(buf,8,1,pf.get());//确保fwrite不会删除指针的情况下,可以将shared_ptr内置指针取出
cout<<"------write in file!-----"<<endl;
}
/*
*****start working****
------write in file!-----
----close pf after works!----
*/
类比TCP/IP中连接打开和关闭的情况,同理都可以使用shared_ptr来管理
总结:
1)不用使用相同的内置/原始/裸指针初始化多个智能指针
2)不要delete get函数返回的指针
3)如果你使用了get返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
4)如果你使用的智能指针管理的资源不是new分配的内存,记得传递一个删除器
5)请勿使用new构造多个shared_ptr作为实参,应该使用make_shared
6)存在循环引用关系时,请使用weak_ptr来保证不会产生内存泄漏
C++ 智能指针 shared_ptr 分析的更多相关文章
- STL源码剖析-智能指针shared_ptr源码
目录一. 引言二. 代码实现 2.1 模拟实现shared_ptr2.2 测试用例三. 潜在问题分析 你可能还需要了解模拟实现C++标准库中的auto_ptr一. 引言与auto_ptr大同小异,sh ...
- c/c++ 智能指针 shared_ptr 和 new结合使用
智能指针 shared_ptr 和 new结合使用 用make_shared函数初始化shared_ptr是最推荐的,但有的时候还是需要用new关键字来初始化shared_ptr. 一,先来个表格,唠 ...
- c/c++ 智能指针 shared_ptr 使用
智能指针 shared_ptr 使用 上一篇智能指针是啥玩意,介绍了什么是智能指针. 这一篇简单说说如何使用智能指针. 一,智能指针分3类:今天只唠唠shared_ptr shared_ptr uni ...
- C++智能指针shared_ptr
shared_ptr 这里有一个你在标准库中找不到的—引用数智能指针.大部分人都应当有过使用智能指针的经历,并且已经有很多关于引用数的文章.最重要的一个细节是引用数是如何被执行的—插入,意思是说你将引 ...
- 智能指针 shared_ptr 解析
近期正在进行<Effective C++>的第二遍阅读,书里面多个条款涉及到了shared_ptr智能指针,介绍的太分散,学习起来麻烦.写篇blog整理一下. LinJM @HQU s ...
- C++ 智能指针Auto_PTR 分析
C++的动态内存的分配与释放是个挺折磨人的事情,尤其异常分支复杂时(比如一堆try catch中,各catch里需要做delete 掉相关的堆上分配的内存),极有可能产生内存泄露的情况.C++中提供了 ...
- 智能指针shared_ptr的用法
为了解决C++内存泄漏的问题,C++11引入了智能指针(Smart Pointer). 智能指针的原理是,接受一个申请好的内存地址,构造一个保存在栈上的智能指针对象,当程序退出栈的作用域范围后,由于栈 ...
- 智能指针shared_ptr
// 智能指针会自动释放所指向的对象. // shared_ptr的应用场景是:程序需要在多个对象间共享数据 /* 先从应用场景入手吧,说矿工A发现了一个金矿. * 然后矿工A喊来了矿工B,一起开采, ...
- 智能指针shared_ptr新特性shared_from_this及weak_ptr
enable_shared_from_this是一个模板类,定义于头文件<memory>,其原型为: template< class T > class enable_shar ...
随机推荐
- HDU6704 K-th occurrence
[传送门] 先求出SA和height.然后找到 rank[l] 的 height 值.能成为相同子串的就是和rank[l]的lcp不小于 $len$ 的.二分出左右端点之后,主席树求第k小即可. #i ...
- python3.7 win10配置opencv和扩展库
- JS的ES6的Symbol
一.Symbol 1.什么是Symbol: Symbol是ES6新添加的原始类型(ES5已有原始数据类型:String,Number,boolean,function,undefined,object ...
- 在vb.net中使用委托:经理 和 员工
现在开发的一个 vb.net系统,其中有两个窗体:alert窗体和 case窗体. 在alert窗体中列出了当前可以操作的若干个alert(可以理解为数据记录),用户可以选择将其中一个或几个alert ...
- MySql的执行计划
一.什么是数据库执行计划: MySQL执行计划是sql语句经过查询优化器后,查询优化器会根据用户的sql语句所包含的字段和内容数量等统计信息,选择出一个执行效率最优(MySQL系统认为最优)的执行计划 ...
- js之select三级联动
效果图如下: 代码逻辑梳理:层层递进,比如选择了课程后,将对应的课程id保存,然后点击选择章时自动触发对应的时间,根据这个课程ID获取其下面的章信息.其它的如节等,同理. 代码说明:如下代码不规范,可 ...
- 三个面向对象相关的装饰器@property@staticmathod@classmethod
@property 先看实例: from math import pi class Circle: def __init__(self,r): self.r = r @property def per ...
- android测试和iOS测试的区别
一.常识性区别 二.导航方式 iOS:Tab放在页面底部,不能通过滑动来切换,只能点击.也有放在上面的,也不能滑动,但有些Tab本身可以滑动,比如天猫的.还有新闻类的应用. Android:一般放在页 ...
- pdf 中画虚线
<?php require('fpdf.php'); class PDF_Dash extends FPDF { function SetDash($black=null, $white=nul ...
- 【PHP】php实现二进制、八进制、十进制、十六进制之间各自转换的函数
<?php /* 常见的进制: 二进制 binary -----> bin 八进制 octal -----> oct 十进制 decimal -----> dec 十六进制 h ...