天天看點

淺談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<<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++