C++的RAII


1、什么是RAII

  RAII(Resource Acquisition Is Initialization),也稱為“資源獲取就是初始化”,是C++語言的一種管理資源、避免泄漏的慣用法。C++標准保證任何情況下,已構造的對象最終會銷毀,即它的析構函數最終會被調用。簡單的說,RAII 的做法是使用一個對象,在其構造時獲取資源,在對象生命期控制對資源的訪問使之始終保持有效,最后在對象析構的時候釋放資源。

  RAII是C++的發明者Bjarne Stroustrup提出的概念,“資源獲取即初始化”也就是說在構造函數中申請分配資源,在析構函數中釋放資源。因為C++的語言機制保證了,當一個對象創建的時候,自動調用構造函數,當對象超出作用域的時候會自動調用析構函數。所以,在RAII的指導下,我們應該使用類來管理資源,將資源和對象的生命周期綁定。

  RAII技術被認為是C++中管理資源的最佳方法,進一步引申,使用RAII技術也可以實現安全、簡潔的狀態管理,編寫出優雅的異常安全的代碼。它利用棧對象在離開作用域后自動析構的語言特點,將受限資源的生命周期綁定到該對象上,當對象析構時以達到自動釋放資源的目的。

  簡單而言RAII就是指資源在我們拿到時就已經初始化,一旦不在需要該資源就可以自動釋放該資源。

  對於C++來說,資源在構造函數中初始化(可以再構造函數中調用單獨的初始化函數),在析構函數中釋放或清理。常見的情形就是在函數調用中創建C++對象時分配資源,在C++對象出了作用域時將其自動清理和釋放(不管這個對象是如何出作用域的,不管是否因為某個中間步驟不滿足條件而導致提前返回,也不管是否正常走完全部流程后返回)。

2、資源管理問題

  什么是資源呢?在計算機系統中,資源是數量有限且對系統正常運行具有一定作用的元素。比如堆上分配的內存、文件句柄、線程、數據庫連接、網絡連接、網絡套接字、互斥鎖和內存等等,它們屬於系統資源。由於系統的資源是有限的,就好比自然界的石油,鐵礦一樣,不是取之不盡,用之不竭的,所以,我們在編程使用系統資源時,都必須遵循一個步驟:獲取資源--->使用資源--->釋放資源

  操作系統的資源是有限的,當我們向操作系統索取資源,使用完后應即時歸還給操作操作,這是一個良好的編程習慣,資源獲取操作流程如下圖:

  內存只是資源的一種,在這里我們討論一下更加廣義的資源管理。比如說文件的打開與關閉、windows中句柄的獲取與釋放等等。按照常規的RAII技術需要寫一堆管理它們的類,有的時候顯得比較麻煩。但是如果手動釋放,通常還要考慮各種異常處理,比如說:

  每當處理需要配對的獲取/釋放函數調用的資源時,都應該將資源封裝在一個對象中,實現自動資源釋放。

  智能指針(std::shared_ptr和std::unique_ptr)即RAII最具代表的實現,使用智能指針,可以實現自動的內存管理,再也不需要擔心忘記delete造成的內存泄漏。毫不誇張的來講,有了智能指針,代碼中幾乎不需要再出現delete了。

 

#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 分配的內存delete掉,導致極度臃腫,效率下降,更可怕的是,程序的可理解性和可維護性明顯降低了,當操作增多時,處理資源釋放的代碼就會越來越多,越來越亂。如果某一個操作發生了異常而導致釋放資源的語句沒有被調用,怎么辦?這個時候,RAII機制就可以派上用場了。

  再來一個例子:

#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機制就可以派上用場了。

  (1)如何使用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;
            m_Array = NULL ; 
        } 
    } 
 
private : 
    int *m_Array; 
}; 
 
bool OperationA(); 
bool OperationB(); 
 
int main() 
{ 
    ArrayOperation arrayOp; 
    arrayOp.InitArray(); 
    arrayOp.ShowArray(); 
    return 0;
}

       上面這個例子沒有多大的實際意義,只是為了說明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;
 
    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機制的使用了。

  (2)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++對象模型這本書》。

3、總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM