http://www.jb51.net/article/79259.htm
2016
這似乎是一個很凝重的話題,但是它真的很有趣。
1. 指針是指向某一類型的東西,任何一個整體,只要能稱為整體就能擁有它自己的獨一無二的指針類型,所以指針的類型其實是近似無窮無盡的
2. 函數(shù)名在表達(dá)式中總是以函數(shù)指針的身份呈現(xiàn),除了取地址運算符以及sizeof
3. C語言最晦澀難明的就是它復(fù)雜的聲明: void (*signal(int sig, void (*func)(int)))(int),試試著把它改寫成容易理解的形式
4. 對于指針,盡最大的限度使用const保護(hù)它,無論是傳遞給函數(shù),還是自己使用
先來看看一個特殊的指針,姑且稱它為指針,因為它依賴于環(huán)境: NULL,是一個神奇的東西。先附上定義,在編譯器中會有兩種NULL(每種環(huán)境都有唯一確定的NULL):
有什么區(qū)別嗎?看起來沒什么區(qū)別都是0,只不過一個是常量,一個是地址為0的指針。
當(dāng)它們都作為指針的值時并不會報錯或者警告,即編譯器或者說C標(biāo)準(zhǔn)認(rèn)為這是合法的:
為什么?為什么0可以賦值給指針,但是10卻不行?他們都是常量。
因為C語言規(guī)定當(dāng)處理上下文的編譯器發(fā)現(xiàn)常量0出現(xiàn)在指針賦值的語句中,它就作為指針使用,似乎很扯淡,可是卻是如此。
回到最開始,對于NULL的兩種情況,會有什么區(qū)別?拿字符串來說,實際上我是將字符數(shù)組看作是C風(fēng)格字符串。
在C語言中,字符數(shù)組是用來存儲一連串有意義的字符,默認(rèn)在這些字符的結(jié)尾添加'\0',好這里又出現(xiàn)了一個0值。
對于某些人,在使用字符數(shù)組的時候總是分不清楚NULL與'\0'的區(qū)別而誤用,在字符數(shù)組的末尾使用NULL是絕對錯誤的!雖然它們的本質(zhì)都是常量0,但由于位置不同所以含義也不同。
開胃菜已過
對于一個函數(shù),我們進(jìn)行參數(shù)傳遞,參數(shù)有兩種形式: 形參與實參
其中,value是形參,11是實參,我們知道場面上,C語言擁有兩種傳遞方式:按值傳遞和按址傳遞,但是你是否有認(rèn)真研究過?這里給出一個實質(zhì),其實C語言只有按值傳遞,所謂按址傳遞只不過是按值傳遞的一種假象。至于原因稍微一想便能明白。
對于形參和實參而言兩個關(guān)系緊密,可以這么理解總是實參將自己的一份拷貝傳遞給形參,這樣形參便能安全的使用實參的值,但也帶給我們一些麻煩,最經(jīng)典的交換兩數(shù)
這就是所謂的按址傳遞,實際上只是將外部指針(實參)的值做一個拷貝,傳遞給形參val_1與val_2,實際上我們使用:
試一試是不是很神奇,而且省去了函數(shù)調(diào)用的時間,空間開銷。上述兩種寫法的原理實質(zhì)是一樣的。
但是,動動腦筋想一想,這種寫法真的沒有瑕疵嗎?如果輸入的兩個參數(shù)本就指向同一塊內(nèi)存,會發(fā)生什么?
會輸出什么?:
對,輸出了0,為什么?稍微動動腦筋就能相通,那么對于后面的SWAP_V3亦是如此,所以在斟酌之下,解決方案應(yīng)該盡可能短小精悍:
這便是目前能找到最好的交換函數(shù),我們在此基礎(chǔ)上可以考慮的更深遠(yuǎn)一些,如何讓這個交換函數(shù)更加通用?即適用范圍更大?暫不考慮浮點類型。 提示:可用void*
與上面的情況類似,偶爾的不經(jīng)意就會造成嚴(yán)重的后果:
上述兩個函數(shù)的功能一樣嗎?恩看起來是一樣的
輸出
如果傳入兩個同一對象呢?
輸出
知道真相總是令人吃驚,指針也是那么令人又愛又恨。
C99 標(biāo)準(zhǔn)之后出現(xiàn)了一個新的關(guān)鍵字, restrict,被用于修飾指針,它并沒有太多的顯式作用,甚至加與不加,在 你自己 看來,效果毫無區(qū)別。但是反觀標(biāo)準(zhǔn)庫的代碼中,許多地方都使用了該關(guān)鍵字,這是為何
關(guān)于數(shù)組的那些事
數(shù)組和指針一樣嗎?
不一樣
要時刻記住,數(shù)組與指針是不同的東西。但是為什么下面代碼是正確的?
我們還是那句話,結(jié)合上下文,編譯器推出 arr處于賦值操作符的右側(cè),默默的將他轉(zhuǎn)換為對應(yīng)類型的指針,而我們在使用arr時也總是將其當(dāng)成是指向該數(shù)組內(nèi)存塊首位的指針。
輸出:
這就是為什么數(shù)組與指針不同的原因所在,在外部即定義數(shù)組的代碼塊中,編譯器通過上下文發(fā)覺此處arr是一個數(shù)組,而arr代表的是一個指向10個int類型的數(shù)組的指針,只所謂最開始的代碼是正確的,只是因為這種用法比較多,就成了標(biāo)準(zhǔn)的一部分。就像世上本沒有路,走的多了就成了路。"正確"的該怎么寫
此時p的類型就是一個指向含有10個元素的數(shù)組的指針,此時(*p)[0]產(chǎn)生的效果是arr[0],也就是parr[0],但是(*p)呢?這里不記錄,結(jié)果是會溢出,為什么?
這就是數(shù)組與指針的區(qū)別與聯(lián)系,但是既然我們可以使用像parr這樣的指針,又為什么要寫成int (*p)[10]這樣丑陋不堪的模式呢?原因如下:
回到最開始說過的傳遞方式,按值傳遞在傳遞arr時只是純粹的將其值進(jìn)行傳遞,而丟失了上下文的它只是一個普通指針,只不過我們程序員知道它指向了一塊有意義的內(nèi)存的起始位置,我想要將數(shù)組的信息一起傳遞,除了額外增加一個參數(shù)用來記錄數(shù)組的長度以外,也可以使用這個方法,傳遞一個指向數(shù)組的指針 這樣我們就能只傳遞一個參數(shù)而保留所有信息。但這么做的也有限制:對于不同大小,或者不同存儲類型的數(shù)組而言,它們的類型也有所不同
如上所示,指向數(shù)組的指針必須明確指定數(shù)組的大小,數(shù)組存儲類型,這就讓指向數(shù)組的指針有了比較大的限制。
這種用法在多維數(shù)組中使用的比較多,但總體來說平常用的并不多,就我而言,更傾向于使用一維數(shù)組來表示多維數(shù)組,實際上誠如前面所述,C語言是一個非常簡潔的語言,它沒有太多的廢話,就本質(zhì)而言C語言并沒有多維數(shù)組,因為內(nèi)存是一種線性存在,即便是多維數(shù)組也是實現(xiàn)成一維數(shù)組的形式。
就多維數(shù)組在這里解釋一下。所謂多維數(shù)組就是將若干個降一維的數(shù)組組合在一起,降一維的數(shù)組又由若干個更降一維的數(shù)組組合在一起,直到最低的一維數(shù)組,舉個例子:
就這個二維數(shù)組而言,將5個每個為3個int類型的數(shù)組組合在一起,要想指向這個數(shù)組該怎么做?
實際上多維數(shù)組只是將多個降一維的數(shù)組組合在一起,令索引時比較直觀而已。當(dāng)真正理解了內(nèi)存的使用,反而會覺得多維數(shù)組帶給自己更多限制 對于第三句的解釋,當(dāng)數(shù)組名出現(xiàn)在賦值號右側(cè)時,它將是一個指針,類型則是 指向該數(shù)組元素的類型,而對于一個多維數(shù)組來說,其元素類型則是其降一維數(shù)組,即指向該降一維數(shù)組的指針類型。這個解釋有點繞,自己動手寫一寫就好很多。
對于某種形式下的操作,我們總是自然的將相似的行為結(jié)合在一起考慮。考慮如下代碼:
輸出: 3 == 3 == 3 ? 實際上對于數(shù)組與指針而言, []操作在大多數(shù)情況下都能有相同的結(jié)果,對于指針而言*(p_4 + 2)相當(dāng)于p_4[2],也就是說[]便是指針運算的語法糖,有意思的是2[p_4]也相當(dāng)于p_4[2],"Iamastring"[2] == 'm',但這只是娛樂而已,實際中請不要這么做,除非是代碼混亂大賽或者某些特殊用途。 在此處,應(yīng)該聲明的是這幾種寫法的執(zhí)行效率完全一致,并不存在一個指針運算便快于[]運算,這些說法都是上個世紀(jì)的說法了,隨著時代的發(fā)展,我們應(yīng)該更加注重代碼整潔之道
在此處還有一種奇異又實用的技巧,在char數(shù)組中使用指針運算進(jìn)行操作,提取不同類型的數(shù)據(jù),或者是在不同類型數(shù)組中,使用char*指針抽取其中內(nèi)容,才是顯示指針運算的用途。但在使用不同類型指針操作內(nèi)存塊的時候需要注意,不要操作無意義的區(qū)域或者越界操作。
實際上,最簡單的安全研究之一,便是利用溢出進(jìn)行攻擊。
Advance:對于一個函數(shù)中的某個數(shù)組的增長方向,總是向著返回地址的,中間可能隔著許多其他自動變量,我們只需要一直進(jìn)行溢出試驗,直到某一次,該函數(shù)無法正常返回了!那就證明我們找到了該函數(shù)的返回地址存儲地區(qū),這時候我們可以進(jìn)行一些操作,例如將我們想要的返回地址覆蓋掉原先的返回地址,這就是所謂的溢出攻擊中的一種。
內(nèi)存的使用的那些事兒
你一直以為你操作的是真實物理內(nèi)存,實際上并不是,你操作的只是操作系統(tǒng)為你分配的資格虛擬地址,但這并不意味著我們可以無限使用內(nèi)存,那內(nèi)存賣那么貴干嘛,實際上存儲數(shù)據(jù)的還是物理內(nèi)存,只不過在操作系統(tǒng)這個中介的介入情況下,不同程序窗口(可以是相同程序)可以共享使用同一塊內(nèi)存區(qū)域,一旦某個傻大個程序的使用讓物理內(nèi)存不足了,我們就會把某些沒用到的數(shù)據(jù)寫到你的硬盤上去,之后再使用時,從硬盤讀回。這個特性會導(dǎo)致什么呢?假設(shè)你在Windows上使用了多窗口,打開了兩個相同的程序:
對此程序(引用前橋和彌的例子),每敲擊一次回車,值加1。當(dāng)你同時打開兩個該程序時,你會發(fā)現(xiàn),兩個程序的stay_here都是在同一個地址,但對它進(jìn)行分別操作時,產(chǎn)生的結(jié)果是獨立的!這在某一方面驗證了虛擬地址的合理性。虛擬地址的意義就在于,即使一個程序出現(xiàn)了錯誤,導(dǎo)致所在內(nèi)存完蛋了,也不會影響到其他進(jìn)程。對于程序中部的兩個讀取語句,是一種理解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的問題
這個語句將會讀取鍵盤輸入,直到回車之前的所有數(shù)據(jù),什么意思?就是回車會留在輸入流中,被下一個輸入讀取或者丟棄。這就有可能會影響我們的程序,產(chǎn)生意料之外的結(jié)果。而使用上當(dāng)兩句組合則不會。
函數(shù)與函數(shù)指針的那些事
事實上,函數(shù)名出現(xiàn)在賦值符號右邊就代表著函數(shù)的地址
上述代碼即聲明并初始化了函數(shù)指針,p_fun的類型是指向一個返回值是int類型,參數(shù)是int類型的函數(shù)的指針
上述三個代碼的意義也相同,同樣我們也能使用函數(shù)指針數(shù)組這個概念
其中func1,func2都是返回值為int參數(shù)為int的函數(shù),接著我們能像數(shù)組索引一樣使用這個函數(shù)了。
Tips: 我們總是忽略函數(shù)聲明,這并不是什么好事。
在C語言中,因為編譯器并不會對有沒有函數(shù)聲明過分深究,甚至還會放縱,當(dāng)然這并不包含內(nèi)聯(lián)函數(shù)(inline),因為它本身就只在本文件可用。
比如,當(dāng)我們在某個地方調(diào)用了一個函數(shù),但是并沒有聲明它:
那么,C編譯器就會推測,這個使用了int型參數(shù)的函數(shù),一定是有一個int型的參數(shù)列表,一旦函數(shù)定義中的參數(shù)列表與之不符合,將會導(dǎo)致參數(shù)信息傳遞錯誤(編譯器永遠(yuǎn)堅信自己是對的!),我們知道C語言是強類型語言,一旦類型不正確,會導(dǎo)致許多意想不到的結(jié)果(往往是Bug)發(fā)生。
對函數(shù)指針的調(diào)用同樣如此
C語言中malloc的那些事兒
我們常常見到這種寫法:
這有什么奇怪的嗎?看下面這個例子:
哪個寫法是正確的?兩個都正確,這是為什么呢,這又要追求到遠(yuǎn)古C語言時期,在那個時候, void* 這個類型還沒有出現(xiàn)的時候,malloc 返回的是 char* 的類型,于是那時的程序員在調(diào)用這個函數(shù)時總要加上強制類型轉(zhuǎn)換,才能正確使用這個函數(shù),但是在標(biāo)準(zhǔn)C出現(xiàn)之后,這個問題不再擁有,由于任何類型的指針都能與 void* 互相轉(zhuǎn)換,并且C標(biāo)準(zhǔn)中并不贊同在不必要的地方使用強制類型轉(zhuǎn)換,故而C語言中比較正統(tǒng)的寫法是第二種。
題外話: C++中的指針轉(zhuǎn)換需要使用強制類型轉(zhuǎn)換,而不能像第二種例子,但是C++中有一種更好的內(nèi)存分配方法,所以這個問題也不再是問題。
Tips:
C語言的三個函數(shù)malloc, calloc, realloc都是擁有很大風(fēng)險的函數(shù),在使用的時候務(wù)必記得對他們的結(jié)果進(jìn)行校驗,最好的辦法還是對他們進(jìn)行再包裝,可以選擇宏包裝,也可以選擇函數(shù)包裝。
realloc函數(shù)是最為人詬病的一個函數(shù),因為它的職能過于寬廣,既能分配空間,也能釋放空間,雖然看起來是一個好函數(shù),但是有可能在不經(jīng)意間會幫我們做一些意料之外的事情,例如多次釋放空間。正確的做法就是,應(yīng)該使用再包裝閹割它的功能,使他只能進(jìn)行擴展或者縮小堆內(nèi)存塊大小。
指針與結(jié)構(gòu)體
乍一看,似乎是一個很中規(guī)中矩的結(jié)構(gòu)體
似乎都是這么用的,但總有那么一些人想出了一些奇怪的用法
這么做是什么意思呢?這叫做可變長結(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,表達(dá)式&arr[N]是被標(biāo)準(zhǔn)允許的行為,但是要記住arr[N]卻是非法的。 這種用法并非是娛樂,而是成為了標(biāo)準(zhǔn)(C99)的一部分,運用到了實際中
對于內(nèi)存的理解
在內(nèi)存分配的過程中,我們使用 malloc 進(jìn)行分配,用 free 進(jìn)行釋放,但這是我們理解中的分配與釋放嗎? 在調(diào)用 malloc 時,該函數(shù)或使用 brk() 或使用 nmap() 向操作系統(tǒng)申請一片內(nèi)存,在使用時分配給需要的地方,與之對應(yīng)的是 free,與我們硬盤刪除東西一樣,實際上:
代碼中,為什么在 free 之后,我又繼續(xù)使用這個內(nèi)存呢?因為 free 只是將該內(nèi)存標(biāo)記上釋放的標(biāo)記,示意分配內(nèi)存的函數(shù),我可以使用,但并沒有破壞當(dāng)前內(nèi)存中的內(nèi)容,直到有操作對它進(jìn)行寫入。 這便引申出幾個問題:
Bug更加難以發(fā)現(xiàn),讓我們假設(shè),如果我們有兩個指針p1,p2指向同一個內(nèi)存,如果我們對其中某一個指針使用了 free(p1); 操作,卻忘記了還有另一個指針指向它,那這就會導(dǎo)致很嚴(yán)重的安全隱患,而且這個隱患十分難以發(fā)現(xiàn),原因在于這個Bug并不會在當(dāng)時顯露出來,而是有可能在未來的某個時刻,不經(jīng)意的讓你的程序崩潰。
有可能會讓某些問題更加簡化,例如釋放一個條條相連的鏈表域。
總的來說,還是那句話C語言是一把雙刃劍。