c++ RAII

在寫C++設計模式——單例模式的時候,在寫到實例銷燬時,設計的GC類是很巧妙的,而這一巧妙的設計就是根據當對象的生命週期結束時會自動調用其析構函數的,而這一巧妙的設計也是有專業的名詞的——RAII。那以下將圍繞RAII,全面的講解RAII的相關知識。

什麼是RAII?

RAII是Resource Acquisition Is Initialization的簡稱,是C++語言的一種管理資源、避免泄漏的慣用法。利用的就是C++構造的對象最終會被銷燬的原則。RAII的做法是使用一個對象,在其構造時獲取對應的資源,在對象生命期內控制對資源的訪問,使之始終保持有效,最後在對象析構的時候,釋放構造時獲取的資源。

爲什麼要使用RAII?

上面說到RAII是用來管理資源、避免資源泄漏的方法。那麼,用了這麼久了,也寫了這麼多程序了,口頭上經常會說資源,那麼資源是如何定義的?在計算機系統中,資源是數量有限且對系統正常運行具有一定作用的元素。比如:網絡套接字、互斥鎖、文件句柄和內存等等,它們屬於系統資源。由於系統的資源是有限的,就好比自然界的石油,鐵礦一樣,不是取之不盡,用之不竭的,所以,我們在編程使用系統資源時,都必須遵循一個步驟:

1.申請資源;
2.使用資源;
3.釋放資源。

第一步和第二步缺一不可,因爲資源必須要申請才能使用的,使用完成以後,必須要釋放,如果不釋放的話,就會造成資源泄漏。

一個最簡單的例子:


#include <iostream> 

 

using namespace std; 

 

int main() 

 

{ 

    int *testArray = new int [10]; 

    // Here, you can use the array 

    delete [] testArray; 

    testArray = NULL ; 

    return 0; 

}

我們使用new開闢的內存資源,如果我們不進行釋放的話,就會造成內存泄漏。所以,在編程的時候,new和delete操作總是匹配操作的。如果總是申請資源而不釋放資源,最終會導致資源全部被佔用而沒有資源可用的場景。但是,在實際的編程中,我們總是會各種不小心的就把釋放操作忘了,就是編程的老手,在幾千行代碼,幾萬行中代碼中,也會犯這種低級的錯誤。

再來一個例子:


#include <iostream> 

using namespace std; 

 

bool OperationA(); 

bool OperationB(); 

 

int main() 

{ 

    int *testArray = new int [10]; 

 

    // Here, you can use the array 

    if (!OperationA()) 

    { 

        // If the operation A failed, we should delete the memory 

        delete [] testArray; 

        testArray = NULL ; 

        return 0; 

    } 

 

    if (!OperationB()) 

    { 

        // If the operation A failed, we should delete the memory 

        delete [] testArray; 

        testArray = NULL ; 

        return 0; 

    } 

 

    // All the operation succeed, delete the memory 

    delete [] testArray; 

    testArray = NULL ; 

    return 0; 

} 

 

bool OperationA() 

 

{ 

    // Do some operation, if the operate succeed, then return true, else return false 

    return false ; 

} 

 

bool OperationB() 

 

{ 

    // Do some operation, if the operate succeed, then return true, else return false 

    return true ; 

}

上述這個例子的模型,在實際中是經常使用的,我們不能期待每個操作都是成功返回的,所以,每一個操作,我們需要做出判斷,上述例子中,當操作失敗時,然後,釋放內存,返回程序。上述的代碼,極度臃腫,效率下降,更可怕的是,程序的可理解性和可維護性明顯降低了,當操作增多時,處理資源釋放的代碼就會越來越多,越來越亂。如果某一個操作發生了異常而導致釋放資源的語句沒有被調用,怎麼辦?這個時候,RAII機制就可以派上用場了。

如何使用RAII?

當我們在一個函數內部使用局部變量,當退出了這個局部變量的作用域時,這個變量也就別銷燬了;當這個變量是類對象時,這個時候,就會自動調用這個類的析構函數,而這一切都是自動發生的,不要程序員顯示的去調用完成。這個也太好了,RAII就是這樣去完成的。由於系統的資源不具有自動釋放的功能,而C++中的類具有自動調用析構函數的功能。如果把資源用類進行封裝起來,對資源操作都封裝在類的內部,在析構函數中進行釋放資源。當定義的局部變量的生命結束時,它的析構函數就會自動的被調用,如此,就不用程序員顯示的去調用釋放資源的操作了。現在,我們就用RAII機制來完成上面的例子。代碼如下:


#include <iostream> 

using namespace std; 

 

class ArrayOperation 

{ 

public : 

    ArrayOperation() 

    { 

        m_Array = new int [10]; 

    } 

 

    void InitArray() 

    { 

        for (int i = 0; i < 10; ++i) 

        { 

            *(m_Array + i) = i; 

        } 

    } 

 

    void ShowArray() 

    { 

        for (int i = 0; i <10; ++i) 

        { 

            cout<<m_Array[i]<<endl; 

        } 

    } 

 

    ~ArrayOperation() 

    { 

        cout<< "~ArrayOperation is called" <<endl; 

        if (m_Array != NULL ) 

        { 

            delete[] m_Array;  // 非常感謝益可達非常犀利的review,詳細可以參加益可達在本文的評論 2014.04.13

            m_Array = NULL ; 

        } 

    } 

 

private : 

    int *m_Array; 

}; 

 

bool OperationA(); 

bool OperationB(); 

 

int main() 

{ 

    ArrayOperation arrayOp; 

    arrayOp.InitArray(); 

    arrayOp.ShowArray(); 

    return 0;

}

上面這個例子沒有多大的實際意義,只是爲了說明RAII的機制問題。下面說一個具有實際意義的例子:

/*

** FileName     : RAII

** Author       : Jelly Young

** Date         : 2013/11/24

** Description  : More information, please go to http://www.jb51.net

*/

 

#include <iostream>

#include <windows.h>

#include <process.h>

 

using namespace std;

 

CRITICAL_SECTION cs;

int gGlobal = 0;

 

class MyLock

{

public:

    MyLock()

    {

        EnterCriticalSection(&cs);

    }

 

    ~MyLock()

    {

        LeaveCriticalSection(&cs);

    }

 

private:

    MyLock( const MyLock &);

    MyLock operator =(const MyLock &);

};

 

void DoComplex(MyLock &lock ) // 非常感謝益可達犀利的review 2014.04.13

{

}

 

unsigned int __stdcall ThreadFun(PVOID pv) 

{

    MyLock lock;

    int *para = (int *) pv;

 

    // I need the lock to do some complex thing

    DoComplex(lock);

 

    for (int i = 0; i < 10; ++i)

    {

        ++gGlobal;

        cout<< "Thread " <<*para<<endl;

        cout<<gGlobal<<endl;

    }

    return 0;

}

 

int main()

{

    InitializeCriticalSection(&cs);

 

    int thread1, thread2;

    thread1 = 1;

    thread2 = 2;

 

    HANDLE handle[2];

    handle[0] = ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void *)&thread1, 0, NULL );

    handle[1] = ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void *)&thread2, 0, NULL );

    WaitForMultipleObjects(2, handle, TRUE , INFINITE );

    return 0;

}

這個例子可以說是實際項目的一個模型,當多個進程訪問臨界變量時,爲了不出現錯誤的情況,需要對臨界變量進行加鎖;上面的例子就是使用的Windows的臨界區域實現的加鎖。但是,在使用CRITICAL_SECTION時,EnterCriticalSection和LeaveCriticalSection必須成對使用,很多時候,經常會忘了調用LeaveCriticalSection,此時就會發生死鎖的現象。當我將對CRITICAL_SECTION的訪問封裝到MyLock類中時,之後,我只需要定義一個MyLock變量,而不必手動的去顯示調用LeaveCriticalSection函數。

上述的兩個例子都是RAII機制的應用,理解了上面的例子,就應該能理解了RAII機制的使用了。

使用RAII的陷阱

在使用RAII時,有些問題是需要特別注意的。容我慢慢道來。

先舉個例子


#include <iostream>

#include <windows.h>

#include <process.h>

 

using namespace std;

 

CRITICAL_SECTION cs;

int gGlobal = 0;

 

class MyLock

{

public:

    MyLock()

    {

        EnterCriticalSection(&cs);

    }

 

    ~MyLock()

    {

        LeaveCriticalSection(&cs);

    }

 

private:

    //MyLock(const MyLock &);

    MyLock operator =(const MyLock &);

};

 

void DoComplex(MyLock lock)

{

}

 

unsigned int __stdcall ThreadFun(PVOID pv)  

{

    MyLock lock;

    int *para = (int *) pv;

 

    // I need the lock to do some complex thing

    DoComplex(lock);

 

    for (int i = 0; i < 10; ++i)

    {

        ++gGlobal;

        cout<< "Thread " <<*para<<endl;

        cout<<gGlobal<<endl;

    }

    return 0;

}

 

int main()

{

    InitializeCriticalSection(&cs);

 

    int thread1, thread2;

    thread1 = 1;

    thread2 = 2;

 

    HANDLE handle[2];

    handle[0] = ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void*)&thread1, 0, NULL );

    handle[1] = ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void*)&thread2, 0, NULL );

    WaitForMultipleObjects(2, handle, TRUE , INFINITE );

    return 0;

}

這個例子是在上個例子上的基礎上進行修改的。添加了一個DoComplex函數,在線程中調用該函數,該函數很普通,但是,該函數的參數就是我們封裝的類。你運行該代碼,就會發現,加入了該函數,對gGlobal全局變量的訪問整個就亂了。你有麼有想過,這是爲什麼呢?網上很多講RAII的文章,都只是說了這個問題,但是沒有說爲什麼,在這裏,我好好的分析一下這裏。

由於DoComplex函數的參數使用的傳值,此時就會發生值的複製,會調用類的複製構造函數,生成一個臨時的對象,由於MyLock沒有實現複製構造函數,所以就是使用的默認複製構造函數,然後在DoComplex中使用這個臨時變量。當調用完成以後,這個臨時變量的析構函數就會被調用,由於在析構函數中調用了LeaveCriticalSection,導致了提前離開了CRITICAL_SECTION,從而造成對gGlobal變量訪問衝突問題,如果在MyLock類中添加以下代碼,程序就又能正確運行


MyLock( const MyLock & temp ) 

{ 

    EnterCriticalSection(&cs); 

}

這是因爲CRITICAL_SECTION 允許多次EnterCriticalSection,但是,LeaveCriticalSection必須和EnterCriticalSection匹配才能不出現死鎖的現象。

爲了避免掉進了這個陷阱,同時考慮到封裝的是資源,由於資源很多時候是不具備拷貝語義的,所以,在實際實現過程中,MyLock類應該如下:


class MyLock

{

public:

    MyLock()

    {

        EnterCriticalSection(&cs);

    }

 

    ~MyLock()

    {

        LeaveCriticalSection(&cs);

    }

 

private:

    MyLock(const MyLock &);

    MyLock operator =(const MyLock &);

};


這樣就防止了背後的資源複製過程,讓資源的一切操作都在自己的控制當中。如果要知道複製構造函數和賦值操作符的調用,可以好好的閱讀一下《深度探索C++對象模型這本書》。

總結

說了這麼多了,RAII的本質內容是用對象代表資源,把管理資源的任務轉化爲管理對象的任務,將資源的獲取和釋放與對象的構造和析構對應起來,從而確保在對象的生存期內資源始終有效,對象銷燬時資源一定會被釋放。說白了,就是擁有了對象,就擁有了資源,對象在,資源則在。所以,RAII機制是進行資源管理的有力武器,C++程序員依靠RAII寫出的代碼不僅簡潔優雅,而且做到了異常安全。在以後的編程實際中,可以使用RAII機制,讓自己的代碼更漂亮。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章