浅谈C++普通指针和智能指针管理动态内存的陷阱
浅谈c++普通指针和智能指针管理动态内存的陷阱
前言:
c++中动态内存的管理主要是使用new/delete表达式和std::allcator类。为了管理动态内存更加安全,c++11新标准库推出了智能指针。这里只讨论使用他们在使用过程常见的错误以及解决方法,不过多讨论语法。
一、使用new和delete管理动态内存三个常见的问题。
1、忘记释放(delete)内存。忘记释放动态内存会导致人们常说的 “内存泄漏(memory leak)” 问题 ,因为这种内存永远不可能归还,除非程序退出。比如在某个作用域的代码如下:向系统申请了一块内存,离开作用域之前没有接管用户这块内存,也没有释放这块内存。
{ //.... int *p = new int(0); //.... }有两个方法可以避免以上问题:
(1) 在p离开它new所在作用域之前,释放这块内存。如:delete p
{ //.... int *p = new int(0); //.... delete p; //释放p的向系统申请的内存 p = nullptr; //尽管在这个地方没必要,这是一个好习惯,也是动态管理内存常见的出错的地方。等下会说到。 }
(2) 接管p的向系统申请的内存。 比如通过赋值,函数返回值等。
int *panother; { //.... int *p = new int(0); //.... panother = p; //panother接管p所指向的内存。 } //panother do something delete panother; //通关panother,将p所申请的内存归还系统。2、使用已经释放内存的对象。这种行为是未定义的,通过在释放内存后将指针设置位空指针(nullptr),有时可以避免这个问题(这是基于一个前提条件,使用动态分配内存对象前,需要检查该对象是否指向空(nullptr))。假如不对已经释放内存的对象赋值空指针,他的值是未定义的,就好比其他变量,使用未初始化的对象,其行为大都是未定义。
note: nullptr(c++11刚引入)是一种特殊类型的字面值,它可以被转换成任何其他指针类型。过去程序使用null的预处理变量来给指针赋值。 他们的值都是0。
使用已经释放内存的对象,如下代码:
{ //.... int *p = new int(0); // p do something delete p; //do other thing... std::cout<<*p<避免以上问题:(对已经释放内存对象赋于一个空指针,使用前进行判断是否为空指针)
{ //.... int *p = new int(0); // p do something delete p; //下面三条语句等价 p = nullptr; //p = null; //p = 0; //do other thing... if(p!=nullptr) //等价if(p) std::cout<<*p<note: 同样当我们定义一个指针时,如果没有立即为它分配内存,也需要将指针设置为空指针,防止不恰当使用。这里也涉及一个问题,new出来的内存也应该初始化,稍后再讲。;>3、同一块内存释放两次。 当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个对象进行了delete操作,对象的内存就归还给系统,如果我们随后有delete第二个指针,堆空间可能被破坏。
产生问题代码:
int *panother; { //.... int *p = new int(0); panother =p; //p do something.... delete p; } delete panother; //未定义行为避免这个问题:在delete p 之后, 将p置为一个空指针。
其次明白一个道理:delete p, p 必须指向一个空指针或者动态分配的内存,否则其行为未定义。
note: 这也很好就解释了为什么delete一个对象之后需要将该对象置为空指针,一是为了避免再次访问它出现未定义行为,二是为了避免再次delete它出现未定义行为。
小结:
1、定义一个指针需要初始化为空指针,(除非在定义的时候给它申请一块内存)
2、访问一个指针需要先判断该指针是否为空指针。
3、 释放一个指针之后,应该将它置为空指针。
二、使用std::allocator类管理动态内存
在继续了解标准库std::allocator类管理动态内存之前,有必要先了解new和delete具体工作(机制)。
new完成的操作:
(1): 它分配足够存储一个特定类型对象的内存
(2):为它刚才分配的内存中的那个对象设定初始值。(对于内置类型对象,就是默认初始化该它,对应类类型,调用constructor初始化)
delete完成的操作:
(1):销毁给定指针指向的对象
(2):释放该对象的对应内存
这儿有详细的讲叙,new, delete背后在做什么:
标准库std::allocator类帮助我们将内存分配和对象初始化分离开来,也允许我们将对象的销毁跟对象内存释放分开来。std::allocator分配的内存是原始的、未构造的。这里提供一个实例感受一下这个流程。然后注意事项跟new/delete类似。std::allocator在memory头文件中。
{ std::allocator allocate_str; //定义一个可以分配内存的string的allocator对象allocate_str std::string *p = allocate_str.allocate(1); //分配一个未初始化的string,p指向一块大小为string的原始内存 //std::cout<<*p<三、智能指针(smart pointer);>为了更加安全的管理动态内存,c++11新标准库推出了智能指针。主要是std::shared_ptr 、 std::unique_ptr 、std::weak_ptr(作为一个伴随类)。他们都位于memory后文件中。
智能指针的行为类似普通指针,一个重要区别是他负责自动释放所指向对象的内存。智能指针可以提供对动态分配的内存安全而又方便的管理,但这是建立在正确使用的前提下,为了正确使用智能指针,我们必须坚持一些基本规范。
在管理new分配出来的资源,shared_ptr类大概可以这样理解:(省略很多,最明显没有一个计数器,但有助加深对智能指针理解,我是这么认为。)
template class shared_ptr { public: shared_ptr(t* p=0):ptr(p) {} //存储对象 ~shared_ptr(){ delete ptr; } //删除对象 t* get() { return ptr;} private: t *ptr; };
1、不使用相同的普通指针初始化多个智能指针。因为当某个智能指针对象释放其内存时,这个普通指针相应会被delete,此时其他智能指针管理的资源已经被释放了,再对资源进行操作其行为是未定义。请看下面代码。
{ int *p = new int(10); std::cout<<*p< ptr1(p); //... { //.... std::shared_ptr ptr2(p); //... } //当ptr2离开其作用域,释放ptr2对象,p所指向的资源也被delete,可以参考上面的hare_ptr类定义。 //.. //此时ptr1对象所管理的资源已经被释放了。 std::cout<<*ptr1<;>2、不delete get()返回的指针。get()即返回智能指针对象中保存的指针,这个应该很容易理解,delete了get()返回的指针,那么相当于释放了智能指针的资源。代码如下:
{ std::shared_ptr ptr(new int(10)); //... int *p =ptr.get(); //.. std::cout<<*p<;>;>3、如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。这个道理跟第2条类似,这两条都是普通指针跟智能指针公用资源,那么无论谁释放了内存,另外一个都不能再使用该资源,其行为是未定义的。
int *p=nullptr; { std::shared_ptr ptr(new int(0)); //ptr do something.... p = ptr.get(); //.... } //当ptr离开作用域,其引用次数减为0,因此释放其所管理资源 std::cout<<*p<;>
4、不使用get()初始化或reset()另一个智能指针。这个道理也是跟上面类似,reset()作用大概是释放调用者所管理的资源,如果有参数,那么该调用者转去管理新的资源(参数)。
std::shared_ptr ptr(new int(0)); { //使用get()去初始化另一个智能指针。那么当ptranother离开其作用域, //他将会释放ptr管理的资源(引用计数为0), std::shared_ptr ptranother(ptr.get()); std::cout<<*ptr< ptrthird; ptrthird.reset(ptr.get()); }5、如果你使用的智能指针管理的资源不是new管理的内存,记住传递它一个删除器。
c++类动应以了析构函数,但是一些为了c和c++两种语言而设计的类。通常都没有定义析构函数。很容易发生内存泄漏。
struct destination; // 表示我们正在连接什么 struct connection; // 打开连接所需的信息 connection connect(destination*); // 打开连接 void disconnect(connection); // 关闭给定的连接 void f(destination &d /* other parameters */) { // 获得一个连接,使用完记得关闭它。 connection c = connect(&d); //.....使用连接 //如果再离开f前忘记调用disconnect,就无法关闭c了。 }为了避免这种问题,可以使用std::shared_ptr,但是需要传递一个删除器给他。
#include #include #include struct connection { std::string ip; int port; connection(std::string ip_, int port_) : ip(ip_), port(port_) {} }; struct destination { std::string ip; int port; destination(std::string ip_, int port_) : ip(ip_), port(port_) {} }; connection connect(destination* pdest) { std::shared_ptr pconn(new connection(pdest->ip, pdest->port)); std::cout << "creating connection(" << pconn.use_count() << ")" << std::endl; return *pconn; } void disconnect(connection pconn) { std::cout << "connection close(" << pconn.ip << ":" << pconn.port << ")" << std::endl; } void end_connection(connection* pconn) { disconnect(*pconn); } void f(destination& d) { connection conn = connect(&d); std::shared_ptr p(&conn, end_connection); //p管理&conn的资源,当其引用计数为0,调用end_connection。 在这里就相当于离开函数f,释放conn的资源。 std::cout << "connecting now(" << p.use_count() << ")" << std::endl; } int main() { destination dest("202.118.176.67", 3316); f(dest); }小结:智能指针跟普通指针混合使用应当特别注意,防止引用不存在的资源。另外不具备析构函数的类,使用智能指针的时候应该提供一个删除器。
;>
上一篇: c++中的重载,重写,重定义
下一篇: 多路分支----switch语句