智能指针

智能指针听名字就知道他应该是一个指针或者类似指针的东西,那么他到底是干什么的?让我们一起来看看

1.为什么需要智能指针?

    我们在谈异常的时候,有下面这么一段代码,这段代码中有一个异常重新抛出的动作,这里我们说过其实是为了防止没有释放array的空间造成内存泄漏。

double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Division by zero condition!";//此处抛出异常}return (double)a / (double)b;
}
void Func()
{// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再// 重新抛出去。int* array = new int[10];try {int len, time;cin >> len >> time;cout << Division(len, time) << endl;}catch (...)//捕获异常处理释放问题,重新抛出{cout << "delete []" << array << endl;delete[] array;throw;}  delete[] array;
}
int main()
{try{Func();}catch (const char* errmsg)//捕获第二次抛出的异常{cout << errmsg << endl;}    return 0;
}

    这里截获异常,处理内存等问题后我们在将异常重新抛出,其实这样的处理的方式对我们维护代码造成了很大的不便,而且一不小心就会造成内存泄漏,所以我们就引出了一个新名词智能指针,那么智能指针是怎么处理这个问题的呢?

    我们只看Func函数中的逻辑,这里我们发现我们使用了一个对象来管理我们new出来的空间,这块空间会随着对象的生命周期结束被释放掉。

template<class T>
class SmartPtr {public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}
private:T* _ptr;
};void Func()
{int* array = new int[10];try {int len, time;cin >> len >> time;cout << Division(len, time) << endl;}  SmartPtr<int> p(array);
}

2.智能指针的使用以及原理

2.1RAII

RAII(Resource Acquisition Is Initialization -> 资源获得即初始化),是一种利用对象生命周期来控制程序资源的简单技术。
在对象构造时获得资源,接着控制对资源的访问在对象的生命周期内始终有效,最后在对象析构时释放资源。这样的好处在于:

  • 不需要显示的释放资源
  • 对象所管理的资源在其生命周期内始终有效

2.2智能指针的原理

我们知道普通的指针还需要支持 -> *等操作,所以智能指针为了模仿普通指针的行为我们需要重载他的 ->等符号

template<class T>
class SmartPtr {public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}T& operator*()//重载*模仿普通指针的行为{return *_ptr;}T* operator->()//如果我们的初始化了结构体指针,我们还需要用重载->来访问他的成员{return _ptr;}~SmartPtr(){if (_ptr)delete _ptr;}
private:T* _ptr;
};
int main()
{int* p = new int;SmartPtr<int> sp(p);*sp = 10;cout << *sp << endl;system("pause");return 0;
}

3.c++库中的智能指针

3.1auto_ptr

    c++库中智能指针都在memory的头文件中,这里我们一起来谈一谈不同的智能指针,首先来看看最原始的智能指针,auto_ptr

3.1.1auto_ptr的问题

    这里是一段auto_ptr使用的代码,运行结果奔溃,之所以程序奔溃是因为在ap被copy拷贝后成了空指针,解引用他导致奔溃,这里导致奔溃的行为叫做转移管理权,为什么会这样接着看模拟实现部分

int main()
{int* p = new int;auto_ptr<int> ap(p);auto_ptr<int> copy(ap);*ap = 10;//程序奔溃system("pause");return 0;
}

3.1.2auto_ptr的模拟实现

    auto_ptr的实现如下,我们上面说过,auto_ptr使用了一种管理权转移的方式来实现,所以当你进行拷贝构造之后原指针已经被置空

