引文:

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 分析的更多相关文章

  1. STL源码剖析-智能指针shared_ptr源码

    目录一. 引言二. 代码实现 2.1 模拟实现shared_ptr2.2 测试用例三. 潜在问题分析 你可能还需要了解模拟实现C++标准库中的auto_ptr一. 引言与auto_ptr大同小异,sh ...

  2. c/c++ 智能指针 shared_ptr 和 new结合使用

    智能指针 shared_ptr 和 new结合使用 用make_shared函数初始化shared_ptr是最推荐的,但有的时候还是需要用new关键字来初始化shared_ptr. 一,先来个表格,唠 ...

  3. c/c++ 智能指针 shared_ptr 使用

    智能指针 shared_ptr 使用 上一篇智能指针是啥玩意,介绍了什么是智能指针. 这一篇简单说说如何使用智能指针. 一,智能指针分3类:今天只唠唠shared_ptr shared_ptr uni ...

  4. C++智能指针shared_ptr

    shared_ptr 这里有一个你在标准库中找不到的—引用数智能指针.大部分人都应当有过使用智能指针的经历,并且已经有很多关于引用数的文章.最重要的一个细节是引用数是如何被执行的—插入,意思是说你将引 ...

  5. 智能指针 shared_ptr 解析

    近期正在进行<Effective C++>的第二遍阅读,书里面多个条款涉及到了shared_ptr智能指针,介绍的太分散,学习起来麻烦.写篇blog整理一下. LinJM   @HQU s ...

  6. C++ 智能指针Auto_PTR 分析

    C++的动态内存的分配与释放是个挺折磨人的事情,尤其异常分支复杂时(比如一堆try catch中,各catch里需要做delete 掉相关的堆上分配的内存),极有可能产生内存泄露的情况.C++中提供了 ...

  7. 智能指针shared_ptr的用法

    为了解决C++内存泄漏的问题,C++11引入了智能指针(Smart Pointer). 智能指针的原理是,接受一个申请好的内存地址,构造一个保存在栈上的智能指针对象,当程序退出栈的作用域范围后,由于栈 ...

  8. 智能指针shared_ptr

    // 智能指针会自动释放所指向的对象. // shared_ptr的应用场景是:程序需要在多个对象间共享数据 /* 先从应用场景入手吧,说矿工A发现了一个金矿. * 然后矿工A喊来了矿工B,一起开采, ...

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

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

随机推荐

  1. Performance --- 前端性能监控

    阅读目录 一:什么是Performance? 二:使用 performance.timing 来计算值 三:前端性能如何优化? 四:Performance中方法 五:使用performane编写小工具 ...

  2. RPC笔记搬迁

      选择dubbo 启动原理 解析服务 暴露服务 引用服务 提供服务流程 结合Netty 对比 HSF   https://www.cnblogs.com/lichengwei/p/5529492.h ...

  3. [Algorithm] Bitwise Operators

    "|" can be used as assign "&" can be used as check // Read, Write, Execute / ...

  4. SpringCloud断路器(Hystrix)和服务降级案列

    断路器(Hystrix) 为什么需要 Hystrix? 在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用(RPC).为了保证其高可用,单个服务又必须集群部署.由于网络原因或者自 ...

  5. js json 排序

    /* json:需要排序的json key:排序项 */ function JsonSort(json, key) { //console.log(json); for (var j = 1, jl ...

  6. Xcode9/iOS 11 无线调试方法

    1.确保手机已经升级到 iOS 11 ,Xcode 已经升级到 9.0 版本,用手机连接电脑,打开 Xcode 选择路径如下图 2.勾选 Connect via network ,勾选之后拔掉手机. ...

  7. 第01组 团队Git现场编程实战

    目录 一.组员职责分工 二.github 的提交日志截图(鼓励小粒度提交) 三.程序运行截图 四.程序运行环境 五.GUI界面 六.基础功能实现 七.鼓励有想法且有用的功能 八.遇到的困难及解决方法 ...

  8. java读取文件夹下文件及txt内容

    public class PositionController {     // 读取txt内容     public static String txt2String(File file) {    ...

  9. JAVA字符编码二:Unicode,ISO-8859,GBK,UTF-8编码及相互转换

    第二篇:JAVA字符编码系列二:Unicode,ISO-8859-1,GBK,UTF-8编码及相互转换   1.函数介绍 在Java中,字符串用统一的Unicode编码,每个字符占用两个字节,与编码有 ...

  10. js待学习

    异步原理 事件循环 任务队列