C++——智能指针
1.为什么必要智能指针
其实和异常有关系,看下面这段代码就明确了:
- // 首先为什么需要智能指针呢?
- // 前面我们学过异常,在异常中有一个异常安全的问题,就是异常会打乱执行流导致内存泄漏和死锁之类的问题
- // 虽然c++本身有一个解决方案——先捕获处理在重新抛出异常
- // 但是这种方案不太好,为什么?看下面这段代码:
- int div()
- {
- int a, b;
- cin >> a >> b;
- if (b == 0)
- throw invalid_argument("除0错误");
- return a / b;
- }
- void Func()
- {
- // 1、如果p1这里new 抛异常会如何?
- // 2、如果p2这里new 抛异常会如何?
- // 3、如果div调用这里又会抛异常会如何?——这里采用捕获再重新抛异常
- int* p1 = new int;
- int* p2 = new int;
- try
- {
- cout << div() << endl;
- }
- catch (exception& e)
- {
- delete p1;
- delete p2;
- throw e;
- }
- // 这种情况我们会发现,p1和p2如果new的时候抛异常会很不好处理。因此为了解决这个问题,智能指针就出来了
- delete p1;
- delete p2;
- }
- int main()
- {
- try
- {
- Func();
- }
- catch (exception& e)
- {
- cout << e.what() << endl;
- }
- return 0;
- }
复制代码 上面这段代码会导致内存泄漏,这是由于抛异常打乱实行流导致该开释的资源没有正常被开释所导致的,除了内存泄漏,抛异常还可以导致死锁的标题。
这里在复习一下内存泄漏:
- **什么是内存泄漏:**内存泄漏指由于疏忽或错误造成程序未能开释已经不再使用的内存的环境。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,由于计划错误,失去了对该段内存的控制,因而造成了内存的浪费。
- **内存泄漏的危害:**长期运行的程序出现内存泄漏,影响很大,如操纵系统、后台服务等等,出现内存泄漏会导致相应越来越慢,终极卡死。
- 内存泄漏的两种方面
C/C++程序中一样平常我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)
堆内存指的是程序实行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free大概delete 删掉。假设程序的计划错误导致这部分内存没有被开释,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
- 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数开释掉,导致系统资源的浪费,严重可导致系统效能减少,系统实行不稳定。
- 如何避免内存泄漏:
- 工程前期精良的计划规范,养成精良的编码规范,申请的内存空间记着匹配的去开释。ps:这个理想状态。但是如果碰上异常时,就算注意开释了,还是可能会出标题。必要下一条智能指针来管理才有保证。
- 采用RAII思想大概智能指针来管理资源
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出标题了使用内存泄漏工具检测。ps:不外很多工具都不够靠谱,大概收费昂贵。
2.智能指针的使用和原理
那智能指针是如何解决这里的标题标呢?下面给一个例子:
SmartPtr.h:
- #pragma once
- #include<iostream>
- using namespace std;
- template<class T>
- class SmartPtr
- {
- public:
- SmartPtr(T* ptr)
- :_ptr(ptr)
- {}
- ~SmartPtr()
- {
- if (_ptr)
- {
- cout << "delete: " << _ptr << endl;
- delete _ptr;
- }
- }
- private:
- T* _ptr;
- };
复制代码 test.cpp:
- // 下面我们来看看智能指针是如何解决这个问题的
- #include"SmartPtr.h"
- int div()
- {
- int a, b;
- cin >> a >> b;
- if (b == 0)
- throw invalid_argument("除0错误");
- return a / b;
- }
- void Func()
- {
- // 用智能指针来解决内存泄漏的问题
- int* p1 = new int();
- cout << p1 << endl;
- SmartPtr<int> sp1(p1); //有了智能指针哪怕在div中抛异常了,也能释放掉p1的资源
- cout << div() << endl;
- }
- int main()
- {
- try
- {
- Func();
- }
- catch (exception& e)
- {
- cout << e.what() << endl;
- }
- return 0;
- }
复制代码 实行结果如下:
可以发现,出现了抛异常后,我们没有做捕获在抛出的处理,但是资源仍然被开释掉了,这就分析智能指针解决了这个标题,那具体是如何实现的呢?
其实很简朴,它将开释资源这个任务交给了智能指针的生命周期了。当生命周期结束后,就会导致生命周期结束,调用析构函数,从而实现开释资源。因此不管是抛异常还是正常结束,都会导致其智能指针的对象的生命周期结束。
这个思路叫做RAII
3.1RAII
RAII(Resource Acquisition Is Initialization)是一种使用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简朴技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有用,最后在对象析构的时候开释资源。借此,我们现实上把管理一份资源的责任托管给了一个对象。这种做法有两大利益:
- 不必要显式地开释资源
- 采用这种方式,对象所需的资源在其生命期内始终保持有用。
RAII是一种托管资源的方式,智能指针使用了这个方案实现。雷同这样的方案还要c++线程库里之前所使用过的,下图所示:
3.2智能指针的原理
上述的SmartPtr还不能算是智能指针,由于它还不具有指针的举动。指针可以解引用,也可 以通过->去访问所指空间中的内容,如:SmartPtr<int> sp2(new int);的环境下,就无法访问不了该指针所指向的内容了,这个时候就要重载*和->,这样就可以访问了
- template<class T>
- class SmartPtr
- {
- public:
- SmartPtr(T* ptr)
- :_ptr(ptr)
- {}
- ~SmartPtr()
- {
- if (_ptr)
- {
- cout << "delete: " << _ptr << endl;
- delete _ptr;
- }
- }
- T& operator*()
- {
- return *_ptr;
- }
- T* operator->()
- {
- return _ptr;
- }
- private:
- T* _ptr;
- };
复制代码 测试代码:
- void Func()
- {
- SmartPtr<int> sp2(new int);
- *sp2 = 10;
- SmartPtr<pair<int, int>> sp3(new pair<int, int>);
- sp3->first = 1; //这里其实是两个->,但是编译器优化成一个->了,这之前讲过了
- sp3->second = 2;
- }
复制代码 总结一下智能指针的原理:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的举动。
3.3智能指针的坑
但是这样还是不能称上完备的智能指针,只能说是最简朴的智能指针。会有很多坑,比如拷贝构造。
如果在代码中直接实行拷贝构作育会报错
- SmartPtr<int> sp4(new int); //拷贝构造
- SmartPtr<int> sp5 = sp4;
复制代码 为什么?之前写了这么多代码,和拷贝构造相干又能爆这个错误,其实就是这里是浅拷贝,又是指针,分析sp4和sp5指向了同一个空间,那么就会对同一个空间开释两次,就会导致程序崩b溃。
那怎么解决呢?——之前所采取的解决方案是用深拷贝,但是这里可以用深拷贝吗?其实是不行的,由于智能指针,我只是想使用你RAII的风格来解决我内存泄漏的标题,我只是将资源托管给你,在你生命周期结束之后可以或许开释资源,而不是擅自开一个空间来使用。
也就是说,智能指针的拷贝构作育应该像原生指针的举动p2 = p1一样,指向的是同一个空间,可是这样会导致瓦解。因此重点在解决析构同一个空间两次这个标题
其实这个标题之前在学习Linux文件系统的时候已经学过了,那就是引用计数【忘记了就要复习】
这里有三种解决方式:
而c++里也是用这三个方式解决的,c++98用管理权转移(auto_ptr)解决, c++11提出了用防拷贝(unique_ptr)和引用计数解决
3.4 auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及标题。 auto_ptr的实现原理:管理权转移的思想,下面简化模仿实现了一份auto_ptr来了解它的原理
- namespace wzf
- {
- template<class T>
- class auto_ptr
- {
- public:
- auto_ptr(T* ptr)
- :_ptr(ptr)
- {
- }
- auto_ptr(auto_ptr<T>& sp)
- :_ptr(sp._ptr)
- {
- // 管理权转移
- sp._ptr = nullptr;
- }
- auto_ptr<T>& operator=(auto_ptr<T>& ap)
- {
- // 检测是否为自己给自己赋值
- if (this != &ap)
- {
- // 释放当前对象中资源
- if (_ptr)
- delete _ptr;
- // 转移ap中资源到当前对象中
- _ptr = ap._ptr;
- ap._ptr = NULL;
- }
- return *this;
- }
- ~auto_ptr()
- {
- if (_ptr)
- {
- cout << "delete:" << _ptr << endl;
- delete _ptr;
- }
- }
- // 像指针一样使用
- T& operator*()
- {
- return *_ptr;
- }
- T* operator->()
- {
- return _ptr;
- }
- private:
- T* _ptr;
- };
- // 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
- }
复制代码 测试代码:
- #include <memory>
- int main()
- {
- //首先是c++98的auto_ptr(转移管理权)
- std::auto_ptr<int> sp1(new int);
- std::auto_ptr<int> sp2(sp1); // 管理权转移
- // sp1悬空
- *sp2 = 10;
- cout << *sp2 << endl;
- //cout << *sp1 << endl; //已经被置空了,无法在操作sp1
- return 0;
- }
复制代码 这个方案并不是很好,由于它并没有完全模仿原生指针的举动,它是将sp1管理的资源(指针),直接赋值给sp2,然后将sp1置空。**即让sp2管理资源,自己不管了,这样就不会析构同一个空间两次了。**可以说是早期的计划缺点
3.5 unique_ptr
C++11中开始提供更靠谱的unique_ptr
unique_ptr文档
unique_ptr的实现原理:简朴粗暴的防拷贝,下面简化模仿实现了一份unique_ptr来了解它的原理
- namespace wzf
- {
- template<class T>
- class unique_ptr
- {
- public:
- unique_ptr(T* ptr = nullptr)
- :_ptr(ptr)
- {}
- unique_ptr(const unique_ptr<T>& up) = delete;
- unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
- ~unique_ptr()
- {
- if (_ptr)
- {
- delete _ptr;
- }
- }
- T& operator*()
- {
- return *_ptr;
- }
- T* operator->()
- {
- return _ptr;
- }
- private:
- T* _ptr;
- };
- }
复制代码 防拷贝就是直接很简朴粗暴的不让拷贝构造了
3.6 shared_ptr
由于总有一些环境要用到拷贝构造,因此c++11又弄了一个支持拷贝构造的智能指针,采取的解决方案是引用计数
- namespace wzf
- {
- template<class T>
- class shared_ptr
- {
- public:
- shared_ptr(T* ptr = nullptr)
- :_ptr(ptr)
- , _pcount(new int(1))
- {}
- //拷贝构造
- shared_ptr(const shared_ptr<T>& sp)
- :_ptr(sp._ptr)
- , _pcount(sp._pcount)
- {
- ++(*_pcount); //指向同一个空间的指针变多了。++
- }
- shared_ptr<T>& operator=(const shared_ptr<T> sp)
- {
- if (this != &sp)
- {
- if (_ptr)
- {
- // 由于要赋值,以为这this这个智能指针不能管理自己的资源了,要管理sp所指向的资源了
- //先释放掉原有的资源,然后再被赋值。
- // 但是这个释放不能简单的直接释放,要考虑引用计数
- (*_pcount)--;
- if ((*_pcount) == 0) //当减为0之后才能释放
- {
- delete _ptr;
- delete _pcount;
- _ptr = nullptr;
- _pcount = nullptr;
- }
- }
- _ptr = sp._ptr;
- _pcount = sp._pcount;
- ++(*_pcount);
- }
- return *this;
- }
- ~shared_ptr()
- {
- if (--(*_pcount) == 0 && _ptr)
- {
- cout << "delete:" << _ptr << endl;
- delete _ptr;
- _ptr = nullptr;
- delete _pcount; //指针也要释放
- _pcount = nullptr;
- }
- }
- T& operator*()
- {
- return *_ptr;
- }
- T* operator->()
- {
- return _ptr;
- }
- private:
- T* _ptr;
- int* _pcount; //引用计数
- };
- }
复制代码 要注意,这个引用计数,不能是静态成员,由于静态成员属于这类,一旦有一个对象不指向同一个空间,就会重置,这样是不对的
这里要用指针,动态的,对每一个不同的空间都有一个指针来做引用计数
测试代码:
- //c++11的引用计数
- wzf::shared_ptr<int> sp5(new int);
- wzf::shared_ptr<int> sp6(sp5);
- wzf::shared_ptr<int> sp7;
- sp7 = sp6;
- wzf::shared_ptr<int> sp8(new int);
复制代码 3. shared_ptr的线程安全标题
其实上面自己模仿实现的shared_ptr会存在线程安全标题,由于存在引用计数,对同一个空间++,–。因此一旦有多线程的操纵,就会存在线程安全的标题。
比如:有两个线程同时对一个shared_ptr进行拷贝构造,那么就会对引用计数都++。一旦拷贝构造的次数多了,由于++不是原子操纵,因此,一旦在++的时候出现线程切换,就可能出现两个线程++一次之后,引用计数只++了一次。这里不具体讲,忘记了就复习Linux——多线程—02
下面是一个例子:
- //关于shared_ptr的线程安全问题【库里的shared_ptr肯定线程安全,这里说的是自己模拟实现的】
- #include"shared_ptr.h"
- #include<thread>
- using namespace std;
- int main()
- {
- wzf::shared_ptr<int> sp(new int);
- cout << sp.use_count() << endl;
- int n = 2000;
- //若shared_ptr线程不安全【对引用计数++,--操作不是互斥的】,则下面是会出现线程安全问题的代码:
- thread t1([&](){
- for (int i = 1; i <= n; i++)
- {
- wzf::shared_ptr<int> sp1(sp); //对sp进行一次拷贝构造
- }
- });
- thread t2([&](){
- for (int i = 1; i <= n; i++)
- {
- wzf::shared_ptr<int> sp2(sp); //进行一次拷贝构造
- }
- });
- t1.join();
- t2.join();
- cout << sp.use_count() << endl;
- return 0;
- }
复制代码 每次的实行结果都不肯定一样
出现线程安全标题标话,只会有两种可能,一个是++的时候+少了,–的时候不够-,导致析构同一个空间两次,程序瓦解,还要一种可能是–的时候-少了,导致–不到0,无法析构。
正常应该是会delete掉的
以是还得继续改造一下shared_ptr,让它变得线程安全才行
以下是改造之后线程安全的shared_ptr
此时线程安全之后,无论怎么实行上面谁人多线程的代码,都是正常的

