c++內存問題整理與智能指針使用

  公司裏小組組織c++知識的分享會,正好我手上碰到過幾個purify的內存泄露問題,就借這裏總結一下c++的內存問題。
  借鑑陳碩總結的分類,c++大致的內存問題有以下幾個方面:
  1.緩衝區溢出
  在使用自己編寫的緩衝區或者使用不安全的函數時,會遇到類似數組越界的緩衝區溢出問題,Linux內核的解決辦法是棧隨機化,金絲雀的檢測,具體的攻擊手段和例子,可以參考我另一篇的buffer lab實驗。在自己寫程序的時候,最重要的一點是記錄或者限制緩衝區的長度,使用vector<char>這樣的容器,strncpy這樣更安全的函數。
  在purify檢測的時候,使用的就是類似金絲雀的機制,在緩衝區前後插入特殊的數值,如果其數值被修改了,就是溢出。
  2.空懸指針/野指針
  指針在所指的內存空間被釋放後,沒有置爲NULL,指針的生存期還沒有結束,這時候指針所指的區域是個隨機值。並且可以繼續使用!!!
  下面使用代碼進行驗證,環境爲win7,64位,Dev-c++5.11,gcc(c11)。

void testNullPtr(){
    int *p=new int(10);
    delete p;
    cout<<*p<<endl;
    *p=20;
    cout<<*p<<endl;
}

  上面代碼的運行結果:
  這裏寫圖片描述
  可以看出第一次是隨機值,第二次竟然正常更新了。
  在正常工程使用中,指針delete完之後置NULL,以及不要返回局部指針變量!!!如果兩個指針指向同一內存,其中一個置NULL,依然解決不了空懸指針的問題。如下:
  重置p對q沒有任何作用!

    int *p(new int(42));
    auto q=p;
    delete p;
    p=nullptr;

  關於局部變量的示例代碼:
  

    char *itoa(int n){
        char buf[43];
        sprintf(buf,"%d",n);
        return buf;
    }

  將buf聲明爲static即可。

  3.重複釋放
  非常經典的double free問題,運行如下程序:
  

void testDoubleFree(){
    int *p=new int(10);
    delete p;
    delete p;
}

  運行結果會產生運行時錯誤,調試狀態下會有SIGTRAP信號。
  解決辦法也是指針置NULL。拓展一下,指針置NULL之後還可以釋放嗎?答案是可以。這裏需要研究一下new和delete。
  new和delete其實就是對malloc和free的封裝。

  • 對於簡單數據,直接調用operator new分配內存。但是可以使用new_handler來處理new失敗的情況。
  • 另一處不同,malloc失敗返回NULL,而new拋出異常。
  • 對於複雜數據結構,例如對象,先調用operator new分配內存後,在調用其構造函數。
  • delete對於簡單數據,直接調用free。
  • 對於複雜的數據結構,先析構,後delete。

    free的部分代碼如下(glibc):
    函數會對指針進行NULL的檢測,爲空直接返回,因此delete完之後置NULL可以避免重複釋放問題。

    if (ptr == NULL)
        {
          catomic_increment (&calls[idx_free]);
          return;
        }

  4.內存泄露
  這一問題可以通過智能指針解決,下面再細講。
  
  5.不配對的new[]/delete
  new和delete在聲明使用的時候需要配對使用。
  

    int *p=new int(10);
    delete p;
    int *pa=new int[10];
    delete []pa;

  當我們釋放一個指向數組的指針時,它指示編譯器指針指向一個對象數組的第一個元素,元素按逆序銷燬。
  
  承接上面對new和delete的研究,在調用new[]和delete[]的時候。
  

  • 對於簡單數據,new[]計算好大小後調用operator new。
  • 對於複雜數據類型,先分配內存,寫入數組大小,然後調用N次構造。
  • 對於簡單數據類型,delete[]和delete效果一樣。
  • 對於複雜的數據類型,delete[]先析構,後釋放空間。

      注意!!!以下代碼正常運行,編譯器沒有警告。
      

