C++中的低級內(nèi)存操作
C++相較于C有一個巨大的優(yōu)勢,那就是你不需要過多地擔心內(nèi)存管理。如果你使用面向?qū)ο蟮木幊谭绞?,你只需要確保每個獨立的類都能妥善地管理自己的內(nèi)存。通過構(gòu)造和析構(gòu),編譯器會幫助你管理內(nèi)存,告訴你什么時候需要進行內(nèi)存操作。將內(nèi)存管理隱藏在類中顯著提高了可用性,這一點在標準庫類中得到了很好的體現(xiàn)。
然而,在某些應用程序或遺留代碼中,你可能會遇到需要更低級別地操作內(nèi)存的情況。無論是出于遺留代碼、效率、調(diào)試還是好奇心,了解如何操作原始字節(jié)總是有幫助的。
指針
C++編譯器會使用指針的聲明類型來允許你進行指針算術(shù)。如果你聲明了一個指向整數(shù)的指針并將其增加1,那么這個指針在內(nèi)存中前進的距離是整數(shù)的大小,而不是一個單一的字節(jié)。這種操作對數(shù)組最有用,因為數(shù)組包含的數(shù)據(jù)是類型一致且在內(nèi)存中連續(xù)的。
例如,假設(shè)你在自由存儲區(qū)聲明了一個整數(shù)數(shù)組:
int* myArray { new int[8] };
你可能已經(jīng)熟悉了以下用于設(shè)置索引為2的值的語法:
myArray[2] = 33;
通過使用指針算術(shù),你也可以使用以下等效的語法,該語法獲取到myArray“向前兩個整數(shù)”處的內(nèi)存的指針,然后解引用它以設(shè)置該值:
*(myArray + 2) = 33;
作為訪問單個元素的另一種語法,指針算術(shù)看起來可能不太吸引人。但其真正的力量在于,像myArray+2這樣的表達式仍然是一個指向整數(shù)的指針,因此可以表示一個更小的整數(shù)數(shù)組。
讓我們通過一個使用寬字符串的例子來看看。寬字符串支持所謂的Unicode字符,以表示例如日語字符串。wchar_t類型是一個可以容納這種Unicode字符的字符類型,并且通常比char要大;也就是說,它不僅僅是一個字節(jié)。要告訴編譯器一個字符串字面量是一個寬字符串字面量,可以在其前面加上一個L。
例如,假設(shè)你有以下寬字符串:
const wchar_t* myString { L"Hello, World" };
進一步假設(shè)你有一個函數(shù),該函數(shù)接受一個寬字符串并返回一個包含輸入字符串大寫版本的新字符串:
wchar_t* toCaps(const wchar_t* text);
請記住,C風格的字符串是以零結(jié)尾的,即它們的最后一個元素包含\0。因此,沒有必要在函數(shù)中添加一個大小參數(shù)來指定輸入字符串的長度。該函數(shù)只需不斷地迭代字符串的各個字符,直到遇到\0字符為止。
通過將myString傳遞給toCaps()函數(shù),你可以將其全部轉(zhuǎn)換為大寫。然而,如果你只想部分地大寫化myString,你可以使用指針算術(shù)來僅引用字符串的后一部分。下面的代碼通過僅向指針加7來調(diào)用toCaps()函數(shù),以處理寬字符串中的“World”部分,盡管wchar_t通常超過1個字節(jié):
toCaps(myString + 7);
另一個有用的指針算術(shù)應用涉及到減法。從同一類型的另一個指針中減去一個指針會給你兩個指針之間指向類型的元素數(shù)量,而不是它們之間的絕對字節(jié)數(shù)。
自定義內(nèi)存管理
在你將遇到的99%(有人可能會說100%)的情況中,C++中的內(nèi)置內(nèi)存分配功能是足夠的。在幕后,new和delete完成了以適當大小的塊分配內(nèi)存、維護可用內(nèi)存區(qū)域列表以及在刪除時將內(nèi)存塊釋放回該列表的所有工作。但是,當資源約束非常緊張,或者在非常特殊的條件下,例如管理共享內(nèi)存,實施自定義內(nèi)存管理可能是一個可行的選項。不用擔心,這并不像聽起來那么可怕。
基本上,自己管理內(nèi)存意味著類會分配一大塊內(nèi)存,并根據(jù)需要將該內(nèi)存分配出去。這種方法有什么好處呢?管理自己的內(nèi)存可能會減少開銷。當你使用new來分配內(nèi)存時,程序還需要預留一小部分空間以記錄分配了多少內(nèi)存。這樣,當你調(diào)用delete時,可以釋放適當數(shù)量的內(nèi)存。對于大多數(shù)對象,開銷比分配的內(nèi)存小得多,因此影響甚微。然而,對于小對象或有大量對象的程序,開銷可能會產(chǎn)生影響。當你自己管理內(nèi)存時,你可能會提前知道每個對象的大小,因此你可能能夠避免每個對象的開銷。對于大量的小對象來說,差異可能是巨大的。
執(zhí)行自定義內(nèi)存管理需要重載new和delete操作符。
垃圾收集
在支持垃圾收集的環(huán)境中,程序員很少(如果有的話)明確釋放與對象關(guān)聯(lián)的內(nèi)存。相反,不再有任何引用的對象將在某個時候被運行時庫自動清理。垃圾收集沒有像在C#和Java中那樣內(nèi)置到C++語言中。在現(xiàn)代C++中,你使用智能指針來管理內(nèi)存,而在遺留代碼中,你將看到通過new和delete在對象級別進行內(nèi)存管理。
諸如shared_ptr這樣的智能指針提供了與垃圾收集內(nèi)存非常相似的東西;也就是說,當某一資源的最后一個shared_ptr實例被銷毀時,該資源也會在那個時刻被銷毀。在C++中實現(xiàn)真正的垃圾收集是可能但不容易的,但釋放自己從釋放內(nèi)存的任務中可能會引入新的問題。
標記-清除垃圾收集
一種垃圾收集的方法被稱為“標記-清除”(Mark and Sweep)。在這種方法下,垃圾收集器會周期性地檢查程序中的每一個指針,并標注哪些內(nèi)存仍然在使用中。在周期結(jié)束時,任何沒有被標記的內(nèi)存被認為是不再使用的,并會被釋放。在C++中實現(xiàn)這樣的算法并不簡單,如果做錯了,可能比使用delete更容易出錯!
盡管在C++中已經(jīng)有了安全且簡單的垃圾收集機制的嘗試,但即使有了完美的C++垃圾收集實現(xiàn),也不一定適用于所有應用程序。垃圾收集的缺點包括:
- 當垃圾收集器正在運行時,程序可能變得無響應。
- 在使用垃圾收集器的情況下,你會遇到所謂的“非確定性析構(gòu)函數(shù)”。因為一個對象在被垃圾收集之前不會被銷毀,所以析構(gòu)函數(shù)在對象離開其作用域時不會立即執(zhí)行。這意味著,由析構(gòu)函數(shù)完成的資源清理(例如關(guān)閉文件、釋放鎖等)直到未來某個不確定的時間才會被執(zhí)行。
編寫垃圾收集機制是非常困難的。你很可能會做錯,而且很可能會很慢。因此,如果你確實想在你的應用程序中使用垃圾收集內(nèi)存,我強烈建議你研究現(xiàn)有的專門的垃圾收集庫,并重用它們。
對象池
垃圾收集就像是為野餐購買盤子,并將用過的盤子留在院子里,以便某人在某個時候?qū)⑺鼈儞炱饋聿⑷拥?。肯定有更環(huán)保的內(nèi)存管理方法。對象池就是回收的等價物。你購買了一定數(shù)量的盤子,使用過一個盤子后,你將其清洗,以便以后可以再次使用。
對象池是理想的解決方案,適用于你需要在一段時間內(nèi)多次使用相同類型的多個對象,并且每次創(chuàng)建都會產(chǎn)生開銷的情況。