优秀的编程知识分享平台

网站首页 > 技术文章 正文

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

nanyue 2024-08-10 18:35:31 技术文章 9 ℃

一、什么是内存泄漏

用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接管的是一个临时右值,赋值之后也销毁了。

所以只有在源指针将存在一段时间的情况下,进行赋值操作,才会报错,如:

如果一定要这样赋值呢,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[]分配的内存。


想学习更多C/C++编程知识,可以加入C/C++学习交流群:587250700,更多详情点击下方

Tags:

最近发表
标签列表