大家都知道C/C++中对内存进行动态申请的时候, 堆上开辟的空间一定要进行释放, 否则造成了内存泄漏是一件十分危险的事情, 但是我们在平时写代码的时候, 难免会忘记释放空间, 或者当程序抛出异常的时候, 释放空间的代码被跳过, 执行到, 这都是造成内存泄漏的原因. 为了解决这一情况, C++中引入了智能指针,来对动态开辟的内存进行管理.
RAII的思想是指, 利用对象的生命周期来对程序的资源进行管理, 即对一个泛型的指针进行封装, 以一个对象的身份来管理动态开辟的空间, 而不单单是一个指针的身份.
当我们要进行动态内存的开辟是, 可以创建一个只能指针的对象, **在该对象的构造函数中进行空间的申请, 在对象的析构函数中进行空间的释放.**这样不仅解决了不需要用户手动释放空间的问题, 还保证了, 资源在对象的生命周期内始终有效.
接下来实现一个简单的实现RAII思想的指针
template<class T>
class AutoPtr{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{
cout<<"AutoPtr()"<<endl;
}
~AutoPtr(){
if(_ptr!=nullptr){
delete _ptr;
_ptr=nullptr;
}
cout<<"~AutoPtr()"<<endl;
}
private:
T* _ptr;
};
void fun(){
int* p=new int(4);
vector<int> v(100000000000,0);
delete p;
}
int main(){
try{
fun();
}
catch(const exception& e){
cout<<e.what()<<endl;
}
catch (...){
cout<<"其他异常"<<endl;
}
return 0;
}
上述程序中, 由于vector的开辟长度太长, 当层序运行到此处是,就会抛出异常, 程序就会转调到main程序中的捕获异常出, 但是fun函数中的对P指针的释放就没有执行到, 这样就造成了内存泄漏.
我们将fun函数改为如下程序
void fun(){
AutoPtr<int> p(new int(2));
vector<int> v(10000000000000,0);
}
此时即使我们的fun函数中抛出异常, 但是当程序跳转到main函数中的时候, 也就是fun函数生命周期结束的时候, 会调用p对象的析构, 从而对空间进行释放.
注意 : 只能指针在进行使用时, 必须保证开辟空间与常见对象在同一步进行, new操作符返回的是一个指针类型, 因此直接在对象的构造中使用new操作符
当然, 只能指针的实现,还包括了指针的功能
template<class T>
class AutoPtr{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{
cout<<"AutoPtr()"<<endl;
}
~AutoPtr(){
if(_ptr!=nullptr){
delete _ptr;
_ptr=nullptr;
}
cout<<"~AutoPtr()"<<endl;
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
private:
T* _ptr;
};
int main(){
AutoPtr<int> p(new int(1));
AutoPtr<int> p1(p);
return 0;
}
注意 : 既然只能实现指针的基本操作,那么就也有指针的拷贝与赋值操作, 指针的拷贝调用的是类的默认拷贝构造, 而默认拷贝构造只是一个浅拷贝, 浅拷贝只拷贝值, 不会拷贝对象中的空间, 因此在执行上述代码后,就会造成两个指针指向同一块空间, 这样当main函数的生命周期执行完之后, 在对 P ,P1进行洗后的时候, 会对同一块空间释放两次, 这样回导致程序的崩溃
在STL中的auto_ptr是最早的一款智能指针, 它对指针的拷贝问题的解决是, 永远保证只有一个智能指针指向一个空间, 当对当前智能指针进行拷贝的时候, 就将原来的指针置空.
在上述的 AutoPtr中对构造函数与赋值运算符重载进行修改
template<class T>
class AutoPtr{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{
cout<<"AutoPtr()"<<endl;
}
~AutoPtr(){
if(_ptr!=nullptr){
delete _ptr;
_ptr=nullptr;
}
cout<<"~AutoPtr()"<<endl;
}
AutoPtr(AutoPtr<T>& p){
_ptr=p._ptr;
p._ptr=nullptr;
}
AutoPtr<T>& operator=(AutoPtr<T>& p){
if(this!=&p ){
if(_ptr!=nullptr){
delete _ptr;
}
_ptr=p._ptr;
p._ptr=nullptr;
}
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
private:
T* _ptr;
};
注意 : 由于auto_ptr只能有一个智能指针指向空间, 因此auto_ptr是一个不完善的智能指针, 因此禁止使用
由于auto_ptr的不完备性, 容易造成空间的二次释放, 因此c++11中有提供了一个unique_ptr, 他处理问题的方法很简单, 直接将拷贝构造与赋值运算符重载封死, 就最简单的解决了问题.
template<class T>
class UniquePtr{
public:
UniquePtr(T* ptr)
:_ptr(ptr)
{
cout<<"AutoPtr()"<<endl;
}
~UniquePtr(){
if(_ptr!=nullptr){
delete _ptr;
_ptr=nullptr;
}
cout<<"~AutoPtr()"<<endl;
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
UniquePtr(AutoPtr<T>& )=delete;
operator=(AutoPtr<T>& )=delete;
private:
T* _ptr;
};
在C++11中还有一个更加全面的智能指针 sheard_ptr 它实现了前面俩个智能指针不能实现的拷贝与赋值.
它的原理是, 在智能指针的内部增加一个计数器, 当有多个智能指针指向一块空间是, 计数器就会加一, 当执行指正对象执行析构的时候, 先对计数器进行减一操作, 之后在判断计数器的结果是否为零, 如果为零,则对空间进行释放, 不为零则不作操作.
template<class T>
class SharedPtr{
public:
SharedPtr(T* ptr)
:_ptr(ptr)
,_count(new int(1));
{}
~SharedPtr(){
(*_count)--;
if(*_count==0){
delete _ptr;
_ptr=nullptr;
}
}
SharePtr(SharePtr<T>& p)
;_ptr(p._ptr)
,_count(p._count)
{
(*_count)++;
}
SharePtr<T>& operator=(SharePtr<T>& p){
if(this!=&p){
(*_count)--
if(*_count==0){
delete _ptr;
delete _count;
}
_ptr=p._ptr;
_count=p._count;
}
(*_count)++;
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
int GitCount(){
return *_count;
}
private:
T* _ptr;
int* _count;
};
注意:在shared_ptr的赋值运算符操作中, 赋值操作是将一个指针替换成另一个, 所以是要对指针记性判空, 删除原来的值,在进行赋值, 而在shared_ptr中, 就需要先判断_count的值是否是0 如果是0 则delete 之后赋值, 如果不是, 则直接改变智能指针内部的成员的值即可