4. shared_ptr的循环引用标题
- // shared_ptr的循环引用问题
- #include"shared_ptr.h"
- class ListNode
- {
- public:
- int _val;
- wzf::shared_ptr<ListNode> _next;
- wzf::shared_ptr<ListNode> _prev;
- ~ListNode()
- {
- cout << "~ListNode" << endl;
- }
- };
- int main()
- {
- wzf::shared_ptr<ListNode> sp1(new ListNode);
- wzf::shared_ptr<ListNode> sp2(new ListNode);
- // shared_ptr的循环引用
- // 因为有一些时候,我们会相同智能指针来管理自定义类型,而在使用的过程中可能会出现给指针赋值的情况
- // 但是这个时候类内的指针域有可能是内置类型,无法完成赋值操作。这个时候就需要将指针域也用智能指针来管理
- // 但是这个时候就会触发循环引用的问题
- sp1->_next = sp2;
- sp2->_prev = sp1;
- // 前面我们说了对于shared_ptr来说赋值就是将当前空间交给对方一起管理。
- // 所以sp1管理着一个ListNode,这个ListNode的_next又管理着sp2,此时sp2的引用计数为2
- // sp2管理着一个ListNode,引用计数为1,然后该ListNnode的prev又管理着sp1,sp1的引用计数为2。
- // 因此在析构的时候,就无法析构,因为只会--一次,不为0就不析构。
- // 这个循环牵制对方的现象就叫做循环引用!
- return 0;
- }
复制代码
循环引用分析:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不必要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就开释了。
- 也就是说_prev析构了,node1就开释了。
- 但是_next属于node的成员,node1开释了,_next才会析构,而node1由_prev管理,_prev属于node2成员,以是这就叫循环引用,谁也不会开释
这是shared_ptr的一个缺点,无法自己解决,只能通过其他方式来解决
那如何解决呢?
c++给了一个弱指针——weak_ptr
weak_ptr是专门用来解决shared_ptr的循环引用标题标,它的原理就是不引用计数,直接将shared_ptr的对象作为参数传给weak_ptr去构造,而weak_ptr不会对shared_ptr所指向的空间做引用计数,这样就不会存在循环引用的标题了
- #include"shared_ptr.h"
- class ListNode
- {
- public:
- int _val;
- //wzf::shared_ptr<ListNode> _next;
- //wzf::shared_ptr<ListNode> _prev;
-
- // 为了防止引用计数的问题,这里要用weak_ptr
- std::weak_ptr<ListNode> _next;
- std::weak_ptr<ListNode> _prev;
- ~ListNode()
- {
- cout << "~ListNode" << endl;
- }
- };
- int main()
- {
- //那这个问题要如何解决呢?
- //就是弱指针——weak_ptr
- std::shared_ptr<ListNode> sp1(new ListNode);
- std::shared_ptr<ListNode> sp2(new ListNode);
- sp1->_next = sp2;
- sp2->_next = sp1;
- return 0;
- }
复制代码 5.C++11和boost中智能指针的关系
- C++ 98 中产生了第一个智能指针auto_ptr.
- C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
- C++ TR1,引入了shared_ptr等。不外注意的是TR1并不是尺度版。
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。必要注意的是unique_ptr对应boost的scoped_ptr。而且这些智能指针的实现原理是参考boost中的实现的。
6.定制删除器
关于定制删除器:
起首智能指针默认的开释资源的处理方式都是delete _ptr;
在一些特殊场景下,单纯的一个delete _ptr,无法满足开释资源的需求了
下面的代码例子中会有讲
- // 关于智能指针的一些补充————定制删除器
- class A
- {
- public:
- ~A()
- {
- cout << "~A()\n";
- }
- private:
- int _a;
- };
- template<class T>
- struct DeleteArr
- {
- void operator()(T* p)
- {
- delete[] p;
- }
- };
- template<class T>
- struct Free
- {
- void operator()(T* p)
- {
- cout << "free()" << endl;
- free(p);
- }
- };
- struct Fclose
- {
- void operator()(FILE* p)
- {
- cout << "fclose()" << endl;
- fclose(p);
- }
- };
- int main()
- {
- //为什么需要对智能指针定制一个删除器呢
- // 这是因为在一些特殊场景下,单纯的一个delete _ptr,无法满足释放资源的需求了
- // 下面是例子:
- shared_ptr<A> sp1(new A); //这个情况下,智能指针能够释放资源
- //DeleteArr<A> d;
- //shared_ptr<A> sp2(new A[5]); //这个情况下,就是报错,因为单纯的delete _ptr无法处理这个情况
- // 不仅是上面这个,下面这两种也会
- //shared_ptr<A> sp3((A*)malloc(sizeof(A)));
- //shared_ptr<FILE> sp4(fopen("test.txt", "r"));
- // 因此我们需要定制一个删除器,以便于处理当前这个情况
- // 具体要怎么做呢,就是传一个仿函数给shared_ptr,让它用仿函数来处理常规方式无法释放的类型的资源的释放
- shared_ptr<A> sp2(new A[5], DeleteArr<A>());
- // 对sp3就要传一个专门处理malloc的仿函数
- shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free<A>());
- // 对sp4要传一个专门处理FILE*的仿函数
- shared_ptr<FILE> sp4(fopen("test.txt", "w"), Fclose());
- // 下面是程序执行的结果
- // fclose()
- // free()
- // ~A()
- // ~A()
- // ~A()
- // ~A()
- // ~A()
- // ~A()
- return 0;
- }
复制代码
这里不是重点,了解即可
;
fclose§;
}
};
int main()
{
//为什么必要对智能指针定制一个删除器呢
// 这是由于在一些特殊场景下,单纯的一个delete _ptr,无法满足开释资源的需求了
// 下面是例子:
shared_ptr sp1(new A); //这个环境下,智能指针可以或许开释资源
//DeleteArr d;
//shared_ptr sp2(new A[5]); //这个环境下,就是报错,由于单纯的delete _ptr无法处理这个环境
// 不仅是上面这个,下面这两种也会
//shared_ptr sp3((A*)malloc(sizeof(A)));
//shared_ptr sp4(fopen(“test.txt”, “r”));
- // 因此我们需要定制一个删除器,以便于处理当前这个情况
- // 具体要怎么做呢,就是传一个仿函数给shared_ptr,让它用仿函数来处理常规方式无法释放的类型的资源的释放
- shared_ptr<A> sp2(new A[5], DeleteArr<A>());
- // 对sp3就要传一个专门处理malloc的仿函数
- shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free<A>());
- // 对sp4要传一个专门处理FILE*的仿函数
- shared_ptr<FILE> sp4(fopen("test.txt", "w"), Fclose());
- // 下面是程序执行的结果
- // fclose()
- // free()
- // ~A()
- // ~A()
- // ~A()
- // ~A()
- // ~A()
- // ~A()
- return 0;
复制代码 }
- [外链图片转存中...(img-MBhVcbzm-1746854443683)]
- 这里不是重点,了解即可
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|