浅谈C++普通指针和智能指针管理动态内存的陷阱

来源:互联网 发布:安卓版看图软件 编辑:程序博客网 时间:2024/06/03 15:47

浅谈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<<std::endl; //*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<<std::endl;         //....    }
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背后在做什么:http://blog.csdn.net/hazir/article/details/21413833

标准库std::allocator类帮助我们将内存分配和对象初始化分离开来,也允许我们将对象的销毁跟对象内存释放分开来。std::allocator分配的内存是原始的、未构造的。这里提供一个实例感受一下这个流程。然后注意事项跟new/delete类似。std::allocator在memory头文件中。

{    std::allocator<std::string> allocate_str;  //定义一个可以分配内存的string的allocator对象allocate_str    std::string *p = allocate_str.allocate(1); //分配一个未初始化的string,p指向一块大小为string的原始内存    //std::cout<<*p<<std::endl;  eg:这种行为是未定义的    allocate_str.construct(p,"hello world");  //初始化p,*p="hello world";    std::cout<<*p<<std::endl;  //打印出hello world    allocate_str.destroy(p);// 销毁p构造的对象。对应的是调用p的析构函数,                           //这时候指向一块原始内存,其值是未定义的。    allocate_str.deallocate(p,1); //将指向的原始内存归还给系统,也就是释放p的内存}

三、智能指针(smart pointer)

      为了更加安全的管理动态内存,C++11新标准库推出了智能指针。主要是std::shared_ptr 、 std::unique_ptr 、std::weak_ptr(作为一个伴随类)。他们都位于memory后文件中。

     智能指针的行为类似普通指针,一个重要区别是他负责自动释放所指向对象的内存。智能指针可以提供对动态分配的内存安全而又方便的管理,但这是建立在正确使用的前提下,为了正确使用智能指针,我们必须坚持一些基本规范。

在管理new分配出来的资源,shared_ptr类大概可以这样理解:(省略很多,最明显没有一个计数器,但有助加深对智能指针理解,我是这么认为。)

template<class T>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<<std::endl;    std::shared_ptr<int> ptr1(p);    //...    {        //....        std::shared_ptr<int> ptr2(p);        //...    }  //当ptr2离开其作用域,释放ptr2对象,p所指向的资源也被delete,可以参考上面的hare_ptr类定义。    //..    //此时ptr1对象所管理的资源已经被释放了。    std::cout<<*ptr1<<std::endl;  //这种行为是未定义的}

2、不delete get()返回的指针。get()即返回智能指针对象中保存的指针,这个应该很容易理解,delete了get()返回的指针,那么相当于释放了智能指针的资源。代码如下:

{    std::shared_ptr<int> ptr(new int(10));    //...    int *p =ptr.get();    //..    std::cout<<*p<<std::endl;  //可以访问    //...    delete p;    //此时ptr对象所管理的资源是被释放了。    std::cout<<*ptr<<std::endl;  //这个值是未定义的}

3、如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。这个道理跟第2条类似,这两条都是普通指针跟智能指针公用资源,那么无论谁释放了内存,另外一个都不能再使用该资源,其行为是未定义的。

int *p=nullptr;{   std::shared_ptr<int> ptr(new int(0));   //ptr do something....    p = ptr.get();    //....} //当ptr离开作用域,其引用次数减为0,因此释放其所管理资源std::cout<<*p<<std::endl;  //此时p的值是未定义的

4、不使用get()初始化或reset()另一个智能指针。这个道理也是跟上面类似,reset()作用大概是释放调用者所管理的资源,如果有参数,那么该调用者转去管理新的资源(参数)。

std::shared_ptr<int> ptr(new int(0));{    //使用get()去初始化另一个智能指针。那么当ptrAnother离开其作用域,    //他将会释放ptr管理的资源(引用计数为0),    std::shared_ptr<int> ptrAnother(ptr.get());    std::cout<<*ptr<<std::endl;    //其分析跟上面一样,    std::shared_ptr<int> 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 <iostream>#include <string>#include <memory>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<connection> 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<connection> 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);}

小结:智能指针跟普通指针混合使用应当特别注意,防止引用不存在的资源。另外不具备析构函数的类,使用智能指针的时候应该提供一个删除器。


原文:http://blog.csdn.net/qq_33850438/article/details/52994314

参考:

C++ Primer 5th  

Effective C++




1 0