智能指针——从根源上解决内存泄漏的问题

一、什么是内存泄漏

用new在堆中分配内存后,没有调用delete,然后指针所在的内存因为作用域规则而被释放。这时候堆中分配的内存仍在,但是指向它的指针没了,导致程序的整个生命周期内都无法访问这部分内存,也就是说内存泄漏了。
如果程序只运行一次两次,偶尔的内存泄漏,其实问题不大。但是如果程序是一直在运行的,那么连续的内存泄漏,迟早会导致程序崩溃,甚至会导致其他程序的崩溃,因为堆中内存已经耗尽了。
例:

void test(){
	char* p = new char[1024*1000*100];//分配100M内存
}
int main(){
	while(true){
		test();
		Sleep(1000);
	}
	system("pause");
	return 0;
}

这个程序会不断的分配内存,但是没有释放,程序很快就会因为内存耗尽而崩溃。所以在动态分配内存的时候,用完了不要忘了释放内存。

void test(){
	char* p = new char[1024*1000*100];//分配100M内存
	delete[] p;//释放内存
}

但是!!!但凡涉及到”不要忘了“、”要记得“之类的操作,通常都会忘记。只是这个例子简单,一眼就能看出来,如果中间还有几百行代码,可能写完之后就忘记释放内存了。可能有机智的小伙伴会提前写好delete[],然后在中间插入代码。但是,以后修改相关代码的时候,还要去找delete[],也是挺麻烦的。

还有更麻烦的事情!!!
例:

void test(){
	char* p = new char[1024*1000*100];//分配100M内存
	int i = 0;
	//接下来对i进行一些操作
	if (i > 10){
		throw exception();//引发异常
	}
	delete[] p;
}
int main(){
	while(true){
		test();
		Sleep(1000);
	}
	system("pause");
	return 0;
}

这里没有忘记delete[],但是真的释放了内存吗?不一定!
i大于10的时候,引发了异常,程序跳到了处理异常的代码块那里,并没有释放内存!

二、智能指针

知道了内存泄漏的原因,既然delete不是最优解,那么有没有更好的办法?有!那就是智能指针。
首先来看一下需求:不管test()函数正常终止还是因为异常而终止,指针p都会被删除,如果同时能将指针指向的内存也释放,那就完美了。

细心的朋友有没有发现,类对象的析构函数似乎能做到这一点!如果指针p是一个对象,那么在对象过期时,可以让它的析构函数去释放指针指向的内存。

智能指针就是这样操作的
下面介绍三个智能指针模板auto_ptr,unique_ptr,shared_ptr。它们的定义在头文件memory中,大家感兴趣的话可以去看一下,这里就不说了。

1.怎样使用智能指针

auto_ptr<char>pc(new char);//pc是char类型的智能指针对象
unique_ptr<int>pi(new int);//pi是int类型的智能指针对象
shared_pte<double>pd(new double);//pd是double类型的智能指针对象

之前的代码可以改成这样:

#include<memory>
using namespace std;
void test(){
	unique_ptr<char>ptr(new char[1024*1000*100]);
	//char* p = new char[1024*1000*100];//分配100M内存
	int i = 0;
	//接下来对i进行一些操作
	if (i > 10){
		throw exception();//引发异常
	}
	//delete[] p;
}

在离开这个函数的时候ptr被删除,因为ptr是一个智能指针对象,所以会自动调用析构函数,它的析构函数会使用delete[]来释放ptr指向的内存。

2.三个智能指针模板之间的区别

auto_ptr是C++98提供的,C++11放弃了它,提供了另外两种。

2.1 shared_ptr为什么能取代auto_ptr

下面来撸一段不适合使用auto_ptr的代码

auto_ptr<string>ptr1(new string("aaa"));
auto_ptr<string>ptr2;
ptr2=ptr1;
cout<<*ptr1<<endl;

这样的代码是不确定的,问题出在ptr2=ptr1;因为auto_ptr的策略是,一个对象只能有一个智能指针指向它,这样做的好处是可以防止重复释放内存,但问题是,赋值操作把ptr1的所有权转让给了ptr2,然后ptr1不再指向有效的数据,导致其行为不确定,留下了一个悬挂指针。
用shared_ptr代替auto_ptr可以解决这个问题。

shared_ptr<string>ptr1(new string("aaa"));
shared_ptr<string>ptr2;
ptr2=ptr1;
cout<<*ptr1<<endl;

shared_ptr可以跟踪指向对象的智能指针数(引用计数),比如赋值的时候,计数加1,指针过期时,计数减1,当最后一个指向对象的指针过期时,才调用delete。这样既避免了重复释放内存,又不会留下悬挂指针。

2.2 unique_ptr为什么能取代auto_ptr

还是上面那个问题,unique_ptr的策略跟auto_ptr是一样的,但它在赋值的时候就会报错。
但是还有这样一个骚操作:

unique_ptr<string> test(string str){
	unique_ptr<string> temp(new string(str));
	return temp;
}
int main(void){	
	unique_ptr<string> p;
	//test()返回一个临时智能指针,然后赋给了p。
	p=test("hahaha");

	unique_ptr<string> p1;
	//p1接管了一个临时匿名智能指针
	p1=unique_ptr<string>(new string("hello world!"));
	return 0;
}

编译器居然允许这种操作!这是因为temp是一个临时的智能指针,赋值之后很快就被销毁了,不会留下隐患。而p1接管的是一个临时右值,赋值之后也销毁了。
所以只有在源指针将存在一段时间的情况下,进行赋值操作,才会报错,如:

unique_ptr<string>p1(new string("hahaha"));
unique_ptr<string>p;
p = p1;//不允许

如果一定要这样赋值呢,auto_ptr都能这样赋值,unique_ptr表示不服:我也要这样操作!
可以!不然怎么能取代auto_ptr呢。unique_ptr使用了移动构造函数和右值引用。下面上代码:

unique_ptr<string>p1(new string("hahaha"));
unique_ptr<string>p;
//p = p1;//不允许
p = move(p1);//允许
p1= unique_ptr<string>(new string("lalala"));//一定要给p1赋新值

由于p1是一个左值,所以直接赋值使用的是复制赋值运算符,赋值之后p1成为了一个悬挂指针,这也是auto_ptr中的隐患,unique_ptr禁止这样赋值。
而move()函数将p1伪装成一个右值,所以使用的是移动赋值运算符,赋值之后p1指向了NULL,而空指针是没有隐患的,如果要重新使用p1的话,必须给它赋新值。
另外unique_ptr还有一个优点,它可以用于数组。
unique_ptr<int []>p(new int[10]);

3.注意事项

1.智能指针对象过期时,析构函数会调用delete/delete[]来释放内存,所以智能指针必须指向new/new[]分配的内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值