一個由C/C++編譯的程序占用的內存分為以下幾個部分:
1、棧區(stack):又編譯器自動分配釋放,存放函數的參數值,局部變量的值等,其操作方式類似於數據結構的棧。
2、堆區(heap):一般是由程序員分配釋放,若程序員不釋放的話,程序結束時可能由OS回收,值得注意的是他與數據結構的堆是兩回事,分配方式倒是類似於數據結構的鏈表。
3、全局區(static):也叫靜態數據內存空間,存儲全局變量和靜態變量,全局變量和靜態變量的存儲是放一塊的,初始化的全局變量和靜態變量放一塊區域,沒有初始化的在相鄰的另一塊區域,程序結束后由系統釋放。
4、文字常量區:常量字符串就是放在這里,程序結束后由系統釋放。
5、程序代碼區:存放函數體的二進制代碼。
一、棧空間
1.1 自動釋放內存無需碼農操作
我們在程序中所定義的定義的局部變量int、局部數組等都是存儲在棧空間中。棧空間具有一個鮮明的特點:函數內定義的變量出了函數范圍,其所占用的內存空間自動釋放。但是,棧空間的尺寸有最大限制,不適合分配大空間使用;
所以,因為棧空間出了函數范圍就釋放,所以不適合要給其他地方使用的內存需求。其最大的好處就在於:不用程序員手動釋放內存。
1.2 不要把局部變量的指針做為返回值返回
首先,我們來看看下面一段代碼,其中getData函數返回了一個int數組類型的指針,而getData2函數返回了另一個int數組類型的指針:
int *getData() { int nums[10]={1,2,3,4,5,6,7,8}; return nums; } int *getData2() { int aaa[10]={8,7,6,5,4,3,2,1}; return aaa; }
我們在main函數中首先調用getData函數,看看能否取得nums數組的前三個元素:
int main(int argc, char *argv[]) { int *nums = getData(); printf("%d,%d,%d\n",nums[0],nums[1],nums[2]); return 0; }
運行結果如下圖所示,我們發現是OK的,原來可以將局部變量的指針作為返回值返回呢!
但是,如果我們在調用getData函數之后,又調用了getData2函數呢,這時還能正確地打印nums數組嗎?看看下面的執行順序:
int main(int argc, char *argv[]) { int *nums = getData(); getData2(); printf("%d,%d,%d\n",nums[0],nums[1],nums[2]); return 0; }
在打印之前,我們又執行了getData2函數,那么運行結果呢,看看下圖吧:
這時,突然覺得,憂傷爆了!剛剛都還是好的啊!
那么,問題來了,這是為什么呢?我們剛剛提到了,棧是由系統自動分配和釋放,函數內部的局部變量的生命周期就只是在函數周期內,只要函數執行完畢,那么其內部的局部變量的生命周期也就結束了。於是,當我們執行完第一句代碼后,nums指針所指向的數組的那一塊內存區域可能就已經被釋放了,但是數據還未清理也就是還留在那兒。但是,當我們執行完第二句代碼后,在getData2函數中又定義了一個數組aaa,它又將剛剛釋放的棧空間內存占用了,於是nums所指向的這塊區域就是aaa了。當執行完第二句代碼,aaa又被釋放了,但是其數據還在那里並未清除,也就是我們前面幾篇提到的臟內存區域。所以,最后顯示的就是8,7,6而不是1,2,3了。
二、堆空間
2.1 技術控都喜歡開手動檔汽車
剛剛提到的棧空間最大的優點就是棧空間出了函數范圍就釋放,不需要程序員手動釋放,就像自動擋汽車一樣,都不用我們去加減檔變速。但是,如果我們向自己控制內存的分配呢?這時候,就可以使用堆空間來存儲,堆空間可以存儲棧空間無法存儲的大內存。這里,我們可以借助malloc函數在堆空間中分配一塊指定大小的內存,用完之后,調用free函數及時釋放內存。
// malloc(要分配的字節數) int *nums = (int*)malloc(sizeof(int)*10); nums[0]=1; nums[1]=8; free(nums);
需要注意的是:在malloc函數中需要指定要分配的內存所占用的字節大小。
2.2 函數返回指針的幾種解決辦法
(1)在方法內malloc,用完了由調用者free
這里我們可以結合malloc和free來解決我們在棧空間中所遇到的問題,重寫上面的代碼如下:
int *getData() { int *nums = (int*)malloc(sizeof(int)*10); nums[0]=1; nums[1]=8; nums[2]=3; return nums; } int *getData2() { int *nums = (int*)malloc(sizeof(int)*10); nums[0]=2; nums[1]=7; nums[2]=5; return nums; } int main(int argc, char *argv[]) { int *numsptr = getData(); int *numsptr2 = getData2(); // numptr[1]等價於*(numptr+1) printf("%d,%d,%d\n",numsptr[0],numsptr[1],numsptr[2]); // 不要忘記釋放內存 free(numsptr); free(numsptr2); return 0; }
這里我們將所有要返回的指針都改為了使用malloc動態分配的,在main函數中調用free將內存手動釋放掉,來看看運行結果:
這下輸出的還是getData函數返回的指針所指向的內存區域的數據,沒有出現交叉影響,完美!
(2)把局部變量定義為static
char *getStr() { static char strs[]="afsafdasfdsdfsaddafafafasdfadfs"; return strs; } int main(int argc, char *argv[]) { char* strsptr = getStr(); return 0; }
由本文開篇可知,除了棧空間和堆空間,還有一塊全局區,它直到程序結束后才會釋放。
But,需要注意的是:不適合於多線程調用,如果想保存返回內容,你需要調用者盡快復制一份。
(3)(推薦)由調用者分配內存空間,只是把指針發給函數,函數內部把數據拷貝到內存中
這里怎么來理解呢,也就是三個步驟,第一步:由調用者分配內存空間;第二步:把指針傳遞給函數;第三步:函數內部把數據拷貝到內存中。下面我們通過一個小案例:從文件名分析文件名和擴展名,來看看這三個步驟怎么來實現。
// Step3:函數內部把數據拷貝到內存中 void parseFileName(char* filename,char* name,char* ext) { char *ptr = filename; while(*ptr != '\0') { ptr++; } // 記錄結尾的指針 char *endPtr = ptr; //ptr移動到了字符串的結尾,再把ptr移動到"."的位置 while(*ptr != '.') { ptr--; } // 兩個指針相減表示兩個指針相隔的元素的個數 memcpy(name,filename,(ptr-filename)*sizeof(char)); memcpy(ext,ptr+1,(endPtr-ptr)*sizeof(char)); } int main(int argc, char *argv[]) { // Step1:由調用者分配內存空間 char str[] = "[TK-300]美.女.avi"; char name[20] = {0}; char ext[20] = {0}; // Step2:只是把指針傳遞給函數 parseFileName(str,name,ext); printf("解析完成:\n"); printf("文件名:%s,后綴:%s\n",name,ext); return 0; }
這種方法避免了函數返回指針,程序員手動分配的內存都是在棧空間中,然后函數內部處理后再將經過邏輯處理后的數據存儲到棧空間中的指定區域內,最后main函數中再訪問修改后的內存區域。這里的運行結果如下圖所示:
雖然這個文件名有點邪惡,但是功能還是完成了,哎喲,不錯哦!
參考資料
如鵬網,《C語言也能干大事(第三版)》