template<class T>
class Auto_Ptr {public:Auto_Ptr(T* ptr = nullptr): _ptr(ptr){}Auto_Ptr(Auto_Ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;//发生拷贝构造后自己被置空,所以解引用会奔溃}Auto_Ptr<T>& operator=(Auto_Ptr<T>& ap){if (this = &ap)//检查是否自己给自己赋值{if (_ptr)delete _ptr;_ptr = ap._ptr;ap._ptr = nullptr;//将对象指针置空}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~Auto_Ptr(){if (_ptr)delete _ptr;}
private:T* _ptr;
};
  • 这里为什么不使用默认的构造函数?因为如果默认的拷贝构造将会是一个浅拷贝,俩个指针指向的同一块空间会被释放俩次
  • 那为什么不使用深拷贝呢?因为一个指针给另一个指针赋值实际意义就是为了让他们指向同一块空间,如果重新开一块空间让被赋值的指针指向它那么就完全违背了指针赋值的意义

3.2unique_ptr

3.2.1unique_ptr的问题

    上面我们研究了auto_ptr的问题,auto_ptr事实上最大的问题就在于他的拷贝,所以unique_ptr顶对拷贝构造的问题做了一件非常简单粗暴的事情,就是将拷贝构造和赋值函数都删除,这里unique_ptr智能指针的行为叫做防拷贝

Unique_Ptr(Unique_Ptr<T>& ap) = delete;
Unique_Ptr<T>& operator=(Unique_Ptr<T>& ap) = delete;

unique_ptr就如同他的名字一样,他被定义出来是唯一的,他不能发生被拷贝和赋值的行为,这里确实解决了指针悬空的问题,但是这里只算是一种回避问题的解决方式,所以从根本上来说我们还是没有解决拷贝构造的问题

3.2.2unique_ptr的模拟实现

这里仅仅在auto_ptr的基础上将拷贝赋值函数设定为delete函数,有的同学说将拷贝赋值函数只声名不定义并且设置为私有的行不行,其实理论上来说没问题,但是不要忘记c++的友元,他会突破类的封装如果在类外定义那就得不偿失了。

template<class T>
class Unique_Ptr {public:Unique_Ptr(T* ptr = nullptr): _ptr(ptr){}Unique_Ptr(Unique_Ptr<T>& ap) = delete;Unique_Ptr<T>& operator=(Unique_Ptr<T>& ap) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~Unique_Ptr(){if (_ptr)delete _ptr;}
private:T* _ptr;
};

3.3靠谱的shared_ptr

上面我们已经谈了俩种实现方式的智能指针,但是他们都有大大小小的缺陷,所以c++11中给出了一种叫shared_ptr的智能指针来弥补上面指针指针存在的缺陷

3.3.1shared_ptr解决拷贝问题

为了解决拷贝的问题,并且希望这里就是完成浅拷贝,所以我们设定了一个计数,每增加一个指针指向同一块空间,这块空间对应的计数就++,每释放一个指针就 - -计数,直到最后一个指针释放的同时释放这块空间

3.3.2怎么设置计数?

这样?

class Shared_Ptr//A{private:T* _ptr;int _count;
};

这样?

class Shared_Ptr//B{private:T* _ptr;static int _count;
};

还是这样?

class Shared_Ptr//C{private:T* _ptr;int* _count;
};

我们来分析一下:A中的计数相当于存在每个对象中,好像并不能达到多个对象一个计数的要求,那B好像看起来是可以的,因为所有的对象都只有一个计数,但是。。

这样我们计数只有一个,违背了我们下图中希望一块空间一个计数的原则

所以C是最合适的,让每一个计数指针指向一个计数,相同类型的对象被拷贝也就不在需要新的指针。

3.3.3shared_ptr的模拟实现

析构函数:当每个对象中的计数被减到0的时候释放计数的指针

~Shared_Ptr(){if (--(*_count) == 0){delete _ptr;delete _count;}}

拷贝构造函数:每被拷贝一次就 ++计数

Shared_Ptr(Shared_Ptr<T>& ap):_ptr(ap._ptr), _count(ap._count){++(*_count);}

赋值重载:1.判断是否给自己赋值 2.如果当前对象已经是最后一个指向目标空间的指针,释放资源 3.赋值给当前对象并且++计数

