理解、正確運(yùn)用計(jì)算機(jī)內(nèi)存
引言
在最近學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)和與同學(xué)交流問(wèn)題過(guò)程中士嚎,發(fā)現(xiàn)了一個(gè)極具普遍性的問(wèn)題呜魄,那就是為什么把一些代碼塊獨(dú)立出來(lái)做成一個(gè)函數(shù)就會(huì)出現(xiàn)問(wèn)題,而不做成函數(shù)反而沒(méi)問(wèn)題莱衩?難道這是要我們反對(duì)使用函數(shù)嗎爵嗅?不! 不是這樣的笨蚁,在閱讀完相關(guān)資料后睹晒,我發(fā)現(xiàn)其實(shí)是同學(xué)們對(duì)內(nèi)存空間的理解不夠,說(shuō)通俗點(diǎn)就是不知道內(nèi)存空間長(zhǎng)啥樣括细。那接下來(lái)我將給大家講解一下幾種對(duì)內(nèi)存的錯(cuò)誤操作以及幾種合理地解決方案伪很,從而讓大家對(duì)內(nèi)存空間有更好的理解。當(dāng)然以下的理解或多或少敘述上有失嚴(yán)謹(jǐn)?shù)牡胤椒艿ィ@些都是基于我自己對(duì)與內(nèi)存空間的理解锉试,希望讀者諒解并指出。
內(nèi)存空間
一般筆記本的內(nèi)存(RAM)有4G或8G览濒。它分為5大區(qū)域呆盖,分別是代碼區(qū)、全局?jǐn)?shù)據(jù)空間匾七、堆空間絮短、棧空間昨忆、內(nèi)核空間丁频。它們的圖例如下(以4G內(nèi)存為例):
棧與堆
我們從最常聽(tīng)到的棧空間與堆空間開(kāi)始邑贴。堆空間和椣铮空間統(tǒng)稱(chēng)為動(dòng)態(tài)儲(chǔ)存空間,它們分擔(dān)著儲(chǔ)存程序運(yùn)行時(shí)產(chǎn)生的變量的任務(wù)拢驾。就椊贝牛空間來(lái)說(shuō),可以這樣理解:函數(shù)先運(yùn)行main函數(shù)繁疤,則先把main函數(shù)放進(jìn)椏空間秕狰,同時(shí)把main函數(shù)里面聲明的變量全部包含在其中,當(dāng)main函數(shù)結(jié)束時(shí)(當(dāng)然也是程序結(jié)束時(shí))躁染,則把main函數(shù)拿出棧(此時(shí)顯然除了main函數(shù)其他函數(shù)都已結(jié)束)鸣哀,并同時(shí)把主函數(shù)里面的變量“帶走”。所謂“帶走”就是說(shuō)在后續(xù)操作中就再也不能使用這些變量了吞彤,就像下面的例子我衬,在調(diào)用完Max函數(shù)后printf("%d",max);是錯(cuò)誤的:
#include <stdio.h>
int Max(int a,int b){
int max;
max=a>b?a:b;
return max;
}
int main(void){
int a,b,mmax;
scanf("%d%d",&a,&b);
mmax=Max(a,b);
printf("%d",mmax);
return 0;
}
當(dāng)程序運(yùn)行時(shí),必然先運(yùn)行main函數(shù)饰恕,于是先把main函數(shù)放進(jìn)椖痈幔空間,同時(shí)把main函數(shù)里面聲明的變量全部包含在其中埋嵌。當(dāng)程序進(jìn)行到調(diào)用Max函數(shù)時(shí)破加,又把Max函數(shù)壓入棧空間莉恼,同時(shí)把Max函數(shù)里面聲明的變量全部包含在其中拌喉。以次類(lèi)推,如果是在調(diào)用函數(shù)A時(shí)俐银,在A(yíng)函數(shù)里面又調(diào)用B函數(shù)尿背,則棧空間的堆放順序則是main函數(shù)在棧底捶惜,接著是A函數(shù)田藐,再者是B函數(shù)。如果是在調(diào)用完A函數(shù)后吱七,main函數(shù)再調(diào)用B函數(shù)汽久,則棧空間的操作方式是main函數(shù)先壓入棧底踊餐,再A函數(shù)壓入棧景醇,而后A函數(shù)拿出棧,最后B函數(shù)再壓入棧吝岭。于是乎三痰,我們就可以理解一類(lèi)函數(shù)的錯(cuò)誤所在了。如下為鏈?zhǔn)骄€(xiàn)性表的插入元素操作函數(shù):
#include <stdio.h>
#include<stdlib.h>
#include<time.h>
typedef struct NODE {
struct NODE *next;
int value;
} Node;
typedef enum { ERROR = 0, OK = 1 } Status;
Status Insert1(Node **ppLink,int newValue){
Node new, *current;
new.value = newValue;
while(current=*ppLink,current!=NULL&¤t->value<newValue){
ppLink = ¤t->next;
}
*ppLink = new;
new.next = current;
return OK;
}
初看起來(lái)我們好像成功地將新元素插入鏈表之中窜管。但是!結(jié)果恰恰相反获搏。我們使得鏈表斷截了失乾,因?yàn)槲覀僆nsert函數(shù)里面的new是局部變量3N酢@俣睢募壕!所以在函數(shù)結(jié)束時(shí)new所占用的棧內(nèi)存將被銷(xiāo)毀,于是乎鏈表就被斷了4汀!以下是錯(cuò)誤運(yùn)用此函數(shù)導(dǎo)致的錯(cuò)誤輸出結(jié)果(錯(cuò)誤的程序):
94 21 11 99 37 68 19 62 61 10//亂序鏈表
10 11 19 21 37 61 62 68 94 99//排序好的鏈表
10 11 19 10 0//錯(cuò)誤插入函數(shù)(試圖插入新元素20)導(dǎo)致的結(jié)果
20 24 32 5 44 17 72 26 99 32//亂序鏈表
5 17 20 24 26 32 32 44 72 99//排序好的鏈表
5 17 32 0//錯(cuò)誤插入函數(shù)(試圖插入新元素20)導(dǎo)致的結(jié)果
46 96 22 19 63 26 82 24 3 36//亂序鏈表
3 19 22 24 26 36 46 63 82 96//排序好的鏈表
3 19 36 0//錯(cuò)誤插入函數(shù)(試圖插入新元素20)導(dǎo)致的結(jié)果
從以上幾個(gè)輸出結(jié)果可以看出錯(cuò)誤插入函數(shù)導(dǎo)致的結(jié)果還不僅僅是將鏈表斷開(kāi)那么簡(jiǎn)單,其結(jié)果是很復(fù)雜的、莫名奇妙的。那么如何解決這個(gè)問(wèn)題呢凿试?国觉?接著我們就將介紹堆空間地用法:
查看網(wǎng)上對(duì)棧與堆的區(qū)別與聯(lián)系請(qǐng)戳這里傲醉。總而言之,結(jié)合前面的內(nèi)存分布圖,我們可以這樣理解堆空間:可以簡(jiǎn)單地理解為malloc區(qū)沪羔,也即保存malloc函數(shù)占用的內(nèi)存溉委,用free釋放。若程序員不釋放的話(huà)越除,程序結(jié)束時(shí)可能由OS(操作系統(tǒng))回收。
所以,malloc出來(lái)的內(nèi)存不會(huì)因?yàn)楹瘮?shù)結(jié)束而消失。因而只要將上Insert函數(shù)稍加改變就能夠使程序得出理想的結(jié)果(只要將Node new改成Node *new然后后面再malloc一塊內(nèi)存就行了):
//malloc版本
Status Insert1(Node **ppLink,int newValue){//可重復(fù)調(diào)用
Node *new, *current;
new = (Node *)malloc(sizeof(Node));
if(new==NULL){
return ERROR;
}
new->value = newValue;
while(current=*ppLink,current!=NULL&¤t->value<newValue){
ppLink = ¤t->next;
}
*ppLink = new;
new->next = current;
return OK;
}
以下是程序運(yùn)行結(jié)果:
66 14 19 69 33 51 51 95 24 4//亂序鏈表
4 14 19 24 33 51 51 66 69 95//排序好的鏈表
4 14 19 20 24 33 51 51 66 69 95//成功將新元素20插入
到這里糕伐,或許還有同學(xué)對(duì)此嗤之以鼻:你說(shuō)的我都會(huì)呀!難道就這樣就能完全理解并運(yùn)用內(nèi)存了嗎?
問(wèn)的好澳骤,所以說(shuō)對(duì)于上述Insert函數(shù)地修改方案絕對(duì)不止堆空間方案一種。我們還可以運(yùn)用靜態(tài)空間舍哄。
bss區(qū)與data區(qū)
其實(shí)這兩個(gè)區(qū)儲(chǔ)存的變量類(lèi)型完全相同宴凉,都是儲(chǔ)存全局變量和靜態(tài)變量的內(nèi)存,所以我們大可把兩者看成同一塊區(qū)域丧靡。但是從很淺的方面講舟山,它們有一個(gè)很大的不同之處拆融,那就是bss區(qū)只儲(chǔ)存未初始化或初始化為0的全局變量和靜態(tài)變量,而data區(qū)則儲(chǔ)存 已初始化的全局變量和靜態(tài)變量但是除了const型的已初始化的全局變量镜豹。講到這里泰讽,大家或許迫不及待地想運(yùn)用靜態(tài)變量解決上述Insert函數(shù)的問(wèn)題了。那么如何運(yùn)用呢?因?yàn)殪o態(tài)空間也不屬于棧區(qū)菇绵,所以當(dāng)調(diào)用完一個(gè)函數(shù)時(shí)肄渗,函數(shù)里面申明的靜態(tài)變量是不會(huì)隨之銷(xiāo)毀的。于是乎我們得出將一個(gè)新元素插入鏈表的函數(shù):
//static版本
Status Insert1(Node **ppLink,int newValue){//只能調(diào)用一次
static Node new;
Node *current;
new.value = newValue;
while(current=*ppLink,current!=NULL&¤t->value<newValue){
ppLink = ¤t->next;
}
*ppLink = new;
new.next = current;
return OK;
}
以下是程序運(yùn)行結(jié)果:
29 70 61 85 68 67 72 87 59 95//亂序鏈表
29 59 61 67 68 70 72 85 87 95//排序好的鏈表
20 29 59 61 67 68 70 72 85 87 95//成功將新元素20插入
但是咬最,別高興地太早翎嫡。細(xì)心的同學(xué)可能注意到了malloc版本有“可重復(fù)調(diào)用”的字樣而static版本卻是“只能調(diào)用一次”。那么為什么會(huì)這樣呢永乌?這里就必須講清楚動(dòng)態(tài)空間與靜態(tài)空間的區(qū)別了惑申。一般說(shuō)來(lái),靜態(tài)空間的變量?jī)?nèi)存在程序一開(kāi)始就分配好了翅雏,在程序運(yùn)行時(shí)不能夠再聲明同一類(lèi)靜態(tài)變量多次圈驼。也就是說(shuō),當(dāng)程序第二次調(diào)用static版本的函數(shù)時(shí)望几,再次遇到
static Node new;
時(shí)绩脆,并不會(huì)再申請(qǐng)一塊靜態(tài)空間的內(nèi)存,而是對(duì)同一個(gè)new變量進(jìn)行操作橄抹。這也就為什么我們可以用static變量記錄調(diào)用函數(shù)的次數(shù)靴迫。
講到這里,我們又不得不提一下全局變量楼誓。關(guān)于這點(diǎn)最近有了一些感悟玉锌。有編程經(jīng)驗(yàn)以及參加過(guò)ACM程序設(shè)計(jì)比賽的同學(xué)們都知道,有時(shí)候我們需要申請(qǐng)一塊很大的內(nèi)存空間(如數(shù)組)來(lái)使用疟羹。但是在main函數(shù)里面申明一塊很大的內(nèi)存空間顯然不明智主守。因?yàn)閙ain函數(shù)里面的變量?jī)?nèi)存都是在棧空間里面的榄融,所以申請(qǐng)大內(nèi)存最好是全局變量参淫。以下是網(wǎng)上對(duì)此的解釋?zhuān)?/p>
全局變量在靜態(tài)存儲(chǔ)區(qū)分配內(nèi)存,局部變量是在棧上分配內(nèi)存空間的剃袍,這么大的數(shù)組放到棧上不溢出嗎黄刚?VC堆棧默認(rèn)是1M,int a[1000000]的大小是4*1000000民效,將近4M憔维,遠(yuǎn)遠(yuǎn)大于1M,編譯連接的時(shí)候不會(huì)有問(wèn)題畏邢,但運(yùn)行是堆棧溢出业扒,程序異常終止。如果你真的需要在堆棧上使用這么大的數(shù)組舒萎,那么可以在工程選項(xiàng)鏈接屬性里設(shè)置合適的堆棧大小程储。
但是作為一名程序員,我們往往會(huì)聽(tīng)到我們老師們或?qū)W長(zhǎng)學(xué)姐們或書(shū)上說(shuō)寫(xiě)程序時(shí)最好不要用全局變量。這就很有意思了章鲤,因?yàn)榇_實(shí)全局變量很容易出問(wèn)題摊灭。關(guān)于全局變量為什么危險(xiǎn)在這里我就不在贅述,讀者自行上網(wǎng)查找败徊,這里有一篇我上網(wǎng)查的比較容易理解的文章帚呼。那么,如果我一定要使用一塊大內(nèi)存空間但是又不能用全局變量皱蹦,我該怎么辦煤杀?仔細(xì)想想,不覺(jué)得想起了我的編程啟蒙老師張學(xué)沪哺,他對(duì)學(xué)生很?chē)?yán)格沈自,甚至可以說(shuō)有些“吝嗇”,但是如果上課真正地認(rèn)真做筆記辜妓,學(xué)習(xí)他的程序設(shè)計(jì)方法枯途,你會(huì)發(fā)現(xiàn)你受益無(wú)窮,而并不只是期末C語(yǔ)言能拿個(gè)高分而已籍滴。好了柔袁,接下來(lái)就是方法的講解:
我們的確可以在main函數(shù)里面申請(qǐng),但是异逐,我們要聲明的不是如下的樣子:
···
int main(void){
int a[1000000];
···
}
我們應(yīng)該在main函數(shù)里面聲明一個(gè)int型的指針,然后再使用malloc函數(shù)插掂。對(duì)灰瞻,相信有的讀者已經(jīng)理解其中的精妙之處,修改后的代碼長(zhǎng)這樣:
···
int main(void){
int *a;
a=(int*)malloc(1000000*sizeof(int));
if(a==NULL){
printf("申請(qǐng)內(nèi)存失敻ㄉ酝润!");
return 1;
}
···
}
如果已經(jīng)看懂了文章前面關(guān)于堆空間的描述,那么就不會(huì)有懷疑了璃弄。在修改后的版本中要销,a是一個(gè)指針,在main函數(shù)里面聲明一個(gè)指針變量顯然是完全夠用的夏块。接著我們?cè)诙芽臻g里面malloc一塊1000000*sizeof(int)個(gè)字節(jié)內(nèi)存疏咐,在讓a指向這塊內(nèi)存。如此脐供,我們就可以像操作數(shù)組那樣進(jìn)行操作了浑塞。還有一個(gè)好處就是我們也不怎么用擔(dān)心訪(fǎng)問(wèn)“數(shù)組”不夠用了,因?yàn)槿绻鹠alloc出來(lái)的區(qū)域用完了政己,我們還可以用realloc函數(shù)為“數(shù)組”加長(zhǎng)酌壕。
text區(qū)
text區(qū)又稱(chēng)ROTEXT區(qū),其中RO的意思是Read Only(只讀)。那么這塊內(nèi)存是什么卵牍?又有什么用處果港?text區(qū)分為只讀數(shù)據(jù)段和代碼段。其中前者為保存程序文件中字符串內(nèi)容的空間以及const型的全局變量糊昙。而代碼段也就是整個(gè)程序文件的代碼辛掠。在以下的代碼中可以更好的理解:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const int g_A = 10; //text區(qū)
int g_B = 20; //data區(qū)
static int g_C = 30; //data區(qū)
static int g_D; //BSS區(qū)
int g_E; //BSS區(qū)
char *p1; //BSS區(qū)
const char *ch[]={"Mon","Tus","Wed","Thu","Fri","Sat","Sun"};
//ch和Mon、Tus溅蛉、Wed公浪、Thu、Fri船侧、Sat欠气、Sun都在text區(qū)
int main( )
{
int local_A; //棧
int local_B; //棧
static int local_C = 0; //data區(qū)
static int local_D; //data區(qū)
char *p3 = "123456"; //123456在text區(qū),p3在棧上
p1 = (char *)malloc( 10 ); //堆镜撩,分配得來(lái)得10字節(jié)的區(qū)域在堆區(qū)
strcpy( p1, "123456" ); //123456{post.content}放在常量區(qū)预柒,編譯器可能會(huì)將它與p3所指向 的"123456"優(yōu)化成一塊
printf("hight address\n");
printf("-------------棧--------------\n");
printf( "棧, 局部變量, local_A, addr:0x%08x\n", &local_A );
printf( "棧, 局部變量,(后進(jìn)棧地址相對(duì)local_A低) local_B, addr:0x%08x\n", &local_B );
printf("-------------堆--------------\n");
printf( "堆, malloc分配內(nèi)存, p1, addr:0x%08x\n", p1 );
printf("------------BSS區(qū)------------\n");
printf( "BSS區(qū), 全局變量, 未初始化 g_E, addr:0x%08x\n", &g_E, g_E );
printf( "BSS區(qū), 靜態(tài)全局變量, 未初始化, g_D, addr:0x%08x\n", &g_D );
printf( "BSS區(qū), 靜態(tài)局部變量, 初始化, local_C, addr:0x%08x\n", &local_C);
printf( "BSS區(qū), 靜態(tài)局部變量, 未初始化, local_D, addr:0x%08x\n", &local_D);
printf("-----------data區(qū)------------\n");
printf( "data區(qū),全局變量, 初始化 g_B, addr:0x%08x\n", &g_B);
printf( "data區(qū),靜態(tài)全局變量, 初始化, g_C, addr:0x%08x\n", &g_C);
printf("-----------text區(qū)------------\n");
printf( "text區(qū),全局初始化變量, 只讀const, g_A, addr:0x%08x\n\n", &g_A);
printf("low address\n");
return 0;
}
以下是輸出結(jié)果:
hight address
-------------棧--------------
棧, 局部變量, local_A, addr:0x0062fe44
棧, 局部變量,(后進(jìn)棧地址相對(duì)local_A低) local_B, addr:0x0062fe40
-------------堆--------------
堆, malloc分配內(nèi)存, p1, addr:0x001e13e0
------------BSS區(qū)------------
BSS區(qū), 全局變量, 未初始化 g_E, addr:0x00407030
BSS區(qū), 靜態(tài)全局變量, 未初始化, g_D, addr:0x00407040
BSS區(qū), 靜態(tài)局部變量, 初始化, local_C, addr:0x00407044
BSS區(qū), 靜態(tài)局部變量, 未初始化, local_D, addr:0x00407048
-----------data區(qū)------------
data區(qū),全局變量, 初始化 g_B, addr:0x00403020
data區(qū),靜態(tài)全局變量, 初始化, g_C, addr:0x00403024
-----------text區(qū)------------
text區(qū),全局初始化變量, 只讀const, g_A, addr:0x00404348
low address
結(jié)語(yǔ)
emmmm,敲文字敲得老闊疼。不管怎樣我覺(jué)得多寫(xiě)總結(jié)(不是那種簡(jiǎn)單的筆記袁梗,而更像是給別人講解)可以讓我更好地深入理解知識(shí)宜鸯,這讓我受益匪淺。不管有沒(méi)有很多人看我的文章遮怜,我的學(xué)習(xí)目的已經(jīng)達(dá)到了淋袖,這是最讓我開(kāi)心的。當(dāng)然锯梁,能夠幫助對(duì)相關(guān)知識(shí)有問(wèn)題的同學(xué)那就更好了即碗。最后祭上我的新晉女神: