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