C++中的RAII機(jī)制詳解
前言
在寫C++設(shè)計(jì)模式——單例模式的時(shí)候,在寫到實(shí)例銷毀時(shí),設(shè)計(jì)的GC類是很巧妙的,而這一巧妙的設(shè)計(jì)就是根據(jù)當(dāng)對(duì)象的生命周期結(jié)束時(shí)會(huì)自動(dòng)調(diào)用其析構(gòu)函數(shù)的,而這一巧妙的設(shè)計(jì)也是有專業(yè)的名詞的——RAII。那以下將圍繞RAII,全面的講解RAII的相關(guān)知識(shí)。
什么是RAII?
RAII是Resource Acquisition Is Initialization的簡(jiǎn)稱,是C++語(yǔ)言的一種管理資源、避免泄漏的慣用法。利用的就是C++構(gòu)造的對(duì)象最終會(huì)被銷毀的原則。RAII的做法是使用一個(gè)對(duì)象,在其構(gòu)造時(shí)獲取對(duì)應(yīng)的資源,在對(duì)象生命期內(nèi)控制對(duì)資源的訪問(wèn),使之始終保持有效,最后在對(duì)象析構(gòu)的時(shí)候,釋放構(gòu)造時(shí)獲取的資源。
為什么要使用RAII?
上面說(shuō)到RAII是用來(lái)管理資源、避免資源泄漏的方法。那么,用了這么久了,也寫了這么多程序了,口頭上經(jīng)常會(huì)說(shuō)資源,那么資源是如何定義的?在計(jì)算機(jī)系統(tǒng)中,資源是數(shù)量有限且對(duì)系統(tǒng)正常運(yùn)行具有一定作用的元素。比如:網(wǎng)絡(luò)套接字、互斥鎖、文件句柄和內(nèi)存等等,它們屬于系統(tǒng)資源。由于系統(tǒng)的資源是有限的,就好比自然界的石油,鐵礦一樣,不是取之不盡,用之不竭的,所以,我們?cè)诰幊淌褂孟到y(tǒng)資源時(shí),都必須遵循一個(gè)步驟:
1.申請(qǐng)資源;
2.使用資源;
3.釋放資源。
第一步和第二步缺一不可,因?yàn)橘Y源必須要申請(qǐng)才能使用的,使用完成以后,必須要釋放,如果不釋放的話,就會(huì)造成資源泄漏。
一個(gè)最簡(jiǎn)單的例子:
#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開(kāi)辟的內(nèi)存資源,如果我們不進(jìn)行釋放的話,就會(huì)造成內(nèi)存泄漏。所以,在編程的時(shí)候,new和delete操作總是匹配操作的。如果總是申請(qǐng)資源而不釋放資源,最終會(huì)導(dǎo)致資源全部被占用而沒(méi)有資源可用的場(chǎng)景。但是,在實(shí)際的編程中,我們總是會(huì)各種不小心的就把釋放操作忘了,就是編程的老手,在幾千行代碼,幾萬(wàn)行中代碼中,也會(huì)犯這種低級(jí)的錯(cuò)誤。
再來(lái)一個(gè)例子:
#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 ;
}
上述這個(gè)例子的模型,在實(shí)際中是經(jīng)常使用的,我們不能期待每個(gè)操作都是成功返回的,所以,每一個(gè)操作,我們需要做出判斷,上述例子中,當(dāng)操作失敗時(shí),然后,釋放內(nèi)存,返回程序。上述的代碼,極度臃腫,效率下降,更可怕的是,程序的可理解性和可維護(hù)性明顯降低了,當(dāng)操作增多時(shí),處理資源釋放的代碼就會(huì)越來(lái)越多,越來(lái)越亂。如果某一個(gè)操作發(fā)生了異常而導(dǎo)致釋放資源的語(yǔ)句沒(méi)有被調(diào)用,怎么辦?這個(gè)時(shí)候,RAII機(jī)制就可以派上用場(chǎng)了。
如何使用RAII?
當(dāng)我們?cè)谝粋€(gè)函數(shù)內(nèi)部使用局部變量,當(dāng)退出了這個(gè)局部變量的作用域時(shí),這個(gè)變量也就別銷毀了;當(dāng)這個(gè)變量是類對(duì)象時(shí),這個(gè)時(shí)候,就會(huì)自動(dòng)調(diào)用這個(gè)類的析構(gòu)函數(shù),而這一切都是自動(dòng)發(fā)生的,不要程序員顯示的去調(diào)用完成。這個(gè)也太好了,RAII就是這樣去完成的。由于系統(tǒng)的資源不具有自動(dòng)釋放的功能,而C++中的類具有自動(dòng)調(diào)用析構(gòu)函數(shù)的功能。如果把資源用類進(jìn)行封裝起來(lái),對(duì)資源操作都封裝在類的內(nèi)部,在析構(gòu)函數(shù)中進(jìn)行釋放資源。當(dāng)定義的局部變量的生命結(jié)束時(shí),它的析構(gòu)函數(shù)就會(huì)自動(dòng)的被調(diào)用,如此,就不用程序員顯示的去調(diào)用釋放資源的操作了?,F(xiàn)在,我們就用RAII機(jī)制來(lái)完成上面的例子。代碼如下:
#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; // 非常感謝益可達(dá)非常犀利的review,詳細(xì)可以參加益可達(dá)在本文的評(píng)論 2014.04.13
m_Array = NULL ;
}
}
private :
int *m_Array;
};
bool OperationA();
bool OperationB();
int main()
{
ArrayOperation arrayOp;
arrayOp.InitArray();
arrayOp.ShowArray();
return 0;
}
上面這個(gè)例子沒(méi)有多大的實(shí)際意義,只是為了說(shuō)明RAII的機(jī)制問(wèn)題。下面說(shuō)一個(gè)具有實(shí)際意義的例子:
/*
** FileName : RAII
** Author : Jelly Young
** Date : 2013/11/24
** Description : More information, please go to http://chabaoo.cn
*/
#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 ) // 非常感謝益可達(dá)犀利的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;
}
這個(gè)例子可以說(shuō)是實(shí)際項(xiàng)目的一個(gè)模型,當(dāng)多個(gè)進(jìn)程訪問(wèn)臨界變量時(shí),為了不出現(xiàn)錯(cuò)誤的情況,需要對(duì)臨界變量進(jìn)行加鎖;上面的例子就是使用的Windows的臨界區(qū)域?qū)崿F(xiàn)的加鎖。但是,在使用CRITICAL_SECTION時(shí),EnterCriticalSection和LeaveCriticalSection必須成對(duì)使用,很多時(shí)候,經(jīng)常會(huì)忘了調(diào)用LeaveCriticalSection,此時(shí)就會(huì)發(fā)生死鎖的現(xiàn)象。當(dāng)我將對(duì)CRITICAL_SECTION的訪問(wèn)封裝到MyLock類中時(shí),之后,我只需要定義一個(gè)MyLock變量,而不必手動(dòng)的去顯示調(diào)用LeaveCriticalSection函數(shù)。
上述的兩個(gè)例子都是RAII機(jī)制的應(yīng)用,理解了上面的例子,就應(yīng)該能理解了RAII機(jī)制的使用了。
使用RAII的陷阱
在使用RAII時(shí),有些問(wèn)題是需要特別注意的。容我慢慢道來(lái)。
先舉個(gè)例子:
#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;
}
這個(gè)例子是在上個(gè)例子上的基礎(chǔ)上進(jìn)行修改的。添加了一個(gè)DoComplex函數(shù),在線程中調(diào)用該函數(shù),該函數(shù)很普通,但是,該函數(shù)的參數(shù)就是我們封裝的類。你運(yùn)行該代碼,就會(huì)發(fā)現(xiàn),加入了該函數(shù),對(duì)gGlobal全局變量的訪問(wèn)整個(gè)就亂了。你有么有想過(guò),這是為什么呢?網(wǎng)上很多講RAII的文章,都只是說(shuō)了這個(gè)問(wèn)題,但是沒(méi)有說(shuō)為什么,在這里,我好好的分析一下這里。
由于DoComplex函數(shù)的參數(shù)使用的傳值,此時(shí)就會(huì)發(fā)生值的復(fù)制,會(huì)調(diào)用類的復(fù)制構(gòu)造函數(shù),生成一個(gè)臨時(shí)的對(duì)象,由于MyLock沒(méi)有實(shí)現(xiàn)復(fù)制構(gòu)造函數(shù),所以就是使用的默認(rèn)復(fù)制構(gòu)造函數(shù),然后在DoComplex中使用這個(gè)臨時(shí)變量。當(dāng)調(diào)用完成以后,這個(gè)臨時(shí)變量的析構(gòu)函數(shù)就會(huì)被調(diào)用,由于在析構(gòu)函數(shù)中調(diào)用了LeaveCriticalSection,導(dǎo)致了提前離開(kāi)了CRITICAL_SECTION,從而造成對(duì)gGlobal變量訪問(wèn)沖突問(wèn)題,如果在MyLock類中添加以下代碼,程序就又能正確運(yùn)行:
MyLock( const MyLock & temp )
{
EnterCriticalSection(&cs);
}
這是因?yàn)镃RITICAL_SECTION 允許多次EnterCriticalSection,但是,LeaveCriticalSection必須和EnterCriticalSection匹配才能不出現(xiàn)死鎖的現(xiàn)象。
為了避免掉進(jìn)了這個(gè)陷阱,同時(shí)考慮到封裝的是資源,由于資源很多時(shí)候是不具備拷貝語(yǔ)義的,所以,在實(shí)際實(shí)現(xiàn)過(guò)程中,MyLock類應(yīng)該如下:
class MyLock
{
public:
MyLock()
{
EnterCriticalSection(&cs);
}
~MyLock()
{
LeaveCriticalSection(&cs);
}
private:
MyLock(const MyLock &);
MyLock operator =(const MyLock &);
};
這樣就防止了背后的資源復(fù)制過(guò)程,讓資源的一切操作都在自己的控制當(dāng)中。如果要知道復(fù)制構(gòu)造函數(shù)和賦值操作符的調(diào)用,可以好好的閱讀一下《深度探索C++對(duì)象模型這本書》。
總結(jié)
說(shuō)了這么多了,RAII的本質(zhì)內(nèi)容是用對(duì)象代表資源,把管理資源的任務(wù)轉(zhuǎn)化為管理對(duì)象的任務(wù),將資源的獲取和釋放與對(duì)象的構(gòu)造和析構(gòu)對(duì)應(yīng)起來(lái),從而確保在對(duì)象的生存期內(nèi)資源始終有效,對(duì)象銷毀時(shí)資源一定會(huì)被釋放。說(shuō)白了,就是擁有了對(duì)象,就擁有了資源,對(duì)象在,資源則在。所以,RAII機(jī)制是進(jìn)行資源管理的有力武器,C++程序員依靠RAII寫出的代碼不僅簡(jiǎn)潔優(yōu)雅,而且做到了異常安全。在以后的編程實(shí)際中,可以使用RAII機(jī)制,讓自己的代碼更漂亮。
相關(guān)文章
一起來(lái)學(xué)習(xí)C++中remove與erase的理解
這篇文章主要為大家詳細(xì)介紹了C++的remove與erase,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-03-03C語(yǔ)言用指針函數(shù)尋找數(shù)組中的最大值與次大值
這篇文章主要給大家介紹了關(guān)于C語(yǔ)言用指針函數(shù)尋找數(shù)組中的最大值與次大值的相關(guān)資料,該代碼通過(guò)定義一個(gè)名為L(zhǎng)argestTow的函數(shù)來(lái)找出數(shù)組中的最大值和次大值,并將結(jié)果分別存入指針?biāo)赶虻膬?nèi)存單元中,需要的朋友可以參考下2024-11-11C語(yǔ)言實(shí)現(xiàn)單鏈表實(shí)現(xiàn)方法
這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)單鏈表實(shí)現(xiàn)方法的相關(guān)資料,鏈表分為單向鏈表、雙向鏈表、循環(huán)鏈表,需要的朋友可以參考下2017-08-08C++中main函數(shù)怎樣調(diào)用類內(nèi)函數(shù)
這篇文章主要介紹了C++中main函數(shù)怎樣調(diào)用類內(nèi)函數(shù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08wxWidgets實(shí)現(xiàn)無(wú)標(biāo)題欄窗口拖動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了wxWidgets實(shí)現(xiàn)無(wú)標(biāo)題欄窗口拖動(dòng)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02詳解C++中typedef 和 #define 的區(qū)別
這篇文章主要介紹了C++中typedef 與 #define 的區(qū)別,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09