入门客AI创业平台(我带你入门,你带我飞行)
博文笔记

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

创建时间:2016-11-04 投稿人: 浏览次数:3315

浅谈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++




声明:该文观点仅代表作者本人,入门客AI创业平台信息发布平台仅提供信息存储空间服务,如有疑问请联系rumenke@qq.com。