欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-08-26 08:07:18
浅谈c++普通指针和智能指针管理动态内存的陷阱 前言: c++中动态内存的管理主要是使用new/delete表达式和std::allcator类。为了管理动态内存更加安全,c++11新标准库推出了智...

浅谈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);
}

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

;>
;>
;>;>
;>
;>
;>