智能指针c/c++
目录
1、内存泄漏
1.1概念
1.2分类
1.3工具及处理方案
2.RAII
2.1概念
2.2实现
2.3auto_ptr
2.4unique_ptr
实现
问题
定制删除器
1、内存泄漏
智能指针的一个用途,在我异常的文章的异常安全部分里有写,在那篇文章中,我举例了异常安全的多个现象,而其中,内存泄漏是非常恶劣且频发的问题。
1.1概念
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上消失,而不是应用程序分配了某段内存后,因为某个错误,失去了对该段内存的控制,造成了内存的浪费。
危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
简单举例:new了一块空间,但是没有delete掉,这就是一种内存泄漏。这个例子扩充下,在new和delete中间有一段会肯定触发异常的代码,导致跳过了delete,这就是一个异常安全问题。
1.2分类
这里主要是指c/c++中的内存泄漏问题:
堆内存泄漏(heap leak)
堆内存指,程序执行中通过malloc/calloc/realloc/new/等从堆中分配的一块内存,用完后必须通过相应的free或delete删掉。比如上面的例子,就是一个heap leak
系统资源泄漏
程序使用系统分配的自由,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重会导致系统效能减少,系统执行不稳定。
1.3工具及处理方案
这里列一下检查内存泄漏的工具
linux:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客
windows:VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库_visual leak detector vs2020-CSDN博客
内存工具比较:内存泄露检测工具比较 - 默默淡然 - 博客园 (cnblogs.com)
处理内存泄漏的方法:
1、代码规范,注意释放;2、使用智能指针或rall思想管理资源;3、某些公司内部的的私有内存管理库(这种库自带内存泄漏的检查选项);4、用检查内存泄漏工具,如上。
2.RAII
2.1概念
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(内存、文件句柄、网络连接、互斥量等等)的技术
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此把管理资源的责任交给对象,不需显示地释放资源、对象所需的资源在生命周期内始终保持有效。
2.2实现
下面是在一个简单的示范
template<class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { cout << "delete" << endl; delete _ptr; } private: T* _ptr; }; void func(int x) { if(x==1) throw "wdwad"; } void f() { try { SmartPtr<int> s1(new int); func(2); SmartPtr<int> s2(new int); func(1); SmartPtr<int> s3(new int); } catch (...) { cout << 1 << endl; } //这样就可以依靠析构函数自动清理资源了。 //就算抛了异常,s1、s2、s3是局部变量,在出了作用域会自动销毁,调用它的析构函数 } int main() { f(); return 0; }
但是既然是指针,就得有指针的用法
template<class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { cout << "delete" << endl; delete _ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; }; void func() { throw "wdwad"; } void f() { try { SmartPtr<int> s1(new int); func(); SmartPtr<int> s2(new int); func(); SmartPtr<int> s3(new int); } catch (...) { cout << 1 << endl; } //这样就可以依靠析构函数自动清理资源了。 //就算抛了异常,s1、s2、s3是局部变量,在出了作用域会自动销毁,调用它的析构函数 } int main() { f(); return 0; }
基本的架构就是这样,但是我们知道内置类型的指针,是可以互相拷贝,且指向同一块空间的。而我们上面的写法,因为没有显示写拷贝构造,而是编译器默认生成的拷贝构造,编译器默认生成的拷贝构造是浅拷贝,光看这里,2个智能指针指向同一块空间,好像很完美,但是因为析构函数,这就导致了同一块空间会被多次析构,这就会出问题。
2.3auto_ptr
c++98的库里就提供了一个智能指针,这个容器对于拷贝的问题,处理方法是:
资源管理权的转移。
int main() { auto_ptr<int> s1(new int(1)); auto s2(s1); //cout << (*s1) << endl; //这里的资源管理权转移,就是s1的ptr被移到了s2的ptr,s1的ptr变成了空指针 //这样就不能访问s1了,因为这样是访问空指针 cout << (*s2) << endl; return 0; }
简单的代码模拟:
template<class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { cout << "delete" << endl; delete _ptr; } SmartPtr(const SmartPtr& ptr) { _ptr = ptr._ptr; ptr._ptr = nullptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
这个处理方法非常的暴力,也很容易坑人,所以不推荐使用。
因为语言不希望改,只希望加,所以虽然auto_ptr很坑,但还是不能删,为了防止类似的事情,c++委员会的成员搞了个boost库,这个库是 准 标准库,下一代的标准库新内容就是从其中择优选择。
2.4unique_ptr
c++11提供了个新的智能指针。这个智能指针更加的粗暴,既然拷贝有问题,那就不让拷贝就好了。这个指针适用于不允许拷贝的情况
template<class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { cout << "delete" << endl; delete _ptr; } SmartPtr(const SmartPtr& ptr) = delete;//利用c++11新增的特性 //直接禁止 //因为默认生成的赋值也是浅拷贝,那这个也要禁掉 SmartPtr<T>& operator=(const SmartPtr<T>& ptr) = delete; T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
2.5 shared_ptr
这也是c++11提供的,但这个是可以支持拷贝的,也解决了多次释放的问题。
这个版本的核心思路就是:引用计数。
每多(构造)一个对象管理同一份资源,计数+1,每销毁(析构)一个对象,计数-1,直到计数-1后为0的那个对象,这个对象的析构会执行释放资源的代码
shared_ptr<int> s2(new int(1)); cout << s2.use_count() << endl; //1 auto s3(s2); cout << s2.use_count() << endl; //2 shared_ptr<int[]>s4(new int[30]); auto s5 = make_shared<int>(10); //返回一个shared_ptr对象,这个主要是将资源和引用计数放在了一个结构体,减少内存碎片等问题
实现
在实现上,这个引用计数首先不可能用私有的普通int来,那么静态成员呢?乍一看是对的。
但是要注意,静态成员是属于这个类(包括模板)的,也就是说同一个类的所有对象,静态成员都是同一个。这个情况下
template<class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { cout << (*_ptr)<<": "<<_cnt << endl; if (--_cnt == 0) { cout << "delete " << (*_ptr) << endl; delete _ptr; } } SmartPtr(const SmartPtr<T>& ptr) { ++_cnt; _ptr = ptr._ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } int use_cnt() { return _cnt; } private: T* _ptr; static int _cnt; }; template<class T> int SmartPtr<T>::_cnt = 1; int main() { SmartPtr<int> s1(new int(1)); cout << s1.use_cnt() << endl; //引用计数为:1 SmartPtr<int> s2(s1); //引用计数为:2 cout << s2.use_cnt() << endl; SmartPtr<int> s3(new int(4)); //引用计数为:2 cout << s3.use_cnt() << endl; /* 1 2 2 4: 2 1: 1 delete 1 -572662307: 0 */ return 0; }
从上面的代码可以看出,只释放了存1的空间,存4的空间没有被释放。因为析构的顺序是从后往前,s3-s2-s1,因为静态成员是同一个且s3是非拷贝构造,那么s2和s3构造出来的时候,计数都为2,s2的析构函数调用时,才会是计数为0的时候。
那么怎么解决呢?每一份资源都配一个引用计数
template<class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) , _cnt(new int(1))//每一个非拷贝构造构造出来的对象都有一个单独的引用计数 //因为拷贝构造的时候,说明2个对象管理的同一份资源,需要用同一个计数 //而非拷贝构造的时候,说明这个对象是管理一个新的资源,这时候不能用同一个计数 {} ~SmartPtr() { release(); } void release() { //当前资源对应的计数减到0的时候,才释放当前资源。 //不同资源对应不同的计数 if (--(*_cnt) == 0) { cout << "delete" << endl; delete _ptr; delete _cnt; } } SmartPtr<T>& operator=(const SmartPtr<T>& ptr) { /*if (this != &ptr)*// if(_ptr!=ptr._ptr)//以免自己赋值给自己,又或者管理同一份资源的对象赋值给自己 { release(); _ptr = ptr._ptr; _cnt = ptr._cnt; ++(*_cnt); //注意,这里将前一个资源的计数--的时候,不能光减,还要判断 } return *this; } SmartPtr(const SmartPtr<T>& ptr) { _ptr = ptr._ptr; _cnt = ptr._cnt; ++(*_cnt); } int use_cnt() { return (*_cnt); } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //T* operator&() { // return _ptr; //} private: T* _ptr; int* _cnt; };
问题
shared_ptr的线程安全问题,我会在线程的文章里再次提及的。
这里先说循环引用的问题
class ttt { public: ttt(int val=0) { _val = val; next = nullptr; prev = nullptr; } int _val; shared_ptr<ttt> next; shared_ptr<ttt> prev; ~ttt() { cout << 1 << endl; } }; int main() { shared_ptr<ttt>s1(new ttt(3)); shared_ptr<ttt>s2(new ttt(2)); s1->next = s2; //s2->prev = s1; //两者存一,就输出2个1,都存在就1个都不输出。 return 0; }
先说下,为什么两者存1,就输出2个1。
首先是当执行完 s1->next = s2;的时候,s1的引用计数为1,s2的引用计数为2.
然后是s2先析构,引用计数--,为1,t2不执行析构;然后是s1开始析构,s1的引用计数--,为0,开始调用t3的析构函数,输出1,继续其余成员的析构,执行s1的next的析构的时候,s2的引用计数--,为0,调用t2的析构函数,输出1。
那为什么都存在的时候会都不输出呢?
首先是当执行完s2->prev = s1;的时候,s1的引用计数为2,s2的引用计数为2.
然后是s2先析构,引用计数--,为1,t2不执行析构;然后是s1开始析构,s1的引用计数--,为1,t3不执行析构。所以什么都不会输出。
那么梳理一下:
首先s2、s1都执行了析构,此时s2、s1的计数都为1
1、s2什么时候释放资源?
s1的next析构了,才能让s2的引用计数为0,从而释放资源
2、s1的next什么时候析构?
s1释放资源的时候,才能让s1的next析构
3、s1什么时候为释放资源?
s2的prev执行析构,才能让s1的引用计数为0,让s1释放资源
4、s2的prev什么时候执行析构?
s2释放资源的时候,才能让s2的prev执行析构。
1-2-3-4-1-2-3-4
这个问题就循坏不止了
这个问题的核心就是,2个自定义对象内部各自有一个智能指针对象管着对方自定义对象
解决方案就是把自定义对象内部的智能指针从shared_ptr改成weak_ptr
weak_ptr不参与管理,不会增加或减少引用计数,也不会自动释放资源
它只支持拷贝构造和无参构造、赋值,且拷贝构造和赋值只接受weak_ptr或者shared_ptr对象。库的shared和weak实现会更加的复杂,考虑了别的问题,比如shared内部的引用计数是独立的一个类对象跟weak共用,当一份资源引用计数已经为0,那么在释放资源之后,weak在使用的时候还要提前用expired()函数来看是否过期(也就是看引用计数是否为0),过期的话这个weak就不要用。
定制删除器
class ttt { public: ttt(int val=0) { _val = val; } int _val; weak_ptr<ttt> next; weak_ptr<ttt> prev; ~ttt() { cout << 1 << endl; } }; template<class T> class deleteptr { public: void operator()(T* ptr) { delete[] ptr; } }; int main() { shared_ptr<ttt>s1(new ttt[10], deleteptr<ttt>());//仿函数 shared_ptr<ttt>s2(new ttt[10], [](ttt* ptr) {delete[] ptr; });//lambda表达式 shared_ptr<FILE>s3(fopen("te2.cpp", "r"), [](FILE* ptr) {fclose(ptr); });//文件 return 0; }
实现
class ttt { public: ttt(int val=0) { _val = val; } int _val; weak_ptr<ttt> next; weak_ptr<ttt> prev; ~ttt() { cout << 1 << endl; } }; template<class T> class deleteptr { public: void operator()(T* ptr) { delete[] ptr; } }; template<class T> class SmartPtr { public: template<class D> SmartPtr(T* ptr,D del) :_ptr(ptr) , _cnt(new int(1)) ,_del(del) {} SmartPtr(T* ptr) :_ptr(ptr) , _cnt(new int(1)) {} ~SmartPtr() { release(); } void release() { if (--(*_cnt) == 0) { cout << "delete" << endl; _del(_ptr);//本质上就是调用函数,函数内部使用delete释放特定的空间 delete _cnt; } } SmartPtr<T>& operator=(const SmartPtr<T>& ptr) { if (_ptr != ptr._ptr) { release(); _ptr = ptr._ptr; _cnt = ptr._cnt; ++(*_cnt); } return *this; } SmartPtr(const SmartPtr<T>& ptr) { _ptr = ptr._ptr; _cnt = ptr._cnt; ++(*_cnt); } int use_cnt() { return (*_cnt); } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; int* _cnt; function<void(T*)>_del = [](T* ptr) {delete ptr; };//因为不管是仿函数还是lambda表达式还是函数指针,都可以被放进包装器 //且返回类型,参数都是确定的。 //再考虑到如果不传包装器,包装器为空,那析构的时候,无论是什么指针,传入_del都会报错 //因为此时的_del是空的,所以这里特意写个默认的lambda表达式,只用于释放一般的指针。 }; int main() { SmartPtr<ttt>s1(new ttt[10], deleteptr<ttt>());//仿函数 SmartPtr<ttt>s2(new ttt[10], [](ttt* ptr) {delete[] ptr; });//lambda表达式 SmartPtr<FILE>s3(fopen("te2.cpp", "r"), [](FILE* ptr) {fclose(ptr); });//文件 SmartPtr<ttt>s1(new ttt(10)); return 0; }