程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 內存管理,內存管理軟件

內存管理,內存管理軟件

編輯:C++入門知識

內存管理,內存管理軟件


在寫C/C++程序的時候,最頭疼的部分一定包括內存管理,一提到內存管理總是提心吊膽,但是又不可避免地常常會遇到,而這塊的知識也是公司招聘的一個評價方面,能夠很好地體現筆試面試者的功底。這部分的內容稍深一些,大家細心閱讀,能對內存管理有一個比較好的把握。

內存分配的方式                                                                       

最長被問到的問題之一,基礎中的基礎。對C和C++而言,內存分配方式有三種: 

       1)從靜態存儲區域分配。例如程序中定義的全局變量和static變量就是這種方式分配內存的。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。 

       2)在棧上創建。這是出現最多的情況,我們程序中的int var就是這種情況的內存分配方式。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。 

       3)從堆上分配,亦稱動態內存分配。程序在運行的時候用malloc 或new 申請任意多少的內存,程序員自己負責在何時用free 或delete 釋放內存。動態內存的生存期由我們決定,使用非常靈活,但問題也最多。 

常見的內存錯誤及其相應建議                                                        

發生內存錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程序運行時才能捕捉到。而這些錯誤大多沒有明顯的症狀,時隱時現,增加了改錯的難度。有時用戶怒氣沖沖地把你找來,程序卻沒有發生任何問題,你一走,錯誤又發作了。 

       常見的內存錯誤和處理方法如下: 

1)內存分配未成功,卻使用了它。

       編程新手常犯這種錯誤,因為他們沒有意識到內存分配會不成功。常用解決辦法是,在使用內存之前檢查指針是否為NULL 。如果指針p 是函數的參數,那麼在函數的入口處用assert (p!=NULL) 進行檢查。如果是用malloc或new 來申請內存,應該用if( p==NULL ) 或if(p!=NULL) 進行防錯處理。 

2)內存分配雖然成功,但是尚未初始化就引用它。

       犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為內存的缺省初值全為零,導致引用初值錯誤(例如數組)。 

       內存的缺省初值究竟是什麼並沒有統一的標准,盡管有些時候為零值,我們寧可信其無不可信其有。所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。 

3)內存分配成功並且已經初始化,但操作越過了內存的邊界。

       例如在使用數組時經常發生下標“多1 ”或者“少1 ”的操作。特別是在for 循環語句中,循環次數很容易搞錯,導致數組操作越界。 

4)忘記了釋放內存,造成內存洩露。

       含有這種錯誤的函數每被調用一次就丟失一塊內存。剛開始時系統的內存充足,你看不到錯誤。終有一次程序突然死掉,系統出現提示:內存耗盡。 

       動態內存的申請與釋放必須配對,程序中malloc與free 的使用次數一定要相同,否則肯定有錯誤(new/delete 同理)。 

5)釋放了內存卻繼續使用它。

       有三種情況: 

       1. 程序中的對象調用關系過於復雜,實在難以搞清楚某個對象究竟是否已經釋放了內存,此時應該重新設計數據結構,從根本上解決對象管理的混亂局面。 

       2. 函數的return 語句寫錯了,注意不要返回指向“棧內存”的“指針”或者“引用”,因為該內存在函數體結束時被自動銷毀。 

       3. 使用free 或delete 釋放了內存後,沒有將指針設置為NULL。導致產生“野指針”。 

       針對以上這些稍不留神,一疏忽就容易犯的錯誤,這裡提出以下建議:

              1)用malloc 或new 申請內存之後,應該立即檢查指針值是否為NULL。防止使用指針值為NULL的內存。 

              2)不要忘記為數組和動態內存賦初值。防止將未被初始化的內存作為右值使用。 

              3)避免數組或指針的下標越界,特別要當心發生“多1 ”或者“少1 ”操作。 

              4)動態內存的申請與釋放必須配對,防止內存洩漏。 

              5)用free 或delete 釋放了內存之後,立即將指針設置為NUL L ,防止產生“野指針”

指針與數組                                                                            

       C++/C程序中,指針和數組在不少地方可以相互替換著用,讓人產生一種錯覺,以為兩者是等價的。數組要麼在靜態存儲區被創建(如全局數組),要麼在棧上被創建。數組名對應著(而不是指向)一塊內存,其地址與容量在生命期內保持不變,只有數組的內容可以改變。

       指針可以隨時指向任意類型的內存塊,它的特征是“可變”,所以我們常用指針來操作動態內存。指針遠比數組靈活,但也更危險。下面從一些角度說明指針和數組的不同。

1)關於兩者的內容修改

       一個例子,以下程序:

char a[] = “hello”;   
a[0] = ‘X’;   
cout << a << endl;   
char *p = “world”; // 注意p指向常量字符串  
p[0] = ‘X’; // 編譯器不能發現該錯誤  
cout << p << endl;

字符數組a的容量是6個字符,其內容為hello\0(位於棧上)。a的內容可以改變,如a[0]=‘X’。指針p指向常量字符串“world”(位於靜態存儲區,內容為world\0),常量字符串的內容是不可以被修改的。從語法上看,編譯器並不覺得語句p[0]= ‘X’有什麼不妥,但是該語句企圖修改常量字符串的內容而導致運行錯誤

2)關於兩者進行內容復制

    組名進行直接復制與比較。若想把數組a的內容復制給數組b,不能用語句b = a ,否則將產生編譯錯誤。應該用標准庫函數strcpy進行復制

       同理,比較b和a的內容是否相同,不能用if(b==a) 來判斷,應該用標准庫函數strcmp進行比較。

語句p =a 並不能把a的內容復制指針p,而是把a的地址賦給了p。要想復制a的內容,可以先用庫函數malloc為p申請一塊容量為strlen(a)+1個字符的內存,再用strcpy進行字符串復制。同理,語句if(p==a) 比較的不是內容而是地址,應該用庫函數strcmp來比較。

       具體的例子如下程序所示:

// 數組…  
char a[] = "hello";   
char b[10];   
strcpy(b, a);  // 不能用  b = a;   
if(strcmp(b, a) == 0) // 不能用if (b == a)   
…  
// 指針…  
int len = strlen(a);   
char *p = (char *)malloc(sizeof(char)*(len+1));   
strcpy(p,a);  // 不要用p = a;   
if(strcmp(p, a) == 0) // 不要用if (p == a)</span></span>

3)關於兩者內存容量的計算

運算符sizeof可以計算出數組的容量(字節數)。如下例程序中,sizeof(a)的值是12(注意別忘了’\0’)。指針p 指向a,但是sizeof(p)的值卻是4。這是因為sizeof(p)得到的是一個指針變量的字節數,相當於sizeof(char*),而不是p所指的內存容量C++/C語言沒有辦法知道指針所指的內存容量,除非在申請內存時記住它。

char a[] = "hello world";   
char *p = a;   
cout<< sizeof(a) << endl; // 12字節  
cout<< sizeof(p) << endl; // 4字節

注意當數組作為函數的參數進行傳遞時,該數組自動退化為同類型的指針。如下例中,不論數組a的容量是多少,sizeof(a)始終等於sizeof(char *)。

void Func(char a[100])   
{   
cout<< sizeof(a) << endl; // 4字節而不是100字節  
}

指針參數怎麼傳遞內存                                                               

如果函數的參數是一個指針,不要指望用該指針去申請動態內存。如下例中,Test函數的語句GetMemory(str,200)並沒有使str獲得期望的內存,str依舊是NULL。

void GetMemory(char *p, int num)   
{   
  p = (char *)malloc(sizeof(char) * num);   
}   
void Test(void)   
{   
  char *str = NULL;   
  GetMemory(str, 100);  // str 仍然為NULL   
  strcpy(str, "hello"); // 運行錯誤  
}

        來分析一下,其實上例的問題出在函數GetMemory中。編譯器總是要為函數的每個參數制作臨時副本,指針參數p的副本是_p,編譯器使 _p= p。如果函數體內的程序修改了_p的內容,就導致參數p的內容作相應的修改。這就是指針可以用作輸出參數的原因。在本例中,_p申請了新的內存,只是把_p所指的內存地址改變了,但是p絲毫未變(形參有時候就是很害人)。所以函數GetMemory並不能輸出任何東西。事實上,每執行一次GetMemory就會洩露一塊內存,因為沒有用free釋放內存。

       那如果筆試題或者面試官問,我一定要用指針參數去申請內存,我們怎麼做呢?好吧,那就耍個小花招,改用“指向指針的指針”好啦。如下程序所示:

void GetMemory2(char **p, int num)   
{   
*p = (char *)malloc(sizeof(char) * num);   
}  
void Test2(void)   
{   
char *str = NULL;   
GetMemory2(&str, 100); // 注意參數是&str,而不是str   
strcpy(str, "hello");   
cout<< str << endl;   
free(str);   
}

什麼,你覺得“指向指針的指針”不是很容易理解?好吧,那換一種方式,我們可以用函數返回值來傳遞動態內存。如下程序所示:

char *GetMemory3(int num)   
{   
char *p = (char *)malloc(sizeof(char) * num);   
return p;   
}  
void Test3(void)   
{   
char *str = NULL;   
str = GetMemory3(100);   
strcpy(str, "hello");   
cout<< str << endl;   
free(str);   
}

寫到這裡,又有一個可能會犯的錯誤需要提一下了,很重要,大家一定要注意了。用函數返回值來傳遞動態內存這種方法雖然好用,但是常常有人把return語句用錯了。這裡強調不要用return語句返回指向“棧內存”的指針因為該內存在函數結束時自動消亡!如下程序所示:

char *GetString(void)   
{   
char p[] = "hello world";   
return p;  // 編譯器將提出警告  
}   
void Test4(void)   
{   
char *str = NULL;   
str = GetString(); // str 的內容是垃圾  
cout<< str << endl;   
}

如果用調試器逐步跟蹤Test4,發現執行str = GetString語句後str不再是NULL指針,但是str的內容不是“hello world”而是垃圾。

寫到這裡,又有人提出問題了,如果上例中的hello world是常字符串,那會怎麼樣呢,就如下程序所示:

char *GetString2(void)   
{   
char *p = "hello world";   
return p;   
}   
void Test5(void)   
{   
char *str = NULL;   
str = GetString2();   
cout<< str << endl;   
}

那這裡要說明的是,事實上這段程序運行不會出錯,但是函數GetString2的設計概念卻是錯誤的。因為GetString2 內的“hello world”是常量字符串,位於靜態存儲區,它在程序生命期內恆定不變。無論什麼時候調用GetString2,它返回的始終是同一個“只讀”的內存塊,所以沒有太大的意義。

關於free和delete                                                                     

       別看free和delete的名字惡狠狠的(尤其是delete),它們只是把指針所指的內存給釋放掉,但指針本身卻被留下來了

       用調試器跟蹤下例,發現指針p被free以後其地址仍然不變(非NULL),只是該地址對應的內存是垃圾,p成了“野指針”。如果此時不把p設置為NULL,會讓人誤以為p是個合法的指針。

char *p = (char *) malloc(100);   
strcpy(p, “hello”);   
free(p);  // p  所指的內存被釋放,但是p所指的地址仍然不變  
…  
if(p != NULL)  // 沒有起到防錯作用  
{   
strcpy(p, “world”); // 出錯  
}

如果程序比較長,我們有時記不住p所指的內存是否已經被釋放,在繼續使用p之前,通常會用語句if(p!=NULL)進行防錯處理。很遺憾,此時if語句起不到防錯作用,因為即便p不是NULL指針,它也不指向合法的內存塊

動態內存會隨程序結束自動釋放嗎                                                   

現在有這樣一個例子:

void Func(void)   
{   
char *p = (char *) malloc(100);  // 動態內存會自動釋放嗎?  
}

       我們都知道函數體內的局部變量會在函數結束時自動消亡。這裡p是局部的指針變量,程序結束後它會消亡,但是它消亡時它所指的動態內存會一起完蛋嗎?答案是:並不會...

       這裡有兩條非常有趣的結論,分別是:

              (1)指針消亡了,並不表示它所指的內存會被自動釋放。

              (2)內存被釋放了,並不表示指針會消亡或者成了NULL指針。

關於“野指針”                                                                       

       “野指針”是最可怕的一類指針,它不是NULL指針,是指向“垃圾”內存的指針。人們一般不會錯用NULL指針,因為用if語句很容易判斷。但是“野指針”是很危險的,if語句對它不起作用。

       “野指針”的成因主要有兩種:

       (1)指針變量沒有被初始化。任何指針變量剛被創建時不會自動成為NULL指針,它的缺省值是隨機的,它會亂指一氣。所以,指針變量在創建的同時應當被初始化,要麼將指針設置為NULL,要麼讓它指向合法的內存。例如

char *p = NULL;   
char *str = (char *) malloc(100);

       (2)指針p被free或者delete之後,沒有置為NULL,讓人誤以為p是個合法的指針。上一節也提到了這個問題。

       (3)指針操作超越了變量的作用范圍。這種情況讓人防不勝防,示例程序如下:現在有這樣一個例子:

class A   
{   
public:   
   void Func(void){ cout << “Func of class A”<< endl; }   
};   
void Test(void)   
{   
   A *p;   
{   
   A  a;   
   p = &a; // 注意a 的生命期  
}   
   p->Func(); // p是“野指針”  
}

函數Test 在執行語句p->Func()時,對象a已經消失,而p是指向a的,所以p就成了“野指針”。

malloc/free和new/delete                                                            

       malloc與free是C++/C語言的標准庫函數,new/delete 是C++的運算符。它們都可用於申請動態內存和釋放內存。

       對於非內部數據類型的對象而言,光用maloc/free無法滿足動態對象的要求。對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。由於malloc/free 是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加於malloc/free。

       因此C++語言需要一個能完成動態內存分配和初始化工作的運算符new,以及一個能完成清理與釋放內存工作的運算符delete。注意new/delete不是庫函數。

       malloc/free和new/delete實現對象的動態內存管理的方法是不同的,下面是一個示例:

class Obj   
{   
public :   
   Obj(void){ cout << “Initialization”<< endl; }   
   ~Obj(void){ cout << “Destroy”<< endl; }   
   void  Initialize(void){ cout << “Initialization”<< endl; }   
   void Destroy(void){ cout << “Destroy”<< endl; }   
};  
void UseMallocFree(void)   
{   
   Obj *a = (obj *)malloc(sizeof(obj)); // 申請動態內存  
   a->Initialize();  // 初始化  
   //…  
   a->Destroy();  // 清除工作  
   free(a);    // 釋放內存  
}  
void UseNewDelete(void)   
{   
   Obj *a = new Obj; // 申請動態內存並且初始化  
   //…  
   delete a;  // 清除並且釋放內存  
}

       類Obj的函數Initialize模擬了構造函數的功能,函數Destroy模擬了析構函數的功能。函數UseMallocFree中,由於malloc/free不能執行構造函數與析構函數,必須調用成員函數Initialize和Destroy來完成初始化與清除工作。函數UseNewDelete則簡單得多。

       所以我們不要企圖用malloc/free來完成動態對象的內存管理,應該用new/delete。由於內部數據類型的“對象”沒有構造與析構的過程,對它們而言malloc/free和new/delete是等價的。

       既然new/delete的功能完全覆蓋了malloc/free,為什麼C++不把malloc/free淘汰出局呢?這是因為C++程序經常要調用C函數,而C程序只能用malloc/free管理動態內存。

       如果用free釋放“new創建的動態對象”,那麼該對象因無法執行析構函數而可能導致程序出錯。如果用delete釋放“malloc申請的動態內存”,理論上講程序不會出錯,但是該程序的可讀性很差。所以new/delete必須配對使用,malloc/free也一樣。

關於“內存耗盡”問題                                                               

       如果在申請動態內存時找不到足夠大的內存塊,malloc和new將返回NULL指針,宣告內存申請失敗。通常有三種方式處理“內存耗盡”問題。

       1)判斷指針是否為NULL,如果是則馬上用return語句終止本函數。例如:

void Func(void)   
{   
A *a = new A;   
if(a == NULL)   
{   
return;   
}   
…  
}

       2)判斷指針是否為NULL,如果是則馬上用exit(1)終止整個程序的運行。例如:

void Func(void)   
{   
A *a = new A;   
if(a == NULL)   
{   
cout << “Memory Exhausted”<< endl;   
exit(1);   
}   
…  
}

       3)為new和malloc設置異常處理函數。例如VisualC++可以用_set_new_hander函數為new設置用戶自己定義的異常處理函數,也可以讓malloc享用與new相同的異常處上述(1)(2)方式使用最普遍。如果一個函數內有多處需要申請動態內存,那麼方式(1)就顯得力不從心(釋放內存很麻煩),應該用方式(2)來處理

       很多人不忍心用exit(1),問:“不編寫出錯處理程序,讓操作系統自己解決行不行?”

       不行。如果發生“內存耗盡”這樣的事情,一般說來應用程序已經無藥可救。如果不用exit(1) 把壞程序殺死,它可能會害死操作系統。道理如同:如果不把歹徒擊斃,歹徒在老死之前會犯下更多的罪。

       雖然對於32 位以上的應用程序而言,無論怎樣使用malloc與new,幾乎不可能導致“內存耗盡”。但是:不加錯誤處理將導致程序的質量很差,千萬不可因小失大。

再說malloc/free                                                                      

       函數malloc的原型如下:

void * malloc(size_t size);

       用malloc申請一塊長度為length的整數類型的內存,程序如下:

int *p = (int *) malloc(sizeof(int) * length);

       我們應當把注意力集中在兩個要素上:“類型轉換”和“sizeof”。

        1)malloc返回值的類型是void *,所以在調用malloc時要顯式地進行類型轉換,將void * 轉換成所需要的指針類型。

        2)malloc 函數本身並不識別要申請的內存是什麼類型,它只關心內存的總字節數。

我們通常記不住int,float 等數據類型的變量的確切字節數。例如int變量在16位系統下是2個字節,在32位下是4個字節;而float變量在16位系統下是4個字節,在32位下也是4個字節。最好用以下程序作一次測試:

cout << sizeof(char) << endl;   
cout << sizeof(int) << endl;   
cout << sizeof(unsigned int) << endl;   
cout << sizeof(long) << endl;   
cout << sizeof(unsigned long) << endl;   
cout << sizeof(float) << endl;   
cout << sizeof(double) << endl;   
cout << sizeof(void *) << endl;

       在malloc的“()”中使用sizeof運算符是良好的風格,但要當心有時我們會昏了頭,寫出p = malloc(sizeof(p))這樣的程序來。

       3)函數free的原型如下:

void free( void * memblock );

       為什麼free函數不象malloc函數那樣復雜呢?這是因為指針p的類型以及它所指的內存的容量事先都是知道的,語句free(p)能正確地釋放內存。如果p是NULL指針,那麼free對p無論操作多少次都不會出問題。如果p不是NULL指針,那麼free對p連續操作兩次就會導致程序運行錯誤。

再說new/delete                                                                       

       運算符new使用起來要比函數malloc簡單得多,例如:

int *p1 = (int *)malloc(sizeof(int) * length);   
int *p2 = new int[length];

       這是因為new內置了sizeof、類型轉換和類型安全檢查功能。對於非內部數據類型的對象而言,new在創建動態對象的同時完成了初始化工作。如果對象有多個構造函數,那麼new的語句也可以有多種形式。例如

class Obj   
{   
public :   
Obj(void);   // 無參數的構造函數  
Obj(int x); // 帶一個參數的構造函數   
…  
}   
void Test(void)   
{   
Obj *a = new Obj;   
Obj *b = new Obj(1); // 初值為1   
…  
delete a;   
delete b;   
}

     如果用new創建對象數組,那麼只能使用對象的無參數構造函數。例如