class Obj{
    public:
        Obj(){}
};
void FitNewDelete(){

    Obj *p=new Obj[3];
    delete p;
}

  在《c++primer》中提到,上面的行爲是未定義的。可能會崩潰,可以是行爲異常。比較一勞永逸的辦法是使用vector代替數組
  6.內存碎片
  常用的解決辦法是實現自己的memory pool,這裏不詳細討論了,因爲首先現在的malloc有所優化,第二這個問題有時候影響不大。
  
  上面參照了《linux多線程服務端》的分類,現在講解一下purify的問題,purify的內存問題主要分爲以下幾類。
  purify內存問題
  這裏不詳細解釋了,英文應該很好懂。
  講一下在工作過程中碰到的兩個實例。
  第一個是UMR,未初始化內存讀的問題,很多時候,這個問題並不算問題,不具有準確性。
  
  例如下面的代碼就會有UMR問題,這裏是因爲結構體的字節填充,其中smth的field2會因爲4字節的地址對齊的需要,被填充三個字節。

struct something {
    int field1;
    char field2;
};

/* ... */

struct something smth, smth2;
smth.field1 = 1;
smth.field2 = 'A';

smth2 = smth;

  而我遇到的UMR代碼如下:
  

struct test: public std::binary_function<x, y, bool>
    {
        bool operator() (const x& thisInfo, y otherEntityType) const
        {
            return (thisInfo.entityType == otherEntityType);
        }
    };

  這個函數乍看沒有任何問題,函數的綁定和函數操作而已,如果做類似調用,就會有UMR問題。
  

this->ritTypeInfo =  std::find_if(RITS_begin, RITS_end, bind2nd(test(), entityType));

  問題出在編譯器自己合成的構造函數和拷貝構造上,如果你不希望編譯器合成,或者不清楚合成的代碼效果,請明確的構造出來。這裏的解決辦法就是構造出空的構造函數和拷貝構造函數即可。
  接下來的一個例子也與拷貝構造函數和拷貝賦值運算符有關。
  首先明確一下兩個函數出現的地方,拷貝賦值是同類對象賦值時出現。拷貝構造是在使用=定義變量時、將一個對象作爲實參傳遞給一個非引用類型的形參、以及從一個返回類型爲非引用類型的函數返回一個對象時使用。
  如果自己沒有定義,編譯器會爲我們合成一個。實例代碼如下:
  

    class CheckApp{};
    class check{
        CheckApp *pCheckApp;
        public:
            check(){
                pCheckApp=new CheckApp(); 
            }
            ~check(){
                if(pCheckApp!=NULL){
                    delete pCheckApp;
                    pCheckApp=NULL;
                }
            }
    };

  這裏在使用拷貝賦值和拷貝構造函數時,編譯器會爲我們合成拷貝構造和拷貝賦值函數。類似下面的代碼:
  

    check::check(const check& rhs)
        :pCheckApp(rhs.pCheckApp){}
    check& check::operator=(const check& rhs){
        if(this!=&rhs){
            pCheckApp=rhs.pCheckApp;
        }
        return *this;
    }

  這個代碼有什麼問題?看一下面的圖片就知道了。
  這裏寫圖片描述

  對於指針型的成員變量,合成的拷貝賦值和拷貝構造只是複製了指針本身,而不是指向的對象,這叫做淺拷貝,當其中的s1或者s2析構釋放的時候,它所指向的內存空間就被釋放掉了,另一個指針就變成了野指針,會出現double free。同時也存在其中一個指針更改值造成另一個對象值也更改的現象。
  應急的解決辦法是自己完成拷貝構造和拷貝賦值。代碼如下:
  

    //先完成拷貝構造,下面賦值要用
    check::check(const check& rhs){
         pCheckApp=new CheckApp();
         if(pCheckApp!=NULL){
            *pCheckApp=*(rhs.pCheckApp);
        }
    }
    //進行深拷貝,按值拷貝
    check& check::operator=(const check &rhs){
        //這個判斷是防止自賦值 a=a
        if(this!=&rhs){
        //使用構造局部變量,進行成員交換
        //局部變量出作用域會自動釋放
        //防止new失敗,簡化設計
            check tmp(rhs);
            CheckApp *tmpCheckApp=pCheckApp;
            pCheckApp=tmp.pCheckApp;
            tmp.pCheckApp=tmpCheckApp;
        }
        return *this;
    }

  上面提到應急兩個字,言外之意,應該有更好的解決辦法,相信各位應該也能想到了,智能指針
  簡單來說,智能指針在上圖的S1和S2與內存空間之間加了一個代理層,一個新的對象,讓s1和s2所指的對象永久有效,先命名爲proxy,同時把兩個指針都變成對象,sp1,sp2。proxy有兩個成員,指針和計數器。sp1析構後,計數器減一,計數爲0時,銷燬proxy指針指向的對象。
  空懸指針野指針可以用shared_ptr/weak_ptr解決,對於重複釋放可以選擇unique_ptr與scoped_ptr解決。
  其中shared_ptr、weak_ptr、scoped_ptr爲boost庫模板。
  c11吸收了shared_ptr、weak_ptr並使用具有移動語意的unique_ptr代替scoped_ptr,它們聲明在memory頭文件中。
  這裏簡單介紹一下用法,代碼如下,更多的查看手冊:
  shared_ptr運行多個指針指向同一個對象,這就可以解決上面的淺拷貝問題。  
  注意shared_ptr有一個非常有用的特性,刪除器,可以使析構動作在構造時被捕捉。

    template<class T>
struct endPtr{
    //主要用於非動態分配的對象
    //用於不具有良好的析構函數的對象
    //deleter是個泛型類型,需要operator() 
    void operator()(T* p){
        delete [] p;
        cout<<"now delete"<<endl;   
    } 

};
void testSharePtr(){
    //shared_ptr<int> q(new int(10)); 不建議
    shared_ptr<int> q=make_shared<int> (42);
    //c11 構造 
    auto p=make_shared<int> (40);
    //使用 
    cout<<"p:"<<*p<<" use: "<<p.use_count()<<endl;
    //引用數 
    cout<<"q:"<<q.use_count()<<endl;
    //判斷
    cout<<"is unique?: "<<q.unique()<<endl; 
    p=q;
    cout<<"p:"<<p.use_count()<<endl;
    cout<<"q:"<<q.use_count()<<endl;
    int *tmp=new int[100];
    //定義自己的刪除器
    shared_ptr<int> r(tmp,endPtr<int>());
}

  結果如下:
  這裏寫圖片描述
  
  一個unique_ptr只能指向一個給定對象,不支持普通的拷貝和賦值,但是可以使用函數release或者reset轉移指針所有權,scoped_ptr則不允許,兩者異同:
  A auto_ptr is a pointer with copy and with move semantics and ownership (=auto-delete).
  
  A unique_ptr is a auto_ptr without copy but with move semantics.
  
  A scoped_ptr is a auto_ptr without copy and without move semantics.
  
  auto_ptr‍s are allways a bad choice – that is obvious.

  Whenever you want to explicitely have move semantics, use a unique_ptr.
  
  Whenever you want to explicitely disallow move semantics,use a scoped_ptr.

  最後介紹一下weak_ptr,shared_ptr是強引用,拿鐵絲綁着對象,而weak_ptr是棉線掛着(陳碩的比喻),weak_ptr不控制所指對象的生命週期,對象的釋放和weak_ptr無關,這種弱引用可以拿來打破shared_ptr的循環引用問題,兩個shared_ptr互相引用,會造成對象無法釋放。
  weak_ptr起到一個檢測的作用!!!
  

    auto p=make_shared<int> (42);
    //不改變引用計數
    weak_ptr<int> wp(p);
    //由於對象可能不存在,使用lock函數,如果有的話,返回shared_ptr
    if(shared_ptr<int> np=wp.lock()){
    }

  weak_ptr還可以用於弱回調,把shared_ptr綁定到function裏,會延長對象的生命週期,如果想實現對象活着就調用,否則忽略的效果,可以使用weak_ptr。
  最後對weak_ptr和shared_ptr做一個簡單的分析。
  
  unique_ptr使用元素,指針和刪除器。

      // unique_ptr內部片段
      template <typename _Tp, typename _Dp = default_delete<_Tp> >
    class unique_ptr
    {
      // use SFINAE to determine whether _Del::pointer exists
      class _Pointer
      {
    template<typename _Up>
      static typename _Up::pointer __test(typename _Up::pointer*);

    template<typename _Up>
      static _Tp* __test(...);

    typedef typename remove_reference<_Dp>::type _Del;

      public:
    typedef decltype(__test<_Del>(0)) type;
      };

      typedef std::tuple<typename _Pointer::type, _Dp>  __tuple_type;
      __tuple_type                                      _M_t;

    public:
      typedef typename _Pointer::type   pointer;
      typedef _Tp                       element_type;
      typedef _Dp                       deleter_type;
    };

  shared_ptr在基類的基礎上加上刪除器參數。下面是示意性的摘錄。
  

      template<typename _Tp>
    class shared_ptr : public __shared_ptr<_Tp>
    {
    public:
        //其中一個構造函數
        template<typename _Tp1, typename _Deleter>
    shared_ptr(_Tp1* __p, _Deleter __d)
        : __shared_ptr<_Tp>(__p, __d) { }
    };
     template<typename _Tp, _Lock_policy _Lp>
    class __shared_ptr
    {
    public:
      typedef _Tp   element_type;
    protected:
      friend class __weak_ptr<_Tp, _Lp>;
    private:
       void*
      _M_get_deleter(const std::type_info& __ti) const noexcept
      { return _M_refcount._M_get_deleter(__ti); }
        _Tp*           _M_ptr;         // Contained pointer.
      __shared_count<_Lp>  _M_refcount;    // Reference counter.
    };

  最後給一個玩具型的參考實現,來自c++primer答案參考。
  

/***************************************************************************
*  @file       shared_pointer.hpp
*  @author     Yue Wang
*  @date       04  Feb 2014
*                  Jul 2015
*                  Oct 2015 
*  @remark     This code is for the exercises from C++ Primer 5th Edition
*  @note
***************************************************************************/

#pragma once
#include <functional>
#include "delete.hpp"

namespace cp5
{
    template<typename T>
    class SharedPointer;

    template<typename T>
    auto swap(SharedPointer<T>& lhs, SharedPointer<T>& rhs)
    {
        using std::swap;
        swap(lhs.ptr, rhs.ptr);
        swap(lhs.ref_count, rhs.ref_count);
        swap(lhs.deleter, rhs.deleter);
    }

    template<typename T>
    class SharedPointer
    {
    public:
        //
        //  Default Ctor
        //
        SharedPointer()
            : ptr{ nullptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} }
        { }
        //
        //  Ctor that takes raw pointer
        //
        explicit SharedPointer(T* raw_ptr)
            : ptr{ raw_ptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} }
        { }
        //
        //  Copy Ctor
        //
        SharedPointer(SharedPointer const& other)
            : ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ other.deleter }
        {
            ++*ref_count;
        }
        //
        //  Move Ctor
        //
        SharedPointer(SharedPointer && other) noexcept
            : ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ std::move(other.deleter) }
        {
            other.ptr = nullptr;
            other.ref_count = nullptr;
        }
        //
        //  Copy assignment
        //
        SharedPointer& operator=(SharedPointer const& rhs)
        {
            //increment first to ensure safty for self-assignment
            ++*rhs.ref_count;
            decrement_and_destroy();
            ptr = rhs.ptr, ref_count = rhs.ref_count, deleter = rhs.deleter;
            return *this;
        }
        //
        //  Move assignment
        //
        SharedPointer& operator=(SharedPointer && rhs) noexcept
        {
            cp5::swap(*this, rhs);
            rhs.decrement_and_destroy();
            return *this;
        }
        //
        //  Conversion operator
        //
        operator bool() const
        {
            return ptr ? true : false;
        }
        //
        //  Dereference
        //
        T& operator* () const
        {
            return *ptr;
        }
        //
        //  Arrow
        //
        T* operator->() const
        {
            return &*ptr;
        }
        //
        //  Use count
        //
        auto use_count() const
        {
            return *ref_count;
        }
        //
        //  Get underlying pointer
        //
        auto get() const
        {
            return ptr;
        }
        //
        //  Check if the unique user
        //
        auto unique() const
        {
            return 1 == *refCount;
        }
        //
        //  Swap
        //
        auto swap(SharedPointer& rhs)
        {
            ::swap(*this, rhs);
        }
        //
        // Free the object pointed to, if unique
        //
        auto reset()
        {
            decrement_and_destroy();
        }
        //
        // Reset with the new raw pointer
        //
        auto reset(T* pointer)
        {
            if (ptr != pointer)
            {
                decrement_n_destroy();
                ptr = pointer;
                ref_count = new std::size_t(1);
            }
        }
        //
        //  Reset with raw pointer and deleter
        //
        auto reset(T *pointer, const std::function<void(T*)>& d)
        {
            reset(pointer);
            deleter = d;
        }
        //
        //  Dtor
        //
        ~SharedPointer()
        {
            decrement_and_destroy();
        }
    private:
        T* ptr;
        std::size_t* ref_count;
        std::function<void(T*)> deleter;

        auto decrement_and_destroy()
        {
            if (ptr && 0 == --*ref_count)
                delete ref_count, 
                deleter(ptr);
            else if (!ptr)
                delete ref_count;
            ref_count = nullptr;
            ptr = nullptr;
        }
    };
}//namespace
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章