謹(jǐn)記
人生有兩條路佳遂,一天需要用心走营袜,叫做夢想;一條需要用腳走丑罪,叫做現(xiàn)實连茧。心走的太快,會迷路的巍糯;腳走的太快,會摔倒的客扎;心走的太慢祟峦,現(xiàn)實會蒼白;腳走的太慢徙鱼,夢不會高飛宅楞。人生的精彩,是心走得好袱吆,腳步剛好能跟上厌衙。掌控好你的心,讓它走正绞绒;加快你的步伐婶希,讓所有夢想生出美麗的翅膀。
前言
今天為大家?guī)淼氖荂語言里面的內(nèi)存管理的知識點蓬衡,這篇文章過后喻杈,我們C語言的大體基本知識就已經(jīng)介紹完了彤枢,那么下一篇文章開始,我講講解OC語法筒饰,也就是蘋果公司推出的Objective-C語言缴啡,這是蘋果應(yīng)用開發(fā)的語言,也歡迎大家閱讀瓷们,本篇文章是對C語言內(nèi)存管理的一個講解业栅,內(nèi)存的使用是程序設(shè)計中需要考慮的重要因素之一,這不僅由于系統(tǒng)內(nèi)存是有限的(尤其在嵌入式系統(tǒng)中)谬晕,而且內(nèi)存分配也會直接影響到程序的效率碘裕。因此,讀者要對C語言中的內(nèi)存管理固蚤,有個系統(tǒng)的了解娘汞。
內(nèi)存管理
在C語言中,定義了4個內(nèi)存區(qū)間:代碼區(qū)夕玩;全局變量與靜態(tài)變量區(qū)你弦;局部變量區(qū)即棧區(qū);動態(tài)存儲區(qū)燎孟,即堆區(qū)禽作。下面分別對這4個區(qū)進(jìn)行介紹。
① 代碼區(qū)揩页。代碼區(qū)中主要存放程序中的代碼旷偿,屬性是只讀的。
② 全局變量與靜態(tài)變量區(qū)爆侣。也稱為靜態(tài)存儲區(qū)域萍程。內(nèi)存在程序編譯的時候就已經(jīng)分配好,這塊內(nèi)存在程序的整個運行期間都存在兔仰。例如:全局變量茫负、靜態(tài)變量和字符串常量。分配在這個區(qū)域中的變量乎赴,當(dāng)程序結(jié)束時忍法,才釋放內(nèi)存。因此榕吼,經(jīng)常利用這樣的變量饿序,在函數(shù)間傳遞信息。
③ 棧區(qū)羹蚣。在棧上創(chuàng)建原探。在執(zhí)行函數(shù)時,函數(shù)內(nèi)局部變量的存儲單元都可以在棧上創(chuàng)
建,函數(shù)執(zhí)行結(jié)束時這些存儲單元自動被釋放踢匣。棧內(nèi)存分配運算內(nèi)置于處理器的指令集中告匠,效率很高,但是分配的內(nèi)存容量有限离唬。在linux系統(tǒng)中后专,通過命令“ulimit –s”,可以看到输莺,棧的容量為8192kbytes戚哎,即8M。
這種內(nèi)存方式嫂用,變量內(nèi)存的分配和釋放都自動進(jìn)行型凳,程序員不需要考慮內(nèi)存管理的問題,很方便使用嘱函。但缺點是甘畅,棧的容量有限制,且當(dāng)相應(yīng)的范圍結(jié)束時往弓,局部變量就不能在使用疏唾。
④ 堆區(qū)。有些操作對象只有在程序運行時才能確定函似,這樣編譯器在編譯時就無法為他們預(yù)先分配空間槐脏,只能在程序運行時分配,所以稱為動態(tài)分配撇寞。
比如:下面的結(jié)構(gòu)體定義:
struct employee
{
char name[8];
int age;
char gender;
float salary;
};
在該結(jié)構(gòu)體定義中顿天,員工的姓名是用字符數(shù)組來存儲。若員工的姓名由用戶輸入蔑担,則只有在用戶輸入結(jié)束后牌废,才能精確的知道,需要多少內(nèi)存啤握,在這種情況下鸟缕,使用動態(tài)內(nèi)存分配更合乎邏輯,應(yīng)該把結(jié)構(gòu)體的定義改成下面的形式:
struct employee
{
char *name;
int age;
char gender;
float salary;
};
動態(tài)分配內(nèi)存就是在堆區(qū)上分配恨统。程序在運行的時候用malloc申請任意多少的內(nèi)存,程序員自己負(fù)責(zé)在何時用free釋放內(nèi)存三妈。動態(tài)內(nèi)存的生存期由我們決定畜埋,使用非常靈活,但問題也最多畴蒲。
下面的這段程序說明了不同類型的內(nèi)存分配悠鞍。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*C語言中數(shù)據(jù)的內(nèi)存分配*/
int a = 0;
char *p1;
int main()
{
int b; /* b在棧 */
char s[] = "abc"; /* s在棧, "abc"在常量區(qū) */
char *p2; /* p2在棧 */
char *p3 = "123456"; /*"123456"在常量區(qū),p3在棧*/
static int c =0; /*可讀可寫數(shù)據(jù)段*/
p1 = (char *)malloc(10); /*分配得來的10個字節(jié)的區(qū)域在堆區(qū)*/
p2 = (char *)malloc(20); /*分配得來的20個字節(jié)的區(qū)域在堆區(qū)*/
/* 從常量區(qū)的“Hello World”字符串復(fù)制到剛分配到的堆區(qū) */
strcpy(p1, “Hello World");
return 0;
}
動態(tài)內(nèi)存的申請和分配
當(dāng)程序運行到需要一個動態(tài)分配的變量時咖祭,必須向系統(tǒng)申請取得堆中的一塊所需大小的存儲空間掩宜,用于存儲該變量。當(dāng)不再使用該變量時么翰,也就是它的生命結(jié)束時牺汤,要顯式釋放它所占用的存儲空間,這樣系統(tǒng)就能對該堆空間進(jìn)行再次分配浩嫌,做到重復(fù)使用有限的資源檐迟。下面將介紹動態(tài)內(nèi)存申請和釋放的函數(shù)。
malloc函數(shù)
在C語言中码耐,使用malloc函數(shù)來申請內(nèi)存追迟。函數(shù)原型如下:
#include <stdlib.h>
void *malloc(size_t size);
其中,參數(shù)size代表需要動態(tài)申請的內(nèi)存的字節(jié)數(shù)骚腥。若內(nèi)存申請成功敦间,函數(shù)返回申請到的內(nèi)存的起始地址,若申請失敗束铭,返回NULL廓块。使用該函數(shù)時,有下面幾點要注意:
(1)只關(guān)心申請內(nèi)存的大小纯露。該函數(shù)的參數(shù)剿骨,很簡單,只有申請內(nèi)存的大小埠褪,單位是字節(jié)浓利。
(2)申請的是一塊連續(xù)的內(nèi)存。該函數(shù)一定是申請一塊連續(xù)的區(qū)間钞速,可能申請到的內(nèi)存比實際申請的大贷掖。也可能申請不到,若申請失敗渴语,返回NULL苹威。讀者,一定記得寫出錯判斷驾凶。
(3)返回值類型是void *牙甫。函數(shù)的返回值是void *,不是某種具體類型的指針调违。讀者可以理解成窟哺,該函數(shù)只是申請內(nèi)存,對在內(nèi)存中存儲什么類型的數(shù)據(jù)技肩,沒有要求且轨。因此,返回值是void *。在實際編程中旋奢,根據(jù)實際情況泳挥,將void * 轉(zhuǎn)換成所需要的指針類型。
(4)顯示初始化至朗。注意屉符,堆區(qū)是不會自動在分配時做初始化的(包括清零),所以程序中需要顯式的初始化爽丹。
free函數(shù)
在堆區(qū)上分配的內(nèi)存筑煮,需要用free函數(shù)顯示釋放。函數(shù)原型如下:
#include <stdlib.h>
void free(void *ptr);
函數(shù)的參數(shù)ptr粤蝎,指的是需要釋放的內(nèi)存的起始地址真仲。該函數(shù)沒有返回值。使用該函數(shù)初澎,也有下面幾點需要注意:
(1)必須提供內(nèi)存的起始地址秸应。調(diào)用該函數(shù)時,必須提供內(nèi)存的起始地址碑宴,不能提供部分地址软啼,釋放內(nèi)存中的一部分是不允許的。因此延柠,必須保存好malloc返回的指針值祸挪,若丟失,則所分配的堆空間無法回收贞间,稱內(nèi)存泄漏贿条。
(2)malloc和free配對使用。編譯器不負(fù)責(zé)動態(tài)內(nèi)存的釋放增热,需要程序員顯示釋放整以。因此,malloc與free是配對使用的峻仇,避免內(nèi)存泄漏公黑。
示例代碼
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int *get_memory(int n){
int *p, i;
if ((p = (int *)malloc(n * sizeof(int))) == NULL){
printf("malloc error\n");
return p;
}
memset(p, 0, n * sizeof(int));
for (i = 0; i < n; i++)
p[i] = i+1;
return p;
}
int main(int argc, const char * argv[]) {
int n, *p, i;
printf("input n:");
scanf("%d", &n);
if ((p = get_memory(n)) == NULL)
return 0;
for (i = 0; i < n; i++)
printf("%d ", p[i]);
printf("\n");
free(p);
p = NULL;
return 0;
}
輸出結(jié)果:
input n:10
1 2 3 4 5 6 7 8 9 10
Program ended with exit code: 0
說明:該程序演示了動態(tài)內(nèi)存的標(biāo)準(zhǔn)用法。動態(tài)內(nèi)存的申請摄咆,通過一個指針函數(shù)來完成凡蚜。內(nèi)存申請時,判斷是否申請成功吭从,成功后朝蜘,對內(nèi)存初始化。在主調(diào)函數(shù)中影锈,動態(tài)內(nèi)存依然可以訪問芹务,不再訪問內(nèi)存時,用free函數(shù)釋放鸭廷。
3)不允許重復(fù)釋放枣抱。同一空間的重復(fù)釋放也是危險的,因為該空間可能已另分配辆床。在上面程序中佳晶,如果釋放堆空間兩次(連續(xù)調(diào)用兩次free(p)),會出現(xiàn)崩潰讼载,控制臺打印很多內(nèi)存指令轿秧。
(4)free只能釋放堆空間。像代碼區(qū)咨堤、全局變量與靜態(tài)變量區(qū)菇篡、棧區(qū)上的變量,都不需要程序員顯示釋放一喘,這些區(qū)域上的空間驱还,不能通過free函數(shù)來釋放,否則執(zhí)行時凸克,會出錯议蟆。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, const char * argv[]) {
int a[10] = {0};
free(a);
return 0;
}
輸出結(jié)果:
內(nèi)存管理(1624,0x10007f000) malloc: *** error for object 0x7fff5fbff820: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
這里程序運行后會報錯,直接會崩潰萎战,這里讀者自己去嘗試一下咐容。
野指針
提到野指針,前面我們說指針的時候蚂维,都已經(jīng)提過了戳粒,這里就簡單的在提一下。
野指針指的是指向“垃圾”內(nèi)存的指針鸟雏,不是NULL指針享郊。出現(xiàn)“野指針”主要有以下原因:
(1)指針變量沒有被初始化。指針變量和其它的變量一樣孝鹊,若沒有初始化炊琉,值是不確定的。也就是說又活,沒有初始化的指針苔咪,指向的是垃圾內(nèi)存,非常危險柳骄。
#include <stdio.h>
int main(int argc, const char * argv[]) {
int *p;
printf("%d\n", *p);
*p = 10;
printf("%d\n", *p);
return 0;
}
(2)指針p被free之后团赏,沒有置為NULL。free函數(shù)是把指針?biāo)赶虻膬?nèi)存釋放掉耐薯,使內(nèi)存成為了自由內(nèi)存舔清。但是丝里,該函數(shù)并沒有把指針本身的內(nèi)容清楚。指針仍指向已經(jīng)釋放的動態(tài)內(nèi)存体谒,這是很危險杯聚。
程序員稍有疏忽,會誤以為是個合法的指針抒痒。就有可能再通過指針去訪問動態(tài)內(nèi)存幌绍。實際上,這時的內(nèi)存已經(jīng)是垃圾內(nèi)存了故响。
關(guān)于野指針會造成什么樣的后果傀广,這是很難估計的。若內(nèi)存仍然是空閑的彩届,可能程序暫時正常運行伪冰;若內(nèi)存被再次分配,又通過野指針對內(nèi)存進(jìn)行了寫操作樟蠕,則原有的合法數(shù)據(jù)糜值,會被覆蓋,這時坯墨,野指針造成的影響將是無法估計的寂汇。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, const char * argv[]) {
int n = 5, *p, i;
if ((p = (int *)malloc(n * sizeof(int))) == NULL){
printf("malloc error\n");
return 0;
}
memset(p, 0, n * sizeof(int));
for (i = 0; i < n; i++){
p[i] = i+1;
printf("%d ", p[i]);
}
printf("\n");
printf("p=%p *p=%d\n", p, *p);
free(p);
printf("after free:p=%p *p=%d\n", p, *p);
*p = 100;
printf("p=%p *p=%d\n", p, *p);
return 0;
}
說明:該程序中,故意在執(zhí)行了“free(p)”之后捣染,通過野指針p對動態(tài)內(nèi)存進(jìn)行了讀寫骄瓣,程序正常執(zhí)行,也在預(yù)料之中耍攘。前面已經(jīng)分析過榕栏,內(nèi)存釋放后,若繼續(xù)訪問甚至修改蕾各,后果是不可預(yù)料的扒磁。
(3)指針操作超越了變量的作用范圍。指針操作時式曲,由于邏輯上的錯誤妨托,導(dǎo)致指針訪問了非法內(nèi)存,這種情況讓人防不勝防吝羞,只能依靠程序員好的編碼風(fēng)格兰伤,已及扎實的基本功。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, const char * argv[]) {
int a[5] = {1, 9, 6, 2, 10}, *p, i, n;
n = sizeof(a) / sizeof(n);
p = a;
for (i = 0; i <= n; i++){
printf("%d ", *p);
p++;
}
printf("\n");
*p = 100;
printf("*p=%d\n", *p);
return 0;
}
說明:該程序故意出了兩個錯誤钧排,一是for循環(huán)的條件“i <= n”敦腔,p指針指向了數(shù)組以外的空間。二是“*p = 100”恨溜,對非法內(nèi)存進(jìn)行了寫操作符衔。
(4)不要返回指向棧內(nèi)存的指針找前。在函數(shù)中,詳細(xì)介紹了指針函數(shù)判族,指針函數(shù)會返回一個指針纸厉。在主調(diào)函數(shù)中,往往會通過返回的指針五嫂,繼續(xù)訪問指向的內(nèi)存。因此肯尺,指針函數(shù)不能返回棧內(nèi)存的起始地址沃缘,因為棧內(nèi)存在函數(shù)結(jié)束時會被釋放。
堆和棧的區(qū)別
1.申請方式
棧(stack)是由系統(tǒng)自動分配的则吟。例如槐臀,聲明函數(shù)中一個局部變量“int b;”,那么系統(tǒng)自動在棧中為b開辟空間氓仲。堆(heap)需要程序員自己申請水慨,并在申請時指定大小。使用C語言中的malloc函數(shù)的例子如下所示敬扛。
p1 = (char *)malloc(10);
2.申請后系統(tǒng)的響應(yīng)
堆在操作系統(tǒng)中有一個記錄空閑內(nèi)存地址的鏈表晰洒。當(dāng)系統(tǒng)收到程序的申請時,系統(tǒng)就會開始遍歷該鏈表啥箭,尋找第一個空間大于所申請空間的堆節(jié)點谍珊,然后將該節(jié)點從空閑節(jié)點鏈表中刪除,并將該節(jié)點的空間分配給程序急侥。另外砌滞,對于大多數(shù)系統(tǒng),會在這塊內(nèi)存空間中的首地址處記錄本次分配的大小坏怪。這樣贝润,代碼中的刪除語句才能正確地釋放本內(nèi)存空間。如果找到的堆節(jié)點的大小與申請的大小不相同铝宵,系統(tǒng)會自動地將多余的那部分重新放入空閑鏈表中打掘。
只有棧的剩余空間大于所申請空間,系統(tǒng)才為程序提供內(nèi)存鹏秋,否則將報異常胧卤,提示棧溢出。
3.申請大小的限制
堆是向高地址擴展的數(shù)據(jù)結(jié)構(gòu)拼岳,是不連續(xù)的內(nèi)存區(qū)域枝誊。這是由于系統(tǒng)用鏈表來存儲的空閑內(nèi)存地址,地址是不連續(xù)的惜纸,而鏈表的遍歷方向是由低地址向高地址叶撒。堆的大小受限于計算機系統(tǒng)中有效的虛擬內(nèi)存绝骚,因此堆獲得的空間比較靈活,也比較大祠够。
棧是向低地址擴展的數(shù)據(jù)結(jié)構(gòu)压汪,是一塊連續(xù)的內(nèi)存區(qū)域。因此古瓤,棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的止剖,如果申請的空間超過棧的剩余空間時,將提示棧溢出落君,因此穿香,能從棧獲得的空間較小。
4.申請速度的限制
堆是由malloc等語句分配的內(nèi)存绎速,一般速度比較慢皮获,而且容易產(chǎn)生內(nèi)存碎片,不過用起來很方便纹冤。棧由系統(tǒng)自動分配洒宝,速度較快,但程序員一般無法控制萌京。
5.堆和棧中的存儲內(nèi)容
堆一般在堆的頭部用一個字節(jié)存放堆的大小雁歌,堆中的具體內(nèi)容由程序員安排。
在調(diào)用函數(shù)時知残,第一個進(jìn)棧的是函數(shù)調(diào)用語句的下一條可執(zhí)行語句的地址将宪,然后是函數(shù)的各個參數(shù),在大多數(shù)的C語言編譯器中橡庞,參數(shù)是由右往左入棧的较坛,然后是函數(shù)中的局部變量。當(dāng)本次函數(shù)調(diào)用結(jié)束后扒最,局部變量先出棧丑勤,然后是參數(shù),最后棧頂指針指向最開始的存儲地址吧趣,也就是調(diào)用該函數(shù)處的下一條指令法竞,程序由該點繼續(xù)運行。
C語言關(guān)鍵字
C語言關(guān)鍵字volatile
C語言關(guān)鍵字volatile(注意它是用來修飾變量而不是上面介紹的volatile)表明某個變量的值可能隨時被外部改變(例如强挫,外設(shè)端口寄存器值)岔霸,因此對這些變量的存取不能緩存到寄存器,每次使用時需要重新讀取俯渤。
該關(guān)鍵字在多線程環(huán)境下經(jīng)常使用呆细,因為在編寫多線程的程序時,同一個變量可能被多個線程修改八匠,而程序通過該變量同步各個線程絮爷。對于C語言編譯器來說趴酣,它并不知道這個值會被其他線程修改,自然就把它緩存到寄存器里面坑夯。volatile的本意是指這個值可能會在當(dāng)前線程外部被改變岖寞,此時編譯器知道該變量的值會在外部改變,因此每次訪問該變量時會重新讀取柜蜈。這個關(guān)鍵字在外設(shè)接口編程中經(jīng)常被使用仗谆。
總結(jié)
本篇文章簡單的介紹C語言中的內(nèi)存管理,希望讀者掌握淑履。
結(jié)尾
希望讀者真誠的對待每一件事情隶垮,每天都能學(xué)到新的知識點,要記住鳖谈,認(rèn)識短暫,開心也是過一天阔涉,不開心也是一天缆娃,無所事事也是一天,小小收獲也是一天瑰排,歡迎收藏和點贊贯要、喜歡。最后送讀者一句話:你的路你自己選擇椭住。