Shared_Ptr<T>& operator=(Shared_Ptr<T>& ap){if (_ptr != ap._ptr){if (--(*_count) == 0){delete _ptr;delete _count;}_ptr = ap._ptr;_count = ap._count;++(*_count);}return *this;}

简单的实现了上面的函数后我们的shared_ptr就算是完成了,但是实际上还是存在问题

如果在多线程中,我们发现 ++ 和 - - 操作不是原子的,经过测试,我们发现执行20000次(次数变大就会出错)后计数的大小并不是20000,这个值小于20000,根本原因就是不是原子的++,所以我们需要对++ - - 操作进行加锁,所以以下是最终版本

template<class T>
class Shared_Ptr {public:Shared_Ptr(T* ptr = nullptr): _ptr(ptr), _count(new int(1)), _pmtx(new mutex){}Shared_Ptr(Shared_Ptr<T>& ap):_ptr(ap._ptr), _count(ap._count), _pmtx(ap._pmtx){Add();}void Add()//加锁的++{_pmtx->lock();++(*_count);_pmtx->unlock();}void Release()//加锁的--{bool flag = false;_pmtx->lock();if (--(*_count) == 0){delete _ptr;delete _count;flag = true;//判断是否释放锁}_pmtx->unlock();if (flag == true)delete _pmtx;}Shared_Ptr<T>& operator=(Shared_Ptr<T>& ap){if (_ptr != ap._ptr){Release();_ptr = ap._ptr;_count = ap._count;Add();}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~Shared_Ptr(){Release();}
private:T* _ptr;int* _count;mutex* _pmtx;
};

3.3.4shared_ptr的问题

     上面我们已经说了shared_ptr的模拟实现,到目前为止我们好像只发现了他加加计数线程不安全的问题,但是进过我们的修改shared_ptr好像已经没什么问题了,但是记住这里只是解决了加加计数的线程安全的问题,如果我们管理int对象的指针解引用并且加加这个对象,如果在加加次数很多并且不加锁的情况下还是会有问题(加n次结果小于n),除了这个问题,shared_ptr看起来非常完美,但是事实上是这样的么?一起来看看

一段没有问题的代码,但是希望cur next能被智能指针所管理

struct ListNode//一段没有问题的代码
{int _data;ListNode* _next;ListNode* _prev;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{ListNode* cur = new ListNode;ListNode* next = new ListNode;cur->_next = next;next->_prev = cur;return 0;
}

于是出现了下面的代码:但是报错了,因为节点中的指针不是智能指针类型

struct ListNode
{int _data;ListNode* _next;ListNode* _prev;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{shared_ptr<ListNode> cur(new ListNode);shared_ptr<ListNode> next(new ListNode);cur->_next = next;//报错next->_prev = cur;//报错system("pause");return 0;
}


那好吧,我们修改节点中的指针:现在编过了,可是一运行,发现没有调用节点的析构函数

struct ListNode
{int _data;shared_ptr<ListNode> _next;shared_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};

观察引用计数:好像也没有什么问题,但是我们通过调试发现问题出在了这

如下图:

  • 当cur生命周期结束时被释放,next也是同理,但是他们释放后计数为一
  • 也就是说_next析构了,node2就释放了。
  • 也就是说_prev析构了,node1就释放了。
  • 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2
    成员,所以这就叫循环引用,谁也不会释放

    所以为了解决这个问题,我们引出了第四种指针weak_ptr,这种指针就是为了解决shared_ptr循环引用的问题的,可以说是shared_ptr的产物,将节点中的指针改成他就解决了问题,他的原理就是不让引用计数加加,所以我们不难理解解决问题的原理

    weak_ptr是用来解决循环引用的问题的,所以他不能管理其他类型的指针,他只能管理shared_ptr类型和自己本身类型。

小结

  • auto_ptr:管理权转移行为,一种带有缺陷的早期设计,一般严禁使用
  • unique_ptr:简单的粗暴的防拷贝行为,效率高,但是功能不全,一般鼓励使用
  • shared_ptr:功能全,支持拷贝,利用了引用计数的计数,但是设计复杂,要考虑线程安全,循环引用需要weak_ptr解决

4.定制删除器

上面我们已经讲过了智能指针的问题和实现,但是我们在使用他们时依旧会存在一些问题:

下面这段代码会导致程序奔溃,因为shared_ptr在释放时是使用delete释放的,并且不仅仅在当前情况下,还有malloc出来的对象,或者是打开一个文件,需要关闭文件

std::shared_ptr<AA> sp1(new AA[10]);

所以我们需要学习一个新的东西,叫定制删除器,前面我们讲过仿函数,这里就使用的是仿函数的原理,重载operator(),并且将此类型作为智能指针的第二个参数就可以帮我们完成删除操作。

class AA
{private:int a;int b;
};template<class T>
struct DeleteArrayFunc {void operator()(T* ptr){cout << "delete[]" << ptr << endl;delete[] ptr;}
};int main()
{DeleteArrayFunc<AA> fun;std::shared_ptr<AA> sp1(new AA[10],fun);return 0;
}

4.1定制删除器完成文件的关闭

对管理文件的指针进行关闭:

struct Fclose
{void operator()(FILE* ptr){cout << "fclose:" << ptr << endl;fclose(ptr);ptr = nullptr;}
};int main()
{Fclose fun;std::shared_ptr<FILE> sp1(fopen("test.txt","w"),fun);return 0;
}

5.RAII拓展

智能指针其实可以说是c++中独特的东西,因为他没有垃圾回收的机制,但是RAII这种思想在所有的语言中都是有用的,来看下面的代码:

这样的情况下就造成了死锁的问题

int main()
{mutex mtx;mtx.lock();//抛异常mtx.unlock();system("pause");return 0;
}

所以既然智能指针能帮我们回收资源,那么我们就可以模仿智能指针的行为,来管理有关锁等一系列的问题:
c++库中也有uniquelock,他也是防拷贝的。

template<class Lock>
class UniqueLock
{public:UniqueLock(Lock& lock):_lock(lock){_lock.lock();}~UniqueLock(){_lock.unlock();}
private:Lock& _lock;
};
int main()
{mutex mtx;UniqueLock<mutex> lock(mtx);system("pause");return 0;
}

总结

  1. 理解为什么需要智能指针?重点注意智能指针是一种预防型的内存泄漏的解决方案。智能指针在C++没
    有垃圾回收器环境下,可以很好的解决异常安全等带来的内存泄漏问题,
  2. 理解RAII.
  3. 理解auto_ptr、unique_ptr、shared_ptr等智能指针的使用及原理

[c++]——智能指针相关推荐

  1. bartender一行打印两个二次开发_C++ 智能指针和二叉树:图解层序遍历和逐层打印二叉树...

    作者:apocelipes  链接:https://www.cnblogs.com/apocelipes/p/10758692.html 二叉树是极为常见的数据结构,关于如何遍历其中元素的文章更是数不 ...

  2. 五点讲述C++智能指针的点点滴滴

    (在学习C/C++或者想要学习C/C++可以加我们的学习交流QQ群:712263501群内有相关学习资料) 0.摘要 本文先讲了智能指针存在之前C++面临的窘境,并顺理成章地引出利用RAII技术封装普 ...

  3. 关于 智能指针 的线程安全问题

    先说结论,智能指针都是非线程安全的. 多线程调度智能指针 这里案例使用的是shared_ptr,其他的unique_ptr或者weak_ptr的结果都是类似的,如下多线程调度代码: #include ...

  4. C++ 智能指针(unique_ptr / shared_ptr)代码实现

    文章目录 unique_ptr 智能指针的实现 shared_ptr 智能指针的实现 指针类型转换 unique_ptr 智能指针的实现 一个对象只能被单个unique_ptr 所拥有. #inclu ...

  5. C++智能指针:unique_ptr详解

    文章目录 unique_ptr描述 声明 作用 函数指针描述 总结 unique_ptr描述 声明 头文件:<memory> 模版类: 默认类型template <class T, ...

  6. C++智能指针:weak_ptr实现详解

    文章目录 weak_ptr描述 声明 作用 原理实现 函数成员使用 总结 weak_ptr描述 声明 头文件:<memory> 模版类:template <class T> c ...

  7. C++智能指针: shared_ptr 实现详解

    文章目录 shared_ptr描述 声明 作用 原理实现 函数使用 关于shared_ptr循环引用问题 shared_ptr描述 声明 shared_ptr属于C++11特性中新加的一种智能指针,它 ...

  8. 【Smart_Point】C/C++ 中智能指针

    C++11智能指针 目录 C++11智能指针 1.1 C++11智能指针介绍 1.2 为什么要使用智能指针 1.2.1 auto_ptr(C++98的方案,C++11已经抛弃)采用所有权模式. 1.2 ...

  9. 【C++】智能指针(一)入门

    1. 智能指针背后的设计思想 智能指针背后的思想是RAII,参见博客[C++]零散知识 我们先来看一个简单的例子: void remodel(std::string & str) {std:: ...

  10. 【C++】Google C++编码规范(三):智能指针

    [C++]Google C++编码规范(一):作用域 [C++]Google C++编码规范(二):类 std::unique_ptr std::unique_ptr是C++11标准里新推出的智能指针 ...

最新文章

  1. 美国杜克大学计算机专业世界排名,美国杜克大学世界排名情况怎么样?
  2. 智能家居 (8) ——智能家居项目整合(网络控制线程、语音控制线程,火灾报警线程)
  3. 去除HTML标签--SQL写法
  4. linux设备驱动之按键外部中断
  5. windows部署免安装版python
  6. kafka调试工具kafkacat的使用
  7. matlab max函数_从零开始的matlab学习笔记——(14)一些有用的函数(上):最值,平均数,中位数...
  8. 在PS中读取敏感数据
  9. k8s学习:WordPress + MySQL + PVC 构建一个博客网站
  10. 十大电子元器件及其相关基础知识
  11. CNC加工中心程序代码大全,你还不收藏吗?
  12. AIS船舶自动识别系统原理
  13. vue运行(Emitted value instead of an instance of Error)
  14. PS 学习笔记 03-移动工具图层概念
  15. 白山搜索引擎优化收费_白山SEO-白山网站优化-白山新站整站快速排名-【
  16. mac如何强制退出程序?强制退出程序的六种方法
  17. 将文件传到免费服务器上,将文件传到服务器上
  18. listen的backlog值分析
  19. 面试题葵花宝典(脚本与运维篇)
  20. java套打实现_java 套打 实现

热门文章

  1. 裁剪图片软件有哪些?这些图片裁剪工具很好用
  2. 深入理解JavaScript电子书pdf下载
  3. AppStore 上架流程 2019年
  4. 基于stm32片内Flash数据的DAC输出
  5. 2021IOTE国际物联网展深圳站核芯物联科技正式发布全球第一款无线全网通国产蓝牙AOA高精度定位基站GA30
  6. 原装正品韩国GENICOM 紫外线探测器-GUVx-T1xGC-I8LW5.1 原厂渠道
  7. 计算机毕业设计springboot基于大数据的疫情追踪系统的设计和实现rva1s源码+系统+程序+lw文档+部署
  8. 【每日随笔】2023年02月14日随笔 ( 随便写点 | 技术无关、没事别点进来看、好好学技术 | 如何攒钱 | 插管与拔管 | 返贫途径 | 守财 | 财富增长 | 推荐书籍 )
  9. 初探RabbitMQ
  10. 2018 东华软件股份有限公司 ETL开发工程师笔试题