有效的內存管理
在程序中使用動態內存優點:
1. 動態內存可以在不同的對象與函數之間共享。
2. 動態分配的內存空間的大小可以在運行時確定。
預備知識:
int i = 7;
i實在棧上分配的。
int *ptr;
ptr = new int;
指針ptr在棧上,而ptr指向的內存在堆上。
int **handle;
handle = new int*;
*handle = new int;
handle指針在棧上,*handle指針在堆上,*handle指向的內存單元也在堆上。
使用new分配內存時是在堆上分配的,它會返回一個指向分配好的內存塊的指針。需要使用delete顯示釋放。如果忽略了new的返回值,或者指針變量超出了作用域,那麼內存就會變成孤立單元,因為你再也無法訪問這塊內存了。
malloc與new的區別:
new不僅僅是分配內存,而且還調用對象的構造函數。malloc只是預留一塊固定大小的內存,它並不關心對象是什麼。
free()與delete的區別:
使用free()時,並不會調用對象的析構函數。使用delete會調用對象的析構函數,並且會正確清除該對象。
數組:
作為經驗:不要使用realloc(),這個很危險,因為用戶定義的對象不能很好地盡享按位復制。
刪除數組:
使用用於數組的new(new[])分配內存時,就必須用用於數組的delete(delete[])來釋放內存。這個delete除了釋放相關內存之外,還會自動撤銷數組中的對象。
Simple *mySimple = new Simple[4];
//use mySimple
delete[] mySimple;
當然,只有當數組中的元素是純對象是才會調用析構函數。如果是一個指針數組,則仍然需要單獨地刪除各個元素,就向單獨分配每個元素一樣。
Simple **mySimple = new Simple*[4];
for(int i = 0;i<4;i++)
mySimple[i] = new Simple();
//use mySimple
for(int i = 0;i<4;i++)
delete mySimple[i];
delete[] mySimple;
多維棧數組:
多維堆數組:
基於堆的多維數組就想基於堆的一維數組一樣,可以通過指針對其進行訪問。區別在於,對於N維數組,需要N層指針。
下面代碼編譯不會通過,
char **board = new char[i][j]; //error
因為基於堆的多維數組不像基於棧的多維數組那樣工作。為其分配的內存不是連續的,所以此做法是不正確的。正確的做法是:必須建基於堆的數組的一維下標分配一個連續的數組。該數組的每個元素實際上是指向另一個數組的指針,這個數組存儲了對應第二維下標的元素。
遺憾的是,編譯器不會自動給你分配子數組的內存。這就需要你明確分配了。釋放時,delete不會自動刪除子數組,也需要手動釋放。
// new
char ** myArray = new char*[xSize];
for(int i = 0;i<xSize;i++)
myArray[i] = new char[ySize];
//delete
for(int i = 0;i<xSize;i++)
delete[] myArray[i];
delete[] myArray;
使用指針:
由於指針使用相對容易,所以很容易遭到濫用。因為指針僅僅是一個內存地址,所以在理論上可以手動修改。甚至可以做下面不該做的事:
char *p = 7;
上面代碼建立了一個指向內存地址為7的指針,它可能是一個隨機垃圾,也可能是應用中其他地方正在使用的一個內存。如果使用了不是用new分配的內存區域,最終會破壞與對象關聯的內存,這樣程序很容易崩潰。
用*對指針進行解除引用。
用&對變量取地址。
指針類型那個強制轉換:
既然指針僅僅是內存地址,所以指針是弱類型的。指向XML文檔的指針和指向整數的指針大小也是一樣的。通常使用c風格的類型強制轉換,編譯器可以很容易地把任何類型的指針強制轉換成另一種指針類型。
Document *docPtr = getDocument();
char *charPtr = (char*)docPtr;
static類型強制轉換的安全性更高一些。編譯器會拒絕對指向不同數據類型的指針完成static類型強制轉換。
Document *docPtr = getDocument();
char *charPtr = static_cast<char*>(docPtr); //error
如果要強制轉換類型的二個指針實際上是指向通過繼承相關聯的對象,編譯器允許完成static_cast類型強制轉換。
const指針:見const關鍵字的說明。
在實際中,很少需要保護指針。如果函數能夠改變所傳遞的指針值,這也無關大礙。其作用僅限於這個函數內部,對於調用者而言,指針仍然指向它原來指向的地址。把指針設置為const,這對於說明指針用途更有意義,其實提供不了多少真正的保護。但是保護指針指向的值則是很常見,從而防止重寫共享數據。
數組與指針的對應:
指針和數組之間存在某種重疊。在堆上分配的數組由指向第一個元素的指針引用。基於棧的數組使用數組語法([])來引用。
基於棧的數組的地址其實就是第0個元素的地址,數組名就是指向第0個元素的指針。只是這個指針不能改變。
可以函數傳遞基於棧或者基於堆的數組。在傳遞基於棧的數組時,編譯器會自動把數組變量退化為指向數組的指針來處理。在傳遞基於堆的數組時,因為指向數組的指針已經存在,只是簡單地按值傳遞給函數即可。如果函數取數組作為實參,並改變了數組中的值,這個函數實際上是修改了原始數組。,而不是數組的副本。其實這樣是因為C++考慮效率問題,而沒有采用復制數組,而是采用將其退化為指針來處理,因為復制數組需要花較多的時間,還可能消耗大量的內存空間。
字符串:
C風格的字符串:
千萬記住C風格字符串後面還有一個占空間的’\0’;
字符串直接量:與字符串直接量相關聯的內存位於內存的只讀部分。
char *ptr = “hello”; //字符串直接量賦給變量,ptr指向了這個只讀內存
ptr[1] = ‘a’; //不能這樣做, 這是個字符串常量,不能修改
安全的做法就是:
const char *ptr = “hello”; //字符串直接量賦給const 變量
ptr[1] = ‘a’;
也可以使用字符串直接量作為基於棧的字符數組的初始值,因為基於棧的變量在任何情況之下都不可能引用其他地方的內存,所以編譯器會負責把字符串直接量復制到基於棧的數組內存中。
char stackArray[] = ‘hello’;
char stackArray[1] = ‘o’; //ok
C風格的字符串的優缺點:
優點:1.比較簡單,利用了底層的基本字符類型和數據結構。
2.占用空間小,如果正確使用,它們只需占用真正需要的內存空間。
3.更底層,所以可以作為原始內存很容易地進行處理和復制。
4.程序員能夠更好地理解。
缺點:
1.不能忍受內存bug的存在,而且很受其影響。
2.沒用充分利用C++面向對象特性。
3.提供的輔助函數命名很糟糕,有時還會把人搞糊塗。
4.要求程序員了解字符串的底層表示。
C++的字符串類string:
基於操作符重載的魔力,string使用+連接二個字符串,=進行賦值(會進行字符串復制),==進行比較,[]進行訪問單個字符。
可以使用c_str()把C++string轉化為C風格的字符串
低級的內存操作:
C++的主要優點之一就是不需要特別擔心關於內存的問題。如果代碼用到了對象,只需要確保各個類能適當地管理它自己的內存即可。通過構造和撤銷對象,編譯器會告訴你什麼時候做什麼,從而幫助你管理內存。但是出於某些應用,可能會遇到這種情況,即需要在低層次上使用內存。
指針運算:指針加1是指針向前移動一個單位。同類型指針減是二指針之間的元素的個數。
如果編一個把字符串轉換為大寫,char *toCaps(const char * inString);
如果只想想把字符串myStr的後面轉換為大寫,則可以這樣調用 toCaps(myStr+5);
自定義內存管理:大多數情況之下,內置的內存分配功能就已經足夠了,但是如果資源要求緊張,就完全可以自己來管理內存。自己管理內存可能會減少資源開銷。使用new分配內存時,程序還需要保留一小塊空間來記錄已經分配了多少內存空間。這樣,調用delete時,就可以釋放適當數量的內存。對於大部分對象,相對於分配的內存來說,這個開銷要小得多,所以並沒有太大的區別。然而,對於小的對象或者有大量對象的程序,這種開銷可能會有較大的影響。自己管理內存時,你事先已經知道每個對象的大小,所以可以避免這個開銷。對於大量的小對象來說,與使用new和delete的方法相比,這樣就會帶來很大的差別。
垃圾回收:在支持垃圾回收的環境中,程序員很少需要顯式地釋放與對象關聯的內存。取而代之的是,有一個低優先級的後台任務負責監視內存狀態,清理它認為不需要的內存。
不同於java語言,在C++中,沒有把垃圾回收作為內置功能。大部分的C++程序通過new和delete在對象層次上管理內存。在C++中實現垃圾回收也不是不可能,但是要想從釋放內存的任務中解脫出來,可能又會帶來新的問題。
垃圾回收的一種方法稱為標記和清掃。使用這種方法,垃圾回收器會周期性地檢查程序中得每個指針,並標記所引用的內存仍在使用。在循環結束時,沒有標記的內存就認為未在使用,可以釋放。
需要完成的步驟:
1. 向垃圾回收器注冊所有的指針,這樣就可以很容易地掃描整個指針列表。
2. 讓所有對象派生一個混合類(如GarbageCollectible),它允許垃圾回收器把對象標記正在使用。
3. 確保垃圾回收器運行時不會對指針做修改,以此來保護對對象的並發訪問。
這個簡單的垃圾回收方法要求程序員很仔細才行。與delete相比,這種方法可能更容易帶來錯誤。在C++中已經試圖建立一種安全而容易的機制來完成垃圾回收,但是即使在C++確實提供了一個理想的垃圾回收實現,也不一定適用於所有的應用。
垃圾回收存在以下缺點:
1. 垃圾回收器主動運行時,可能會使程序的運行減慢。
2. 如果程序大量地分配內存,那麼垃圾回收器可能跟不上這個速度。
3. 如果垃圾回收器本身有bug,或者認為一個已經拋棄的對象仍然在使用,可能會造成不可恢復的內存洩露。
對象池:後面再分析。
函數指針:每個函數的確都位於一個特定的地址。在C++中,可以把函數作為數據使用,換句話說,可以把函數的地址作為參數,就想變量一樣使用。
函數指針根據參數類型和兼容函數的返回類型來確定函數類型。使用函數指針最容易的方法就是使用typedef機制來為一組有給定特征的函數賦一個函數名。下面聲明了一個類型YesNoFcn,它表示一個指針,該指針指向有二個int參數且返回bool類型的任意函數。
typedef bool (*YesNoFcn)(int,int);
既然有了新類型,就可以編寫一個取YesNoFcn作為參數的函數了。
void findMatches(int values1[],int values2[],int numValues,YesNoFcn inFunction)
{
for(i=0;i<numValues;i++)
if(inFunction(values1[i],values[2]))
cout<<”match! “;
else cout<<”not match! “;
cout<<endl;
}
bool intEqual(int inItem1,int inItem2)
{
return inItem1==inItem2;
}
//調用
int a[2]={1,2},b[2]={1,3};
findMatches(a,b,2,&intEqual);
常見的內存陷阱:
1. 字符串空間分配不足
char str[3] = “yes”; //error 還有一個’\0’
2. 內存洩露
如果分配了內存,但是忘記了釋放,此時就容易產生內存洩露。(跟蹤內存使用情況可以使用免費的valgrind工具)。
也可以使用智能指針避免內存洩露。即如果把一切都放在棧中,這就可以避免與內存相關的大多數問題。棧比堆更安全,因為棧變量一旦超出作用域時就會自動撤銷和清除。智能指針結合了棧變量的安全性和堆變量的靈活性。它是一個帶有關聯指針的對象。當智能指針超出作用域時,會刪除關聯的指針。本質上講就是在一個基於棧的對象內包裝一個堆對象。
C++標准模板庫包含了一個智能指針的基本實現。叫做auto_ptr。可以把動態分配的對象存儲在基於棧的auto_ptr實例中,而不是存儲在指針中。不需要顯式地釋放與auto_ptr關聯的內存-auto_ptr超出作用域時,與之關聯的內存會得到清除。
void leaky()
{
Simple * mySimple = new Simple();
mySimple->go();
}//沒有顯式地釋放內存,刪除對象。
void leaky()
{
auto_ptr<Simple> mySimple(new Simple);
mySimple->go();
}
智能指針也和標准指針一樣可以使用* ->來解除引用。
3.二次刪除與無效指針
一旦使用delete釋放了與指針關聯的內存,程序中得其他部分就可以使用這段內存了。但是,沒有什麼能阻止你試圖繼續使用這個指針。二次刪除也是一個問題。如果在指針上第二次使用delete,程序可能會釋放已經指派給另一個對象的內存。
許多內存洩露檢查程序(如valgrind)也會檢查二次刪除和使用已釋放的對象等問題。
4.訪問越界指針
導致越界寫內存的bug經常稱為緩沖區溢出錯誤。這樣的bug已經被一些能力極強的病毒和蠕蟲所利用,狡猾的黑客也可以利用只一點重寫內存的某一部分。從而在正在運行的程序中注入代碼。
幸運的是,許多內存檢測工具也可以檢測出緩沖區溢出錯誤。而且,盡管C風格的字符串和數組寫入內容時存在大量相關的bug,但是如果使用像C++字符串和向量這樣一些高級構造的話,有助於防止這樣一些bug