我们在异常代码中留下了一个坑没填,就是下面这个代码关于array多次释放资源的问题。
#include <iostream>
using namespace std;double Division(int a, int b)
{if (b == 0){throw "Division by zero condition";}return (double)a / (double)b;
}void Function()
{int* array_1 = new int[10];//(6)new 本身也会抛出异常int* array_2 = new int[10];//(7)new 本身也会抛出异常try {int len, time;cin >> len >> time;cout << Division(len, time) << '\n';//(1)假设抛出“除零异常”}catch (...)//(2)“除零异常”被代码拦截下来{//(3)先释放 array,防止内存泄露cout << "delete_1[] " << array_1 << '\n';delete[] array_1;cout << "delete_2[] " << array_2 << '\n';delete[] array_2;throw;//(4)重新将异常抛出去}cout << "delete_1[] " << array_1 << '\n';delete[] array_1;cout << "delete_2[] " << array_2 << '\n';delete[] array_2;
}int main()
{try{Function();}catch (const char* error)//(5)“除零错误”会在这里被接收{cout << error << endl;}catch (const exception& e)//(8)new 的异常可以在这里被接收{cout << e.what() << '\n';}return 0;
}
这样捕捉异常就会十分困难,稍不注意就会导致内存泄漏,因此我们有必要学习智能指针。
1.内存泄漏分类
要解决这一痛病,就需要“对症下药”,那常见的内存泄露有哪一些呢?C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(
Heap leak) :堆内存是指程序执行中,通过malloc、calloc、realloc、new等函数或关键字,从堆空间中分配的某块内存。该资源被用完后就必须通过调用对应的free或者delete释放掉。假设由于程序的设计错误,导致这部分内存资源没有被正常释放,那么以后这部分空间将无法再被使用(持续占用),就会产生内存泄漏 - 系统资源泄漏(
system resource leak):指程序使用系统分配的资源,比如使用了:方套接字、文件描述符、管道等,没有使用对应的函数释放掉,导致系统资源的浪费,严重时会导致系统效能减少,系统执行不稳定,资源逐渐减少
2.内存泄漏应对
2.1.事前预防型
- 工程前期指定好良好的设计规范,编码过程中养成良好的编码规范,申请的内存空间需要匹配者释放(但如果碰上带有异常的代码,就算注意释放了,可能还会出现问题)
- 采用
RAII思想和智能指针来管理资源
2.2.事后查错型
- 有些公司内部规范使用内部实现的私有内存管理库,这套库自带内存泄漏检测的功能选项
- 出问题了使用内存泄漏工具检测(不过很多工具都不够靠谱,或者收费昂贵)
3.RAII和库实现
3.1.智能指针模拟
实际上智能指针的原理没那么复杂,我们可以简单实现一个智能指针,让指针在是否抛出异常的情况下都可以正常释放,进而解决内存泄漏的问题。
#include <iostream>
using namespace std;class SmartPtr
{
public:SmartPtr(int* ptr): _ptr(ptr){}~SmartPtr(){delete _ptr;cout << "delete " << _ptr << '\n';}
private:int* _ptr;
};int main()
{int* p = new int;SmartPtr sp(p);//交给智能指针管理return 0;
}
实际上这就是RAII(Resource Acquisition Is Initialization)技术,即:“资源请求立即初始化”。
这翻译什么意思?好像和上面用到的东西没什么太大关联呀?实际上,这里的资源指new一类的调用,初始化就是将获取到的资源放入到构造函数内。
因此RAII实际就是一种利用对象生命周期控制持续资源(例如:内存、文件句柄、网络连接、互斥量等等)的技术。
在C++中体现为“在对象生命周期结束时使用析构函数释放”。实际上就是将管理资源的责任转移给了一个对象,这样做的好处很明显:
- 无需显式释放资源,将释放资源的责任交给编译器,而不是
C++程序员 - 对象在生命周期内始终保持有效
库中的智能指针就是类似的原理(RAII是这类技术的统称,比如:智能指针、锁的守卫等具体实现)。
但是生成的智能指针对象需要像指针一样被用户使用,这就是库中智能指针和我们实现的简易智能指针的最大区别(因此重载*和->和()等符号就尤为重要、以及关于智能指针对象拷贝、delete[]实现的问题)。
不过在考虑拷贝问题之前,我们先来试着将我们自己的SmartPtr{};改造为一个“指针”,也就是重载两个和指针脱不了干系的符号:*和->,并且改为类模板。
#include <iostream>
using namespace std;struct Data
{Data(): _data_1(1), _data_2(2), _data_3(2) {}int _data_1;int _data_2;int _data_3;
};template <typename Type>
class SmartPtr
{
public:SmartPtr(Type* ptr): _ptr(ptr){}~SmartPtr(){delete _ptr;cout << "delete " << _ptr << '\n';}Type& operator*(){return *_ptr;}Type* operator->(){return _ptr;}
private:Type* _ptr;
};int main()
{SmartPtr<int> sp_1(new int[10]);//交给智能指针管理(*sp_1) = 5;SmartPtr<Data> sp_2(new Data);sp_2->_data_1 = 10;//完整写法为 sp_2.operator->()->_data_1sp_2->_data_2 = 10;sp_2->_data_3 = 10;return 0;
}
行为果然很像指针,接下来我们尝试想指针一样赋值或初始化,可以发现代码会奔溃:
#include <iostream>
using namespace std;struct Data
{Data(): _data_1(1), _data_2(2), _data_3(2) {}int _data_1;int _data_2;int _data_3;
};template <typename Type>
class SmartPtr
{
public:SmartPtr(Type* ptr): _ptr(ptr){}~SmartPtr(){delete _ptr;cout << "delete " << _ptr << '\n';}Type& operator*(){return *_ptr;}Type* operator->(){return _ptr;}
private:Type* _ptr;
};int main()
{SmartPtr<int> sp_1(new int[10]);//交给智能指针管理(*sp_1) = 5;SmartPtr<Data> sp_2(new Data);sp_2->_data_1 = 10;//完整写法为 sp_2.operator->()->_data_1sp_2->_data_2 = 10;sp_2->_data_3 = 10;SmartPtr<Data> sp_3 = sp_2;//模仿指针拷贝,代码崩溃return 0;
}
原因是析构了两次资源,这怎么解决呢?深拷贝?不行,为什么?因为指针赋值是希望两个指针都可以拥有对同一块内存的管理权限。而我们以往实现的容器,在拷贝后都有属于各自容器对象的资源,而指针可以共享,这是智能指针拷贝的最大问题。
补充:除了
auto_ptr{};的探索失误,准标准C++库boost也做了很多智能指针的探索工作,这里给一个Boost官方网站供您探索一二。
3.2.auto_ptr
那怎么办呢?使用引用计数即可解决这个问题,但C++在最开始的时候并不是这么做的。在C++ 98中,第一次出现智能指针auto_ptr{};,在处理这方面的问题时,使用了“管理权转移”,认为“两个对象管理一份资源”是存在问题的,因此在上面代码sp_3 = sp_2中就会表现为:sp_2的资源转移给sp_3。
这种实现非常简单,但是不符合我们对指针的理解,容易出现对象悬空的问题(资源被别的对象掠夺了)。
3.3.unique_ptr
因此auto_ptr{};这种智能指针在后来也被委员会标记为弃用,改用智能指针unique_ptr{};。
这个做得很极端,直接让智能指针对象禁用拷贝,这如何实现?两种方法:
- 使用自定义的拷贝构造和赋值重载(而不是默认拷贝构造和默认赋值重载),但是限定为私有,并且只声明不实现
- 使用
C++11语义重载后的关键字delete(”删除“成员函数)禁止编译器生成默认的拷贝构造和赋值重载
3.4.shared_ptr
但是unique_ptr{};这种做法有点逃避现实问题的意思,因此后面又出现了新的智能指针shared_ptr{};,支持引用计数后,智能指针就允许拷贝构造和赋值重载了,但是其具体实现有一些细节需要注意。
但是怎么计数呢?直接定义一个成员变量?不可以,这会导致多个成员计数不同步。那改成静态成员变量?依旧不行,会影响到同类型的指向其他资源的智能指针对象,这就更坑了。
那另辟蹊径,直接将引用计数存放到指向的资源处,但是更加麻烦了,还要考虑更多问题。
最后还有一种方案,每一个对象新增计数指针_pcount,单独指向的空间存储计数数据,指向同一块空间资源的智能指针对象,其内部的_pcount指针也需要指向同一块计数空间。
#include <iostream>
using namespace std;struct Data
{Data(): _data_1(1), _data_2(2), _data_3(2) {}int _data_1;int _data_2;int _data_3;
};template <typename Type>
class SmartPtr
{
public:SmartPtr(Type* ptr): _ptr(ptr), _pcount(new int(1)){cout << "deposit " << _ptr << '\n';}void _release(){if (--(*_pcount) == 0){cout << "delete " << _ptr << '\n';delete _ptr;delete _pcount;}else{cout << "count-1 " << _ptr << '\n';}}~SmartPtr(){_release();}SmartPtr(const SmartPtr<Type>& sp): _ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);cout << "count+1 " << _ptr << '\n';}SmartPtr<Type>& operator=(const SmartPtr<Type>& sp)//这个代码需要注意一下{_release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);return *this;}Type& operator*(){return *_ptr;}Type* operator->(){return _ptr;}
private:Type* _ptr;int* _pcount;
};int main()
{SmartPtr<int> sp(new int[10]);//交给智能指针管理(*sp) = 5;SmartPtr<Data> sp1(new Data);sp1->_data_1 = 10;//完整写法为 sp_2.operator->()->_data_1sp1->_data_2 = 10;sp1->_data_3 = 10;SmartPtr<Data> sp2 = sp1;SmartPtr<Data> sp3(new Data);sp2 = sp3;return 0;
}
3.5.weak_ptr
但是很遗憾,尽管shared_ptr{};已经很完美了,但实践中依旧出现了一个关于”循环引用“的问题,我们简单介绍一下什么是”循环引用“:
#include <iostream>
using namespace std;template <typename Type>
class SmartPtr
{
public:SmartPtr(Type* ptr = nullptr): _ptr(ptr), _pcount(new int(1)){}void _release(){if (--(*_pcount) == 0){delete _ptr;delete _pcount;}}~SmartPtr(){_release();}SmartPtr(const SmartPtr<Type>& sp): _ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}SmartPtr<Type>& operator=(const SmartPtr<Type>& sp)//这个代码需要注意一下{if (_ptr != sp._ptr)//防止给自己赋值{_release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}Type& operator*(){return *_ptr;}Type* operator->(){return _ptr;}
private:Type* _ptr;int* _pcount;
};struct ListNode
{int val;SmartPtr<ListNode> next;SmartPtr<ListNode> prev;~ListNode(){cout << "~ListNode()" << '\n';}
};int main()
{SmartPtr<ListNode> n1 = new ListNode;SmartPtr<ListNode> n2 = new ListNode;//发生循环引用n1->next = n2;n2->prev = n1;//析构顺序是先析构 n2,再析构 n1//但是这里的析构是“引用析构”,不会真的调用 delete 释放资源,//仅仅只是 (*pcount)-- //然后就导致:n2 的 prev 依旧指向 n1 的资源,n1 的 next 依旧指向 n2 的资源//想要析构 n1 的 next,就需要 n1 先析构//想要 n1 先析构,就需要 prev 先析构//想要 prev 先析构,就需要 n2 先析构//想要 n2 先析构,就需要 next 先析构//想要 next 先析构,就需要 n1 先析构//ok 想要 n1 析构?这不又回到开头了?return 0;
}
这就比较尴尬了,这种问题还是比较大的,我们经常会使用到类似链表的结构,一旦使用智能指针,那必然造成内存泄漏,并且是不可逆转的内存泄漏,用户即便知道这一循环引用,也无力释放…
由此诞生了weak_ptr{};,严格来说这不是智能指针,而是专门为了解决“循环引用”而生的。因此在库中,上述代码就会类似写成:
#include <iostream>
#include <memory>
using namespace std;struct ListNode
{int val;//shared_ptr<ListNode> next;//shared_ptr<ListNode> prev;weak_ptr<ListNode> next;weak_ptr<ListNode> prev;~ListNode(){cout << "~ListNode()" << '\n';}
};int main()
{shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << " " << n2.use_count() << '\n';//输出 1n1->next = n2;n2->prev = n1;cout << n1.use_count() << " " << n2.use_count() << '\n';//使用了 weak_ptr 还是 1,这和之前不同return 0;
}
什么原理呢?实际上weak_ptr{};在指向别的shared_ptr{};时不会进行计数,我们可以尝试简单实现一下weak_ptr{};。
#include <iostream>
using namespace std;template <typename Type>
class SmartPtr
{
public:SmartPtr(Type* ptr = nullptr): _ptr(ptr), _pcount(new int(1)){}void _release(){if (--(*_pcount) == 0){delete _ptr;delete _pcount;}}~SmartPtr(){_release();}SmartPtr(const SmartPtr<Type>& sp): _ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}SmartPtr<Type>& operator=(const SmartPtr<Type>& sp)//这个代码需要注意一下{if (_ptr != sp._ptr)//防止给自己赋值{_release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}Type& operator*(){return *_ptr;}Type* operator->(){return _ptr;}int use_count() const{return *_pcount;}Type* get() const{return _ptr;}private:Type* _ptr;int* _pcount;
};template <typename Type>
class WeakPtr
{
public:WeakPtr(): _ptr(nullptr){}WeakPtr(SmartPtr<Type>& sp): _ptr(sp.get()){}WeakPtr<Type>& operator=(const SmartPtr<Type>& sp){_ptr = sp.get();return *this;}Type& operator*(){return *_ptr;}Type* operator->(){return _ptr;}private:Type* _ptr;
};struct ListNode
{int val;WeakPtr<ListNode> next;WeakPtr<ListNode> prev;~ListNode(){cout << "~ListNode()" << '\n';}
};int main()
{SmartPtr<ListNode> n1(new ListNode);SmartPtr<ListNode> n2(new ListNode);cout << n1.use_count() << " " << n2.use_count() << '\n';n1->next = n2;n2->prev = n1;cout << n1.use_count() << " " << n2.use_count() << '\n';return 0;
}