内存泄漏
什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。
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 << div() << endl;delete p1;
}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
问题分析:上面的问题分析出来我们发现有什么问题?
在以上代码中,当用户输入的除数为0时,div函数就会抛出异常,这时程序的执行流就会跳到main函数中的catch块中的执行,最终导致Func函数中申请的内存资源没有得到释放
利用异常重新捕获解决
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{int* p1 = new int;try {div();}catch (const exception& e){delete p1;throw;}delete p1;
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
使用智能指针解决
template<class T>
class SmartPtr
{
public://RAIISmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){if (_ptr != nullptr){cout << "~SmartPtr" << endl;delete _ptr;}}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{SmartPtr<int> p1(new int(1));div();
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
这里使用的是一个RAII的一个简单技术
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针对象的拷贝问题
int main()
{SmartPtr<int> ptr1(new int);SmartPtr<int> ptr2(ptr1); //拷贝构造SmartPtr<int> ptr3(new int);SmartPtr<int> ptr4(new int); ptr3 = ptr4; //赋值重载return 0;
}
以上代码会产生一个浅拷贝问题,多个对象指向一个空间,最后在程序析构时,会对一个空间析构多次,导致程序崩溃
需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。
C++中的智能指针
std::auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。 auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份auto_ptr来了解它的原理
int main()
{auto_ptr<int> ptr1(new int(1));auto_ptr<int> ptr2(ptr5);(*ptr1)++;(*ptr2)++;return 0;
}
由于我们进行了拷贝构造,ptr1内存空间管理权转移到了ptr2上的原因,我们对*ptr1进行自加运算时,编译器在运行时会崩溃
auto_ptr的模拟实现
template<class T>
class auto_ptr
{
public://RAIIauto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ptr):_ptr(ptr._ptr){ptr._ptr = nullptr;}~auto_ptr(){if (_ptr != nullptr){cout << "~auto_ptr" << endl;delete _ptr;}}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
std::unique_ptr
unique解决拷贝的方式比较暴力,它是直接禁止对象之间进行拷贝
例如:
int main()
{unique_ptr<int> ptr1(new int);unique_ptr<int> ptr2(ptr1);return 0;
}
unique_ptr的模拟实现
template<class T>
class unique_ptr
{
public://RAIIunique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr != nullptr){cout << "~unique_ptr" << endl;delete _ptr;}}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}unique_ptr(unique_ptr<T>& ptr) = delete;unique_ptr& operator=(unique_ptr<T>& ptr) = delete;
private:T* _ptr;
};
std::shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
int main()
{shared_ptr<int> ptr1(new int(10));shared_ptr<int> ptr2(ptr1);cout << ++(*ptr1) << endl;cout << ++(*ptr2) << endl;shared_ptr<int> ptr3(new int(10));shared_ptr<int> ptr4(new int(11));ptr3 = ptr4;cout << *ptr3 << endl;cout << *ptr4 << endl;return 0;
}
shared_ptr的模拟实现
template<class T>
class shared_ptr
{
public://RAIIshared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) == 0){//判断_ptr是否为空if (_ptr == nullptr){cout << "~unique_ptr" << endl;delete _ptr;_ptr = nullptr;}delete _pcount;_pcount = nullptr;}}shared_ptr(unique_ptr<T>& ptr):_ptr(ptr._ptr), _pcount(ptr._pocunt){++(*_pcount);}shared_ptr& operator=(shared_ptr<T>& ptr){//避免ptr1和ptr1赋值或者ptr1和ptr2赋值(ptr1和ptr2地址相同)if (_ptr == ptr._ptr) return *this;if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = ptr._ptr;_pcount = ptr._pcount;++(*ptr._pcount);return *this;}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int GetCount(){return *_pcount;}T* Getshared_ptr(){return _ptr;}
private:T* _ptr; //管理的资源int* _pcount; //管理资源对应的引用计数
};
shared_ptr的缺点:循环引用
shared_ptr的循环引用问题在一些特定的场合下才会产生。比如定义如下带的结点类
struct Node
{Node* _prev = nullptr;Node* _next = nullptr;int val;~Node() {cout << "~Node" << endl;}
};int main()
{Node* ptr1 = new Node;Node* ptr2 = new Node;ptr1->_next = ptr2;ptr2->_prev = ptr1;delete ptr1;delete ptr2;return 0;
}
上述程序是没问题的,两个结点都可以正确的释放。为了避免程序中途返回或抛异常等原因导致结点未被释放,我们将这两个结点交给shared_ptr对象进行管理,这时为了让连接节点时的赋值操作也能执行,将需要把Node类中的next和prev的类型也设置未shared_ptr
struct Node
{shared_ptr<Node> _prev = nullptr;shared_ptr<Node> _next = nullptr;int val;~Node() {cout << "~Node" << endl;}
};int main()
{shared_ptr<Node> node1(new Node);shared_ptr<Node> node2(new Node);node1->_next = node2;node2->_prev = node1;return 0;
}
使用new的方式申请到两个Node结点并交给智能指针管理之后,这两个资源引用计数都被加到了1
将这两个节点连接起来后,现在node1和prev共同管理一块资源,node2和next共同管理一块资源
即资源1和资源2的 引用计数变为2
当出了main的作用域后,node1和node2的生命周期也结束了,所以这两份资源的引用计数都被减到了1
循环引用导致资源没有被释放的原因:
当资源的引用计数减为0时,这些资源才会被释放,左边的资源什么时候释放,当prev释放的时候释放,那么prev什么时候释放,当左边资源释放的时候释放,右边的资源什么时候释放,当next释放的时候释放,next什么时候释放,右边资源释放的时候释放,于是这就变成了一个死循环,最终导致资源无法释放
如果只进行一个连接操作时,那么node1和node2生命周期结束时,就会有一个资源对应的引用计数被减为0,最后两个资源都可以释放了,这就是为什么只进行一个连接操作时这两个结点都可以释放的原因
std::weak_ptr
使用来weak_ptr来解决循环引用问题,weak_ptr并不是智能指针,它没有RAII的特性,它被创造出来就是用来解决shared_ptr循环引用问题的
weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一块资源,但不会增加这块资源的引用计数
将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源
struct Node
{weak_ptr<Node> _prev;weak_ptr<Node> _next;int val;~Node() {cout << "~Node" << endl;}
};int main()
{shared_ptr<Node> node1(new Node);shared_ptr<Node> node2(new Node);cout << node1.use_count() << endl;cout << node1.use_count() << endl;node1->_next = node2;node2->_prev = node1;cout << node1.use_count() << endl;cout << node1.use_count() << endl;return 0;
}
通过use_count获取这两个资源对应的引用计数就可以发现在连接结点之前它们资源的引用计数就是1,连接之后还是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数
weak_ptr的模拟实现
template<class T>
class weak_ptr
{
public:weak_ptr():_ptr(nullptr){}weak_ptr(shared_ptr<T>& p):_ptr(p._ptr) //注意这里p._ptr是私有的,我们需要使用shared_ptr专门提供的获取资源的接口{}~weak_ptr(){if (_ptr != nullptr){cout << "~weak_ptr" << endl;delete _ptr;}}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
说明一下:shared_ptr还会提供一个get函数,用于获取它管理的资源
std::shared_ptr的定制删除器
在shared_ptr中,它的析构函数的可以释放的数目是固定的,它并不支持大量数据的释放或者关闭一个文件指针
比如:
int main()
{shared_ptr<int> ptr(new int[20]); //errorshared_ptr<FILE> ptr(fopen("test.txt", "r")); //errorreturn 0;
}
在以上代码中,我们使用new申请20个空间的地址和打开了一个文件,在对象周期结束的时,如果它还是以delete的方式释放资源,那一定会存在内存泄漏,可能还会触发程序崩溃,使用new[ ]的方式申请空间的时候,我们必须使用delete[ ]的方式进释放,而文件指针则需要调用fclose函数进行文件的关闭
这时将需要使用定制删除器来控制释放资源的方式,在C++11的标准库中的shared_ptr提供了如下构造函数:
template <class U, class D>
shared_ptr (U* p, D del);
参数说明:
p:需要让智能指针管理的资源
del:删除器,这个删除器是一个可调用对象,比如函数指针,仿函数,lambda表达式以及被包装器包装后的可调用对象
当智能指针管理的资源不是以new的方式申请到的内存空间,就需要在构造智能指针对象时传入定制的删除器
比如:
template<class T>
struct deleteArray
{void operator()(T* _ptr){return delete[] _ptr;}
};int main()
{shared_ptr<int> ptr1(new int[20], deleteArray<int>()); shared_ptr<A> ptr2(new A[20], deleteArray<A>()); shared_ptr<FILE> ptr3(fopen("test.txt", "r"), [](FILE* pf) {cout << "fclose" << endl;fclose(pf);}); return 0;
}
定制删除器的模拟实现
template<class T>
struct deleteArray
{void operator()(T* _ptr){return delete[] _ptr;}
};namespace nxbw
{template<class T>class shared_ptr{public:shared_ptr(const T* ptr): _ptr(ptr), _pcount(new int(1)){}shared_ptr(shared_ptr<T>& s):_ptr(s._ptr), _pcount(s._pcount){*(_pcount)++;}template<class D>shared_ptr(T* ptr, D del):_ptr(ptr), _pcount(new int(1)), _del(del){}~shared_ptr(){if (--*(_pcount) == 0){cout << "~shared_ptr" << endl;_del(_ptr);delete _pcount;}}//赋值运算符重载shared_ptr<T>& operator=(shared_ptr<T>& s){if (_ptr == s._ptr) return *this;if (--*(_pcount) == 0){delete _ptr;delete _pcount;}_ptr = s._ptr;_pcount == s._pcount;++*(_pcount);}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* Get(){return _ptr;}int use_count(){return *(_pcount);}private:T* _ptr;int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; }; };
}
核心问题:怎么设计_del来接收del?
1.使用D来创建一个成员函数,因为D是构造函数的模板参数,并不是这个类的模板参数,所以这个方法是不可行的
2.使用包装器,它可以将仿函数包装起来然后使用包装器类型创建一个对象,创建之后,我们不需要关心D的类型,你传什么类型,包装器就接收什么样的类型,直接使用该成员就可以
3.给类模板增加一个参数,这个方法是可行的,但是很麻烦