C++的内存管理
C++內存區:全局/靜態,堆,棧,常量區,自由區
???? 堆:比棧大,可穿越作用域
庫:管理內存
RAII(resource acquisition is initlization):初始化獲取資源,銷毀釋放資源
?
???
??
摘要:C++是一種流行且功能強大的程序設計語言,利用C++已經產生出世界上各異的程序軟件包。C++是在C語言的基本之上發展而來,它能夠對C語言向下兼容,所以它也C語言的許多不足,其中內存管理就是社區中大家反應很大的“遺憾”。
概述
內存是計算機系統最重要的資源之一,要是管理不當會引起許多的問題。當然對整個系統的內存管理是操作系統的功能,這里我們不去關注這一點,這里我們關注是一般程序的內存管理,相對而言操作系統對內存管理是非一般的。
內存管理顧名思義就是程序對自身內存資源使用的管理,內存資源存在一個申請、使用、釋放的過程,對內存的管理就是對這個過程的按排與統籌,從而實現正確、高效的內存使用。
為什么要內存管理
在許多的其它語言中內存管理是由運行時系統自動完成的,這個特點也是許多語言相比較C++語言經津津樂道的高級特性。C++對內存管理是半自動的,也就是說有些內存的使用是系統自動管理的,而同時也有一些內存是系統不管理的,用戶必需自己手工去管理。
讓我們看看C++系統內存的建立的模型。C++中存在五類不同卻相互補充的內存形式,即全局靜態存儲區、堆存儲區、棧存儲區、自由存儲區和常量存儲區。這五類存儲區域系統提供了不同的支持,分別用于不同情況。對于用戶的內存管理主要是對于堆存儲區和自由存儲區域的,而自由存儲區域是C++從C中繼承而來,社區里極力反對使用它,事實上C++程序員很少使用或根本不用,對于其它三種存儲區都是系統自動管理的,用戶很少需要去手工管理,但是用戶卻需要在幾種內存區域里選擇一個合理的區域。
先讓我們看兩個例子,通過這個大家可以感性地感覺到內存管理的必要性和必需性。
While(1000*1000)
{
Int * p=new int;
(對 p或*p一些操作)(可能產生異常,從而產生內存泄漏)
Delete p;
}
這個例子中會看在一個大的循環我們從堆中分配了一個,然后對它做了一些事,最后把它釋放了。我們不防對比一下這個例子與下面的例子:
While(1000*1000)
{
Int p;
(對 p或&p一些操作)(就算有異常,也是異常安全的)
}
這個例子與上面的例子最大的區別就是一個是從堆中分配內存,一個是從棧中分配內存,其它的他們沒有區別在功能上它們完全做到一樣。然而有經驗的C++程序員都偏向于使用后者。因為前者較之后者容易引起如下問題:
在堆是分配與釋放效率是比棧中慢許多,如果大的循環中,這種慢會被放大,從而嚴重危害運行效率。
在堆中分配的內存必須手工管理,也就是說必須手工顯式地去釋放,系統不會自動去釋放,從而容易孳生內存泄漏——C++的一個雷區。
不是異常安全的,當發生異常時,即使我們沒有忘記釋放內存,可是流程沒有到釋放的地方可能就因為異常而返回。
通過這個例子的對比我們發現不同的選擇、不同的處理方法會引起不同的效果,有的會更少出現問題,有的卻問題總是難以避免。從而內存區域的選擇與具體內存區域的使用上就存在許多注意點,同時產生許多內存的使用的習慣,這就是為什么要管理內存的原因。
如何管理內存
內存管理的目的有兩個,一個是讓程序正確,這是一個基本的問題,因為C++程序可能存在內存泄漏,讓程序正確是首要的目的。另一個目的就是讓程序高效,這是一個相對次要的問題,因為程序首先要是正確的,然后才是性能問題。
C++社區里對內存的使用已經形成一大套有效且可行的辦法。由于程序正確是首要的,這里我們主要講述讓程序正確的設計哲學和慣用法。
內存管理最境界——不去管理
內存管理的最高境界就是“不去管理”,把系統能夠處理的全部交給系統處理。正如上面的例子一樣,我們完全可以不用堆,使用棧完全可以更高效地完成相同的事務,所以這時我們就應該使用堆,而應該毫不憂郁地選擇棧,不僅因為棧比堆高效,更因為棧上內存是系統自動管理的。可能有要問,那我們可以把所有內存的都在棧上申請,這個想法是不現實的,一方面系統的棧內存要比堆內存小得多,可能存在一些非常大的數據需要大量的內存,棧內存可能不夠,這時只有使用堆了。另一方面,棧內存的生命期也是系統自動管理的,有是可能讓內存的生命期穿過多個作用域,棧內存使用起來就不如堆內存了,相應的管理也不見得比堆內存管理方便,因為這時似乎在與系統反著做事一樣。
“不去管理”的方法有許多。最重要也是被社區廣泛推薦的方法就是使用庫,把內存管理的任務交給庫。C++標準庫就是一個最重要的庫,熟練使用這個庫,可以有效地減少打工內存管理的情況,從而間接實現“不去管理”的目標。
例如:我們現在需要寫一個函數,它要把用戶從標準輸入中輸入的文字傳入程序里以供進一步處理。如果我們使用標準庫中string類將會有效地降低問題的復雜度,同時也不用我們手工去管理內存,內存的管理已經由string的實現者完成了。如果我不使用string類,反而向C語言的方法處理,我們使用指向字符的數組或指針,這時內存管理就是必須的了,因為我不可能知道用戶的輸入有多大,所以這就必須從堆中分配內存,從而不免產生一系列問題,就算沒有問題產生,所以作出的努力也不能與前相比。
內存管理慣用法——RAII
“不去管理”哲學已經深入人心,其中著名的RAII慣用法在社區里已經廣泛傳播。RAII是英文“resource acquisition is initlization”的首字母縮寫,意思是“資源獲取就是初始化”。
這里的資源泛指系統的各種各樣的資源,如內存、鎖、文件等等。解釋一下這句話的意思就是把資源的獲取與對象的初始化相聯系,把資源獲取轉換為對象的初始化,從而可以把系統于對象的支持轉換為對資源管理的支持。讓我們先來看一個例子。
foo()
{
Object *p=new Object;
(對 p 做一些事)
Delete p;
}
這個例子是一個經典的例子,這樣代碼廣泛地存在世界的每一個地方,初一看上去,這個并沒有錯誤,可是仔細推敲一下就會發現大量不足,最大的不足就是,這樣做不是異常安全的,并且有時可能產生代碼冗余,就好比下面的樣子:
foo()
{
Object *p=new Object;
(對 p 做一些事)
(--》異常發生,p 指向的內存沒有釋放)
If(something)
{
Delete p; (每個出口都需要,復雜又易錯)
Return 0;
}
(對 p 做一些事)
Delete p;
}
如上面的例子,函數可能存在多個出口,這樣在每個出口都要加上delete 語句,這樣即煩瑣又不安全,程序員可能會不能承受這樣的復雜與多變,從而埋下錯誤的根源。同時如果在處理的過程,如果發生異常,那么p所指向的內存將不會釋放,從而導致程序錯誤,內存泄漏已經發生。這樣做不足已經看過,再讓我們看看RAII的做法。
Class object_piont
{
Object *p;
Public:
Object_piont():p(new object){}
~object_piont(){ delete p;}
(其它函數的定義,重載與指針相關的重要運算符)
};
foo()
{
Object_piont p;
(對p做自己想做的事)
(這里我不需要在擔心內存泄漏,也不需要在每個出口釋放內存,因為內存釋放已經
委托給析構函數了,而析構函數是系統自動調用的。)
}
讓我們來分析一下這樣做法的特點。首先它把內存的獲取放在對象的初始化里完成,這正是RAII的名字由來,“資源獲取就是初始化”。經過這樣處理之后就可以做到異常安全與內存管理的自動化,因為建構函數與析構函數都是系統自動完成的。
foo()
{
Object_piont p;
(對 p 做一些事)
(--》異常發生,p 指向的內存會自動釋放)
If(something)
{
//Delete p; (出口不需要再釋放內存,系統會自動完成)
Return 0;
}
(對 p 做一些事)
Delete p;
}
上面的例子解釋了什么是RAII慣用法,如何實現RAII慣用法。其實RAII慣用法是如此重要,以至于標準庫都已經提供了直接的支持,在 C++1998-2003的標準里存在一個智能指針auto_ptr ,這經個智能指針就會像上岸的例子一樣完成自己的任務,當然這是一個模板類,無能哪種對象都可以輕松產生出相應的指針類型。事實上,auto_ptr設計上存在許多不讓人滿意之處,所以還存在大量第三方庫提供了不同類型的智能指針。其中最著名的就是boost庫提供的智能指針庫,并且在新C++0x標準中已經把這個庫加入了標準。可見智能指針是多重要。當然智能指針只是RAII的思想了一個實現,我們在自己設計過程要時刻不忘這個思想方法,它可以讓我們的設計更優秀,實現的中產生錯誤的機會更小。
使用內存池,讓內存使用變得更高效
我們知道使用的分配與釋放是一個消耗CPU的事情,這個過程可能需要上下文切換,所以它的時間級別是毫秒級的,相對棧的時間相差許多倍。如果一個程序大量使用了堆內存的話,并且平凡地分配、釋放,這樣的話內存的分配與釋放就會影響到系統的性能和程序執行的效率上。所以為了在堆上高效地使用內存,人們開發出內存池技術。
其實堆上內存分配與釋放的低效在于可能的上下文切換與中斷處理,如果減少需要的上下文切換與中斷的話,就可以提高效率,實現快速的分配與釋放堆內存。內存池技術在于一個內存池管理模塊,這個模塊在初始化的時候會向系統堆中申請大量空余的內存,而其它模塊需要內存的時候不會向系統申請,而向這個內存池模塊申請,這樣就省去了操作系統的中斷與上下文切換處理,從而提高了運行效率與運行速度。
當然如果其它模塊需要的內存比內存池模塊擁有的內存少,內存的分配是不需要操作系統參于的,不過可能會一些內存的始終沒有用上,從浪費一些內存,可是對于這個空間浪費也是必須的,犧牲空間換來時間在程序設計中是常有的事;可是如果其它模塊需要的內存大于內存池所有的容量,那么內存池模塊會向操作系統申請內存,這也會產生中斷與上下文切換,不過這個機會要大大小于沒有內存池的時候,對系統性能的影響不是很大。
內存池實現對于大多數來說都不是需要的,因為已經有許多做過這個事了,在大名鼎鼎的boost庫中就有這樣一個內存池庫,在大名鼎鼎的apach網絡服務器的背也有他們實現的內存池,就是在GNU libc中也有這樣一個功能的庫。當然后兩者都C語言的庫,不過在C++中可以正常使用,前者卻是正統的C++庫,一般比較推薦這個。除了這三個以外,其實還大量存在其它的實現,不見如此有名,但也是多種多樣。
結束語
C++內存管理是程序對內存申請、使用、釋放過程的按排與統籌,系統會為我們做好多數的事,只有一些情況下需要手工去管理內存,需要手工管理的內存主要是堆內存。社區里已經堪一套有效的內存管理慣例思想方法。不去管理內存,把所以的事都交給系統是內存管理的最境界,但有時手工管理也是難免的,這時我們要優先選擇庫,再接著自己去設計一個RAII的類,最后才會考慮其它的情況。
C++的內存管理是一個比較深入的課題,相對那些可以完全自動管理內存的語言,這一點好像要遺憾一下。許多新手都在感言,要是C++可以自動管理內存的話該多好,其實有許多都在嘗試。垃圾回收在C++社區里一直有討論,可是社區里的重量級人物一般都不看好垃圾回收,一方面它C++中的析構機制不兼容,另一方面C++某種意義還是一個中級語言,它直接提供指針以及整數與指針的轉換,所以垃圾回收的算法等方法要比其它語言復雜多變與不確定,更重要的是 C++提供給內存管理的機制已經足夠強大,如果運用得當,不見得要多少精力去手工管理內存。要是一個程序里面這樣的手工管理內存機會太多,可能映射出設計上的缺陷與不足。
?
http://cpp.ezbty.org/content/science_doc/c%E7%9A%84%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86
總結
- 上一篇: 属性访问器(Property Acces
- 下一篇: .Net Framwork概述