鍍金池/ 教程/ C/ 0x05-C語(yǔ)言指針(Volume-2)
0x0E-單線程備份(下)
0x11-套接字編程-1
0x05-C語(yǔ)言指針:(Volume-1)
0x13-套接字編程-HTTP服務(wù)器(1)
0x0C-開(kāi)始行動(dòng)
C 語(yǔ)言進(jìn)階
第一部分
0x05-C語(yǔ)言指針(Volume-2)
0x08-C語(yǔ)言效率(下)
0x07-C語(yǔ)言效率(上)
0x04 C代碼規(guī)范
0x0F-多線程備份
0x05-C語(yǔ)言變量
第四部分
0x16-套接字編程-HTTP服務(wù)器(4)
0x0D-單線程備份(上)
總結(jié)
0x01-C語(yǔ)言序言
0x15-套接字編程-HTTP服務(wù)器(3)
0x14-套接字編程-HTTP服務(wù)器(2)
0x17-套接字編程-HTTP服務(wù)器(5)
第三部分
我的C語(yǔ)言
0x06-C語(yǔ)言預(yù)處理器
0x09-未曾領(lǐng)略的新風(fēng)景
0x0A-C線程和Glib的視角
第二部分
0x10-網(wǎng)絡(luò)的世界
0x12-套接字編程-2
0x03-C代碼
0x0B-C語(yǔ)言錯(cuò)誤處理

0x05-C語(yǔ)言指針(Volume-2)

0x05-C語(yǔ)言指針(Volume-2)

內(nèi)存的使用的那些事兒

你一直以為你操作的是真實(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ù)指針的那些事

事實(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ù)聲明,這并不是什么好事。

  • 在C語(yǔ)言中,因?yàn)榫幾g器并不會(huì)對(duì)有沒(méi)有函數(shù)聲明過(guò)分深究,甚至還會(huì)放縱,當(dāng)然這并不包含內(nèi)聯(lián)函數(shù)(inline),因?yàn)樗旧砭椭辉诒疚募捎谩?/li>
  • 比如,當(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ā)生。

  • 對(duì)函數(shù)指針的調(diào)用同樣如此
C語(yǔ)言中malloc的那些事兒

我們常常見(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:

  • C語(yǔ)言的三個(gè)函數(shù)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)存塊大小。
指針與結(jié)構(gòu)體
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í)際中

對(duì)于內(nèi)存的理解

在內(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)題:

  • Bug更加難以發(fā)現(xiàn),讓我們假設(shè),如果我們有兩個(gè)指針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)意的讓你的程序崩潰。
  • 有可能會(huì)讓某些問(wèn)題更加簡(jiǎn)化,例如釋放一個(gè)條條相連的鏈表域。

某些大哥提到說(shuō),free并不是什么都不做,而是將該段地址空間的前面一小部分置零 但是如果地址空間很長(zhǎng)的話,依舊有誤用的風(fēng)險(xiǎn),希望大家還是警惕

實(shí)際上之所以庫(kù)作者不讓free操作將地址空間清空,有一部分原因是為了性能考慮,因?yàn)橹昧悴僮魇且粋€(gè)消耗性能的行為,具體可以自行嘗試,所謂雙刃劍就在于此。

總的來(lái)說(shuō),還是那句話C語(yǔ)言是一把雙刃劍。