你一直以為你操作的是真實(shí)物理內(nèi)存,實(shí)際上并不是,你操作的只是操作系統(tǒng)為你分配的資格虛擬地址,但這并不意味著我們可以無(wú)限使用內(nèi)存,那內(nèi)存賣(mài)那么貴干嘛,實(shí)際上存儲(chǔ)數(shù)據(jù)的還是物理內(nèi)存,只不過(guò)在操作系統(tǒng)這個(gè)中介的介入情況下,不同程序窗口(可以是相同程序)可以共享使用同一塊內(nèi)存區(qū)域,一旦某個(gè)傻大個(gè)程序的使用讓物理內(nèi)存不足了,我們就會(huì)把某些沒(méi)用到的數(shù)據(jù)寫(xiě)到你的硬盤(pán)上去,之后再使用時(shí),從硬盤(pán)讀回。這個(gè)特性會(huì)導(dǎo)致什么呢?假設(shè)你在Windows上使用了多窗口,打開(kāi)了兩個(gè)相同的程序:
...
int stay_here;
char tran_to_int[100];
printf("Address: %p\n", &stay_here);
fgets(tran_to_int, sizeof(tran_to_int), stdin);
sscanf(tran_to_int, "%d", &stay_here);
for(;;)
{
printf("%d\n", stay_here);
getchar();
++stay_here;
}
...
對(duì)此程序(引用前橋和彌的例子),每敲擊一次回車(chē),值加1。當(dāng)你同時(shí)打開(kāi)兩個(gè)該程序時(shí),你會(huì)發(fā)現(xiàn),兩個(gè)程序的stay_here
都是在同一個(gè)地址,但對(duì)它進(jìn)行分別操作時(shí),產(chǎn)生的結(jié)果是獨(dú)立的!這在某一方面驗(yàn)證了虛擬地址的合理性。虛擬地址的意義就在于,即使一個(gè)程序出現(xiàn)了錯(cuò)誤,導(dǎo)致所在內(nèi)存完蛋了,也不會(huì)影響到其他進(jìn)程。對(duì)于程序中部的兩個(gè)讀取語(yǔ)句,是一種理解C語(yǔ)言輸入流本質(zhì)的好例子,建議查詢用法,這里稍微解釋一下:
通俗地說(shuō),fgets將輸入流中由調(diào)用起,stdin
輸入的東西存入起始地址為tran_to_int
的地方,并且最多讀取sizeof(tran_to_int)
個(gè),并在后方sscanf
函數(shù)中將剛才讀入的數(shù)據(jù)按照%d
的格式存入stay_here
,這就是C語(yǔ)言一直在強(qiáng)調(diào)的流概念的意義所在,這兩個(gè)語(yǔ)句組合看起來(lái)也就是讀取一個(gè)數(shù)據(jù)這么簡(jiǎn)單,但是我們要知道一個(gè)問(wèn)題,一個(gè)關(guān)于scanf
的問(wèn)題
scanf("%d", &stay_here);
這個(gè)語(yǔ)句將會(huì)讀取鍵盤(pán)輸入,直到回車(chē)之前的所有數(shù)據(jù),什么意思?就是回車(chē)會(huì)留在輸入流中,被下一個(gè)輸入讀取或者丟棄。這就有可能會(huì)影響我們的程序,產(chǎn)生意料之外的結(jié)果。而使用上當(dāng)兩句組合則不會(huì)。
事實(shí)上,函數(shù)名出現(xiàn)在賦值符號(hào)右邊就代表著函數(shù)的地址
int function(int argc){ /*...*/
}
...
int (*p_fun)(int) = function;
int (*p_fuc)(int) = &function;//和上一句意義一致
上述代碼即聲明并初始化了函數(shù)指針,p_fun
的類型是指向一個(gè)返回值是int類型,參數(shù)是int類型的函數(shù)的指針
p_fun(11);
(*p_fun)(11);
function(11);
上述三個(gè)代碼的意義也相同,同樣我們也能使用函數(shù)指針數(shù)組這個(gè)概念
int (*p_func_arr[])(int) = {func1, func2,};
其中func1,func2
都是返回值為int
參數(shù)為int
的函數(shù),接著我們能像數(shù)組索引一樣使用這個(gè)函數(shù)了。
Tips: 我們總是忽略函數(shù)聲明,這并不是什么好事。
比如,當(dāng)我們?cè)谀硞€(gè)地方調(diào)用了一個(gè)函數(shù),但是并沒(méi)有聲明它:
CallWithoutDeclare(100); //參數(shù)100為 int 型
那么,C編譯器就會(huì)推測(cè),這個(gè)使用了int
型參數(shù)的函數(shù),一定是有一個(gè)int
型的參數(shù)列表,一旦函數(shù)定義中的參數(shù)列表與之不符合,將會(huì)導(dǎo)致參數(shù)信息傳遞錯(cuò)誤(編譯器永遠(yuǎn)堅(jiān)信自己是對(duì)的!),我們知道C語(yǔ)言是強(qiáng)類型語(yǔ)言,一旦類型不正確,會(huì)導(dǎo)致許多意想不到的結(jié)果(往往是Bug)發(fā)生。
我們常常見(jiàn)到這種寫(xiě)法:
int* pointer = (int*)malloc(sizeof(int));
這有什么奇怪的嗎?看下面這個(gè)例子:
int* pointer_2 = malloc(sizeof(int));
哪個(gè)寫(xiě)法是正確的??jī)蓚€(gè)都正確,這是為什么呢,這又要追求到遠(yuǎn)古C語(yǔ)言時(shí)期,在那個(gè)時(shí)候, void*
這個(gè)類型還沒(méi)有出現(xiàn)的時(shí)候,malloc
返回的是 char*
的類型,于是那時(shí)的程序員在調(diào)用這個(gè)函數(shù)時(shí)總要加上強(qiáng)制類型轉(zhuǎn)換,才能正確使用這個(gè)函數(shù),但是在標(biāo)準(zhǔn)C出現(xiàn)之后,這個(gè)問(wèn)題不再擁有,由于任何類型的指針都能與 void*
互相轉(zhuǎn)換,并且C標(biāo)準(zhǔn)中并不贊同在不必要的地方使用強(qiáng)制類型轉(zhuǎn)換,故而C語(yǔ)言中比較正統(tǒng)的寫(xiě)法是第二種。
題外話: C++中的指針轉(zhuǎn)換需要使用強(qiáng)制類型轉(zhuǎn)換,而不能像第二種例子,但是C++中有一種更好的內(nèi)存分配方法,所以這個(gè)問(wèn)題也不再是問(wèn)題。
Tips:
malloc
, calloc
, realloc
都是擁有很大風(fēng)險(xiǎn)的函數(shù),在使用的時(shí)候務(wù)必記得對(duì)他們的結(jié)果進(jìn)行校驗(yàn),最好的辦法還是對(duì)他們進(jìn)行再包裝,可以選擇宏包裝,也可以選擇函數(shù)包裝。realloc
函數(shù)是最為人詬病的一個(gè)函數(shù),因?yàn)樗穆毮苓^(guò)于寬廣,既能分配空間,也能釋放空間,雖然看起來(lái)是一個(gè)好函數(shù),但是有可能在不經(jīng)意間會(huì)幫我們做一些意料之外的事情,例如多次釋放空間。正確的做法就是,應(yīng)該使用再包裝閹割它的功能,使他只能進(jìn)行擴(kuò)展或者縮小堆內(nèi)存塊大小。typedef struct tag{
int value;
long vari_store[1];
}vari_struct;
乍一看,似乎是一個(gè)很中規(guī)中矩的結(jié)構(gòu)體
...
vari_struct vari_1;
vari_struct* vari_p_1 = &vari_1;
vari_struct* vari_p_2 = malloc(sizeof(vari_struct))(
似乎都是這么用的,但總有那么一些人想出了一些奇怪的用法
int what_spa_want = 10;
vari_struct* vari_p_3 = malloc(sizeof(vari_struct) + sizeof(long)*what_spa_want);
這么做是什么意思呢?這叫做可變長(zhǎng)結(jié)構(gòu)體,即便我們超出了結(jié)構(gòu)體范圍,只要在分配空間內(nèi),就不算越界。what_spa_want
解釋為你需要多大的空間,即在一個(gè)結(jié)構(gòu)體大小之外還需要多少的空間,空間用來(lái)存儲(chǔ)long
類型,由于分配的內(nèi)存是連續(xù)的,故可以直接使用數(shù)組vari_store
直接索引。
而且由于C語(yǔ)言中,編譯器并不對(duì)數(shù)組做越界檢查,故對(duì)于一個(gè)有N
個(gè)數(shù)的數(shù)組arr
,表達(dá)式&arr[N]
是被標(biāo)準(zhǔn)允許的行為,但是要記住arr[N]
卻是非法的。
這種用法并非是娛樂(lè),而是成為了標(biāo)準(zhǔn)(C99)的一部分,運(yùn)用到了實(shí)際中
在內(nèi)存分配的過(guò)程中,我們使用 malloc
進(jìn)行分配,用 free
進(jìn)行釋放,但這是我們理解中的分配與釋放嗎?
在調(diào)用 malloc
時(shí),該函數(shù)或使用 brk()
或使用 mmap()
向操作系統(tǒng)申請(qǐng)一片內(nèi)存,在使用時(shí)分配給需要的地方,與之對(duì)應(yīng)的是 free
,與我們硬盤(pán)刪除東西一樣,實(shí)際上:
int* value = malloc(sizeof(int)*5);
...
free(value);
printf("%d\n", value[0]);
代碼中,為什么在 free
之后,我又繼續(xù)使用這個(gè)內(nèi)存呢?因?yàn)?free
只是將該內(nèi)存標(biāo)記上釋放的標(biāo)記,示意分配內(nèi)存的函數(shù),我可以使用,但并沒(méi)有破壞當(dāng)前內(nèi)存中的內(nèi)容,直到有操作對(duì)它進(jìn)行寫(xiě)入。
這便引申出幾個(gè)問(wèn)題:
p1
,p2
指向同一個(gè)內(nèi)存,如果我們對(duì)其中某一個(gè)指針使用了 free(p1);
操作,卻忘記了還有另一個(gè)指針指向它,那這就會(huì)導(dǎo)致很?chē)?yán)重的安全隱患,而且這個(gè)隱患十分難以發(fā)現(xiàn),原因在于這個(gè)Bug并不會(huì)在當(dāng)時(shí)顯露出來(lái),而是有可能在未來(lái)的某個(gè)時(shí)刻,不經(jīng)意的讓你的程序崩潰。某些大哥提到說(shuō),
free
并不是什么都不做,而是將該段地址空間的前面一小部分置零 但是如果地址空間很長(zhǎng)的話,依舊有誤用的風(fēng)險(xiǎn),希望大家還是警惕實(shí)際上之所以庫(kù)作者不讓
free
操作將地址空間清空,有一部分原因是為了性能考慮,因?yàn)橹昧悴僮魇且粋€(gè)消耗性能的行為,具體可以自行嘗試,所謂雙刃劍就在于此。
總的來(lái)說(shuō),還是那句話C語(yǔ)言是一把雙刃劍。