程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> [C++]資源管理

[C++]資源管理

編輯:關於C++

資源管理

所謂資源就是,一旦使用了它,將來必須歸還給系統!C++最常見的資源就是動態分配內存,如果不歸還就會內存洩露。

1. 以對象管理資源

我們通常希望有一個對象來幫助我們解決資源管理的問題(自動調用析構函數),於是此章我們討論auto_ptr和shared_ptr。

問題產生

假設我們希望使用一個工廠方法如:

class investment {...};  // 代表一個root class
investment* creatinvestment() {  //  返回一個指針指向繼承體系內動態分配的對象。
    ...
}
void f() {
    investment* pInv = createinvestment();
    ...
    delete pInv;
}

乍看起來這個函數並沒有什麼問題,正常的動態分配了對象,同時也刪除了該對象。但問題在於…中可能出現異常情況,例如,提前的return,異常拋出,某個循環的continue等等,使得控制流無法讀到delete語句,如此便出現了內存洩露。所以“單純地以來f總是會執行其delete語句”是行不通的。

解決問題

為了解決這個問題,我們希望能把資源放入對象中,使得對象在離開其作用域時自動調用析構函數。於是,我們使用了auto_ptr。

void f() {
    std::auto_ptr pInv(createinvestment());
    ...
}
//  經由auto_ptr的析構函數自動刪除pInv。

這個簡單的例子示范了“以對象管理資源”的兩個關鍵想法:
* 獲得資源後立刻放進管理對象內。此觀念常被稱為“資源取得時機便是初始化時機”(RAII准則),因為我們幾乎總是在取得一筆資源後同一語句內以它初始化某個對象。
* 管理對象運用析構函數確保資源被釋放。不管控制流如何離開作用域,一旦對象被銷毀,其析構函數自然會被自動調用,於是資源被釋放。

然而,使用auto_ptr也需要注意,一定不能讓多個auto_ptr指向同一個對象!因為每一個auto_ptr都會使用析構函數,他並不像智能指針一樣會管理有多少指針指向同一對象。並且與copy相關的操作都會使原來的指針指向null,如此使對象只有一個auto_ptr指向。

基於auto_ptr的特性,我們便不能子啊STL容器中使用auto_ptr。auto_ptr

void f() {
    std::shared_ptr pInv(createinvestment());
    ...
}
//  經由shared_ptr的析構函數自動刪除pInv。

幾乎和auto_ptr一樣,但是相應的復制行為會正常了許多。

補充auto_ptr相關知識

C++的auto_ptr所做的事情,就是動態分配對象以及當對象不再需要時自動執行清理。
使用std::auto_ptr,要#include 。
以下是源碼:

template
class auto_ptr
{
private:
    T*ap;
public:
    //constructor & destructor-----------------------------------(1)
    explicit auto_ptr(T*ptr=0)throw():ap(ptr)
    {
    }

    ~auto_ptr()throw()
    {
        delete ap;
    }
    //Copy & assignment--------------------------------------------(2)
    auto_ptr(auto_ptr& rhs)throw():ap(rhs.release())
    {
    }
    template
    auto_ptr(auto_ptr&rhs)throw():ap(rhs.release())
    {
    }
    auto_ptr& operator=(auto_ptr&rhs)throw()
    {
        reset(rhs.release());
        return *this;
    }
    template
    auto_ptr& operator=(auto_ptr&rhs)throw()
    {
        reset(rhs.release());
        return *this;
    }
    //Dereference----------------------------------------------------(3)
    T& operator*()const throw()
    {
        return *ap;
    }
    T* operator->()const throw()
    {
        return ap;
    }
    //Helper functions------------------------------------------------(4)
    //value access
    T* get()const throw()
    {
        return ap;
    }
    //release owner ship
    T* release()throw()
    {
        T* tmp(ap);
        ap = 0;
        return tmp;
    }
    //reset value
    void reset(T* ptr = 0)throw()
    {
        if(ap != ptr)
        {
            delete ap;
            ap = ptr;
        }
    }
    //Special conversions-----------------------------------------------(5)
    template
    struct auto_ptr_ref
    {
        Y*yp;
        auto_ptr_ref(Y*rhs):yp(rhs){}
    };
    auto_ptr(auto_ptr_refrhs)throw():ap(rhs.yp)
    {
    }

    auto_ptr& operator=(auto_ptr_refrhs)throw()
    {
        reset(rhs.yp);
        return*this;
    }

    template
    operator auto_ptr_ref()throw()
    {
        return auto_ptr_ref(release());
    }

    template
    operator auto_ptr()throw()
    {
        return auto_ptr(release());
    }
};

shared_ptr

詳情見:shared_ptr

在資源管理類中小心copying行為

前面我們提到了RAII觀念,並以此作為“資源管理類”的記住,也描述了auto_ptr和shared_ptr如何將這個觀念表現在heap-based資源上。然而並非所有資源都是heap-based,對那種資源而言,我們就需要建立自己的資源管理類。(棧,由編譯器自動管理,無需程序員手工控制;堆:產生和釋放由程序員控制。)

問題產生

我們處理類型為Mutex的互斥器對象,同時使用一個class來管理它。

void lock(Mutex* pm);
void unlock(Mutex* pm);

class Lock {
public:
    explicit Lock(Mutex* pm) : mutexPtr(pm) {
        lock(mutexPtr);
    }
    ~Lock() { unlock(mutexPtr); }
private:
    Mutex *mutexPtr;
};

main () {
    Mutex m;
    ...
    {
    Lock m1(&m);
    ...
    Lock m11(&m);
    Lock m12(m11);  // 將m11復制到m12身上,會發生事?
    }
}

當一個RAII對被復制,會發生什麼事?以下是你的兩種選擇:
* 靜止復制。許多時候允許RAII對象被復制並不合理。對一個像Lock這樣的class卻是可能的,因為很少能夠合理擁有“同步化期初器物”的復件。如果復制對RAII class並不合理,就應該讓該class繼承uncopyable(見前文:)。
* 對底層資源使用“引用計數法”。有時候,我們希望保有資源,直到它的最後一個使用者被銷毀,這種情況下復制RAII對象,該資源的引用計數遞增。

問題解決

通常只要內含shared_ptr成員變量,就可以實現reference-counting copying行為。並且shared_ptr允許指定所謂的“刪除器”(deleter)(一個函數或函數對象),當引用次數為0時便被調用,而不是執行析構函數。刪除器對shared_ptr構造函數來說是可有可無的第二參數。

class Lock {
public:
    explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) {
        lock(mutexPtr.get()); // 隨後會討論get
    }
private:
    shared_ptr mutexPtr;
}

此處不再需要聲明析構函數,因為沒有必要。因為class析構函數會自動調用其non-static成員變量的析構函數,也就是此處的刪除器函數。

資源復制的原則

復制底部資源。有時候,只要你喜歡,可以針對一份資源擁有其任意數量的復制(副本)。而你需要“資源管理類”的唯一理由是,當你不再需要某個復件時確保它被釋放。在此情況下復制資源管理對象,應該同時也復制其所包括的資源,也就是進行“深拷貝”。 轉移底部資源的擁有權。某些罕見場合下,你可以希望確保永遠只有一個RAII對象指向一個未加工資源,即使是RAII對象被復制依然如此,此時,資源的擁有權會從被復制物轉移到目標物。就像auto_ptr的復制意義。

3. 在資源管理類中提供對原始資源的訪問

API往往要求訪問原始資源,所以每一個RAII class應該提供一個“取得其所管理之資源”的方法。

問題產生

在#1中我們提到了以下factory函數

investment* pInv = createinvestment();

假設我們希望以某個函數處理investment對象:

int daysHeld(const investment* pi); //  返回投資天數

int days = daysHeld(pInv);  
// error!!! 類型不匹配!函數需要investment指針,而你傳給它類型為shared_ptr的對象。

問題解決

這時候我們需要一個函數可將RAII class對象轉換為其所內含之原始資源。以下提供兩種方法:

顯示轉換

shared_ptr和auto_ptr都提供了一個get函數用來執行顯示轉換。

int days = daysHeld(pInv.get());  

同樣的,就像幾乎所有智能指針一樣,shared_ptr和auto_ptr都重載了指針取值的操作符operator -> and operator*。

隱式轉換

提供一種重載操作符,完成隱式轉換。類似於以下代碼:

class type {
public:
    type(int i) : a(i) {
    }
    operator int() {
        return a;
    }
private:
    int a;
};
int main(int argc, const char * argv[]) {
    type a(10);
    std::cout << a << std::endl;
    return 0;
}

總結

是否應該提供一個顯式轉換函數將RAII class轉換為其底部資源,或者應該提供隱式轉換,取決於RAII class被設計執行的特定工作,以及它被使用的情況。最佳的設計很可能是堅持“讓接口容易被正確使用,不易被誤用”。通常顯式轉換函數如get是比較好的,因為它將“非故意之類型轉換”的可能性最小化了。

4. 成對使用new和delete時要采取相同形式

當你使用new時,有兩件事情發生:1)內存被分配出來。2)針對此內存會有一個或多個構造函數被調用。同樣的,當你使用delete時,也有兩件事情發生:1)針對此內存會有一個或多個析構函數被調用。2)內存被釋放。但問題出來了,即將被刪除的內存之內究竟有多少個對象?這決定了有多少個析構函數被調用。

因為單一對象的內存分配和數組的內存分配是不一樣的,所以每當我們使用delete的時候要告訴編譯器,我們使用的是單一對象還是數組。方法就是加上[]!

此規則對於喜歡使用typedef的人十分重要,這意味著作者必須說清楚typedef是什麼,要用什麼類型的delete。

typedef std::string AddressLines[4];

std::string* pal = new AddressLines;

delete [] pal; // that is right! But user may misunderstand.

為了避免諸如此類的錯誤,最好不要對數組形式采用typedef動作,而是使用vector,string等template來替換數組。

5. 以獨立語句將new對象直接放入智能指針

我們無法知道編譯器會按照什麼樣的順序來執行一條代碼語句,而這其中可能隱含著某些異常。

問題產生

假設我們有個函數來揭示處理程序的優先權,另一個函數用來在某動態分配所得的Widget上進行某些帶有優先權的處理。

// function definition:
int priority();
void processWidget(std::shared_prt pw, int priority);

// function reference:
processWidget(std::shared_ptr

問題發生得很隱蔽!

我們本以為把一個對象放入”對象資源管理器“就可以有效的解決資源洩露的問題,但其實在這裡隱含危機。問題出在編譯器運行順序上,如果編譯器按照以下順序:
1. 執行new Widget
2. 調用priority
3. 調用shared_ptr構造函數
而此時,priority的調用導致異常,那麼new Widget返回的指針就會遺失,導致內存洩露。(幸福來得太突然)

問題解決

此問題的解決非常的簡單。既然編譯器有機會因為代碼運行順序不同而出錯,那麼我們就強行讓他按照我們預想的順序執行就可以了~

std::shared_ptr pw(new Widget);
processWidget(pw, priority());

因為編譯器對於“跨越語句的各項操作”沒有重新排列的自由!所以此代碼不會出現問題!

所以,盡可能單獨地把new對象直接放入智能指針中總是有道理的。

6. 區別堆和棧

管理方式不同
棧,由編譯器自動管理,無需程序員手工控制;堆:產生和釋放由程序員控制。 空間大小不同
棧的空間有限;堆內存可以達到4G。 能否產生碎片不同
棧不會產生碎片,因為棧是種先進後出的隊列。堆則容易產生碎片,多次的new/delete
會造成內存的不連續,從而造成大量的碎片。 生長方向不同
堆的生長方式是向上的,棧是向下的。 分配方式不同
堆是動態分配的。棧可以是靜態分配和動態分配兩種,但是棧的動態分配由編譯器釋放。 分配效率不同
棧是機器系統提供的數據結構,計算機底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令。堆則是由C/C++函數庫提供,庫函數會按照一定的算法在堆內存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於內存碎片太多),就有可能調用系統功能去增加程序數據段的內存空間,這樣就有機會分到足夠大小的內存,然後進行返回。顯然,堆的效率比棧要低得多。

堆和棧相比,由於大量new/delete的使用,容易造成大量的內存碎片;由於沒有專門的系統支持,效率很低;由於可能引發用戶態和核心態的切換,內存的申請,代價變得更加昂貴。所以棧在程序中是應用最廣泛的,就算是函數的調用也利用棧去完成,函數調用過程中的參數,返回地址,EBP和局部變量都采用棧的方式存放。所以,我們推薦大家 盡量用棧,而不是用堆。

棧和堆相比不是那麼靈活,有時候分配大量的內存空間,還是用堆好一些。

無論是堆還是棧,都要防止越界現象的發生。

總結

對於C++這種技巧較高的語言來說,資源管理從來都不會是一個簡單的事情,所以掌握一定的資源管理方法總是有用的!此博客根據《effective c++》總結了資源管理的幾條准則,附加少量自己的補充。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved