用RAII技术管理资源及其泛型实现
前言
RAII的含義是“資源獲取即初始化”。
一段看似安全的代碼
首先看一段代碼:
這段代碼中,我們先進行了動態內存分配,使用完釋放,看起來很完美,但是這段程序是否真的保證不會發生內存泄漏?
考慮這樣一種情形,程序在使用這段內存的過程中throw一個異常,于是程序轉向catch塊,然后XXX。 這段內存被釋放了嗎? 顯然沒有。 那么這段程序應該從哪里改進呢?
對象的生命期
考慮一個問題:C++中對象的聲明期是怎樣的?在C++中,對象創建的方式有兩種,一種是棧上,一種是堆上創建。
上面的代碼中,第一個對象創建在棧上,更明確的說法是它是一個局部變量,這意味著它的生命期起源于被創建的這行語句,終結與所在作用域的末尾,也就是這里的右花括號(})。
而第二個對象呢?它是采用所謂的動態內存分配生成的,需要程序員手工去釋放,當調用delete的時候才銷毀,但調用delete的時機不是固定的。
也就是說,棧對象的生命期是明確的,而堆對象的生命期由于取決于調用delete的時機,因而是不明確的。
看到這里,之前那段有可能內存泄漏的代碼如何去改進呢?
答案就是用棧對象明確的生命期去管理資源。
用對象的生命期管理資源
試想一下,如果我們把之前程序中,對內存的分配寫在構造函數中,把釋放資源寫在析構函數中,而棧對象的生命期是明確的,當該管理資源的對象過期時,連同它管理的資源一起釋放,豈不是非常智能化?
我們嘗試著寫出下列代碼:
我們把之前的代碼做如下的改進:
再來分析一下這段代碼:
如果正常執行,那么當執行完try塊時,scope對象過期,執行析構函數,同時釋放了那段數組。如果使用的過程中發生了異常,那么當程序進入catch塊時,同樣會銷毀try內的局部變量。
無論是哪種情況,內存總是會被釋放。
如果這里不是int,而是其他復雜的類型,使用這個封裝的ScopePtr是不是不太方便?顯然不會,我們去重載成員操作符就可以了,使它表現的像個指針,這就是一個最簡單的智能指針的產生。
問題得到了完美的解決!
資源獲取即初始化
我們上面解決問題的辦法就是RAII技術,RAII的含義是“資源獲取即初始化”,這個概念有兩個要點:
- 獲得資源后立即放進管理對象
- 管理對象運用析構函數確保資源被釋放
看另外一個例子:我們在訪問一些臨界區資源的時候通常需要加鎖,所以產生了下面的代碼:
這種方式是很容易出現問題的,例如程序中間遇見錯誤情況需要退出這個函數,此時很容易忘記解鎖:
此時如果再次進行Lock操作,就造成了死鎖。
解決這個問題的辦法仍然很簡單,我們去寫一個類:
這樣剛才那段代碼就可以修改成:
這樣,一旦離開這段代碼,程序立刻自動解鎖。
不過為了防止錯誤使用這個類,例如:
可以定義一個宏:
這樣我們在錯誤使用的時候,編譯期間就能發現錯誤。
一種泛型解決方案
劉未鵬在他的《C++11(及現代C++風格)和快速迭代式開發》中提出了一種泛型實現,利用了C++11的function和Lambda匿名函數,如下:
使用方式也很簡單:
其實就是將該資源釋放的函數代碼段注冊到Scope類,其中原理不再贅述。
與其他語言的對比
RAII是C++獨有的編程手段。通過RAII技術我們能夠做到資源不需要使用時立即釋放,這是其他GC語言所不具備的。
以Java為例,Java具有完善的GC(Garbage Collection,垃圾回收)機制,但是存在如下的缺點:
- GC只能回收內存,而對于打開的文件、數據庫連接等仍然需要手工關閉。
- GC因為進程優先級等原因,回收效率底下,詳情可以參考孟巖的《垃圾收集機制(Garbage Collection)批判》
conclusion
RAII技術是現代C++編程技術中及其重要的一部分,甚至有人稱其為“C++編程中最重要的編程技法”,可見其重要性。通過RAII,我們完全可以實現資源的自動化管理,寫出永不內存泄漏的程序。
參考資料
- 《C++ Primer》
- 《Effective C++》
- 《Linux多線程服務器端編程》
總結
以上是生活随笔為你收集整理的用RAII技术管理资源及其泛型实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql grant查看用户权限命令
- 下一篇: 83998 连接服务器出错_服务端 TC