Obj *objects = new Obj[100]; // 創建100個動態對象

不能寫成

Obj *objects = new Obj[100](1);

在用delete釋放對象數組時,留意不要丟了符號‘[]’。例如

delete []objects; // 正確的用法  
delete objects;  // 錯誤的用法

後者相當於delete objects[0],漏掉了另外99個對象

總結                                                                                   

       知識點在上已經一一敘述過了,這裡總結兩句話:

              1)越是怕指針,就越要使用指針。不會正確使用指針,肯定算不上是合格的程序員。

              2)必須養成“使用調試器逐步跟蹤程序”的習慣,只有這樣才能發現問題的本質。

我是天王蓋地虎的分割線                                                             

 

 

參考:http://blog.csdn.net/han_xiaoyang/article/details/10676931


什是內存管理

內存管理,是指軟件運行時對計算機內存資源的分配和使用的技術。其最主要的目的是如何高效,快速的分配,並且在適當的時候釋放和回收內存資源。一個執行中的程式,譬如網頁浏覽器在個人電腦或是圖靈機(Turing machine)裡面,為一個行程將資料轉換於真實世界及電腦內存之間,然後將資料存於電腦內存內部(在計算機科學,一個程式是一群指令的集合,一個行程是電腦在執行中的程式)。一個程式結構由以下兩部分而成:“本文區段”,也就是指令存放,提供CPU使用及執行; “資料區段”,儲存程式內部本身設定的資料,例如常數字串。

內存可以通過許多媒介實現,例如磁帶或是磁盤,或是小陣列容量的微芯片。 從1950年代開始,計算機變的更復雜,它內部由許多種類的內存組成。內存管理的任務也變的更加復雜,甚至必須在一台機器同時執行多個進程。
虛擬內存是內存管理技術的一個極其實用的創新。它是一段程序(由操作系統調度),持續監控著所有物理內存中的代碼段、數據段,並保證他們在運行中的效率以及可靠性,對於每個用戶層(user-level)的進程分配一段虛擬內存空間。當進程建立時,不需要在物理內存件之間搬移數據,數據儲存於磁盤內的虛擬內存空間,也不需要為該進程去配置主內存空間,只有當該進程被被調用的時候才會被加載到主內存。
可以想像一個很大的程序,當他執行時被操作系統調用,其運行需要的內存數據都被存到磁盤內的虛擬內存,只有需要用到的部分才被加載到主內存內部運行。
 

什是內存管理

內存管理,是指軟件運行時對計算機內存資源的分配和使用的技術。其最主要的目的是如何高效,快速的分配,並且在適當的時候釋放和回收內存資源。一個執行中的程式,譬如網頁浏覽器在個人電腦或是圖靈機(Turing machine)裡面,為一個行程將資料轉換於真實世界及電腦內存之間,然後將資料存於電腦內存內部(在計算機科學,一個程式是一群指令的集合,一個行程是電腦在執行中的程式)。一個程式結構由以下兩部分而成:“本文區段”,也就是指令存放,提供CPU使用及執行; “資料區段”,儲存程式內部本身設定的資料,例如常數字串。

內存可以通過許多媒介實現,例如磁帶或是磁盤,或是小陣列容量的微芯片。 從1950年代開始,計算機變的更復雜,它內部由許多種類的內存組成。內存管理的任務也變的更加復雜,甚至必須在一台機器同時執行多個進程。
虛擬內存是內存管理技術的一個極其實用的創新。它是一段程序(由操作系統調度),持續監控著所有物理內存中的代碼段、數據段,並保證他們在運行中的效率以及可靠性,對於每個用戶層(user-level)的進程分配一段虛擬內存空間。當進程建立時,不需要在物理內存件之間搬移數據,數據儲存於磁盤內的虛擬內存空間,也不需要為該進程去配置主內存空間,只有當該進程被被調用的時候才會被加載到主內存。
可以想像一個很大的程序,當他執行時被操作系統調用,其運行需要的內存數據都被存到磁盤內的虛擬內存,只有需要用到的部分才被加載到主內存內部運行。
 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved