這似乎是一個(gè)很凝重的話題,但是它真的很有趣。
1. 指針是指向某一類型的東西,任何一個(gè)整體,只要能稱為整體就能擁有它自己的獨(dú)一無(wú)二的指針類型,所以指針的類型其實(shí)是近似無(wú)窮無(wú)盡的
2. 函數(shù)名在表達(dá)式中總是以函數(shù)指針的身份呈現(xiàn),除了取地址運(yùn)算符以及sizeof
3. C語(yǔ)言最晦澀難明的就是它復(fù)雜的聲明: void (*signal(int sig, void (*func)(int)))(int)
,試試著把它改寫成容易理解的形式
4. 對(duì)于指針,盡最大的限度使用const
保護(hù)它,無(wú)論是傳遞給函數(shù),還是自己使用
先來(lái)看看一個(gè)特殊的指針,姑且稱它為指針,因?yàn)樗蕾囉诃h(huán)境: NULL
,是一個(gè)神奇的東西。先附上定義,在編譯器中會(huì)有兩種NULL(每種環(huán)境都有唯一確定的NULL):
#define NULL 0
#define NULL ((void*)0)
有什么區(qū)別嗎?看起來(lái)沒(méi)什么區(qū)別都是0
,只不過(guò)一個(gè)是常量,一個(gè)是地址為0的指針。
當(dāng)它們都作為指針的值時(shí)并不會(huì)報(bào)錯(cuò)或者警告,即編譯器或者說(shuō)C標(biāo)準(zhǔn)認(rèn)為這是合法的:
int* temp_int_1 = 0; //無(wú)警告
int* temp_int_2 = (void*)0; //無(wú)警告
int* temp_int_3 = 10; //出現(xiàn)警告
為什么?為什么0
可以賦值給指針,但是10
卻不行?他們都是常量。
因?yàn)镃語(yǔ)言規(guī)定當(dāng)處理上下文的編譯器發(fā)現(xiàn)常量0
出現(xiàn)在指針賦值的語(yǔ)句中,它就作為指針使用,似乎很扯淡,可是卻是如此。
回到最開(kāi)始,對(duì)于NULL
的兩種情況,會(huì)有什么區(qū)別?拿字符串來(lái)說(shuō),實(shí)際上我是將字符數(shù)組看作是C風(fēng)格字符串。
在C語(yǔ)言中,字符數(shù)組是用來(lái)存儲(chǔ)一連串有意義的字符,默認(rèn)在這些字符的結(jié)尾添加'\0'
,好這里又出現(xiàn)了一個(gè)0值。
對(duì)于某些人,在使用字符數(shù)組的時(shí)候總是分不清楚NULL
與'\0'
的區(qū)別而誤用,在字符數(shù)組的末尾使用NULL
是絕對(duì)錯(cuò)誤的!雖然它們的本質(zhì)都是常量0,但由于位置不同所以含義也不同。
對(duì)于一個(gè)函數(shù),我們進(jìn)行參數(shù)傳遞,參數(shù)有兩種形式: 形參與實(shí)參
int function(int value)
{
/*...*/
}
//...
function(11);
其中,value
是形參,11
是實(shí)參,我們知道場(chǎng)面上,C語(yǔ)言擁有兩種傳遞方式:按值傳遞和按址傳遞,但是你是否有認(rèn)真研究過(guò)?這里給出一個(gè)實(shí)質(zhì),其實(shí)C語(yǔ)言只有按值傳遞,所謂按址傳遞只不過(guò)是按值傳遞的一種假象。至于原因稍微一想便能明白。
對(duì)于形參和實(shí)參而言兩個(gè)關(guān)系緊密,可以這么理解總是實(shí)參將自己的一份拷貝傳遞給形參,這樣形參便能安全的使用實(shí)參的值,但也帶給我們一些麻煩,最經(jīng)典的交換兩數(shù)
void swap_v1(int* val_1, int* val_2)
{
int temp = *val_1;
*val_1 = *val_2;
*val_2 = *val_1;
}
這就是所謂的按址傳遞,實(shí)際上只是將外部指針(實(shí)參)的值做一個(gè)拷貝,傳遞給形參val_1
與val_2
,實(shí)際上我們使用:
#define SWAP_V2(a, b) (a += b, b = a - b, a -= b)
#define SWAP_V3(x, y) {x ^= y; y ^= x; x ^= y}
試一試是不是很神奇,而且省去了函數(shù)調(diào)用的時(shí)間,空間開(kāi)銷。上述兩種寫法的原理實(shí)質(zhì)是一樣的。
但是,動(dòng)動(dòng)腦筋想一想,這種寫法真的沒(méi)有瑕疵嗎?如果輸入的兩個(gè)參數(shù)本就指向同一塊內(nèi)存,會(huì)發(fā)生什么?
...
int test_1 = 10, test_2 = 100;
SWAP_V2(test_1, test_2);
printf("Now the test_1 is %d, test_2 is %d\n", test_1, test_2);
.../*恢復(fù)原值*/
SWAP_V2(test_1, test_1);
printf("Now the test_1 is %d\n", test_1);
會(huì)輸出什么?:
$: Now the test_1 is 100, test_2 is 10
$: Now the test_1 is 0
對(duì),輸出了0,為什么?稍微動(dòng)動(dòng)腦筋就能相通,那么對(duì)于后面的SWAP_V3
亦是如此,所以在斟酌之下,解決方案應(yīng)該盡可能短小精悍:
static inline void swap_final(int* val_1, int* val_2)
{
if(val_1 == val_2)
return;
*val_1 ^= *val_2;
*val_2 ^= *val_1;
*val_1 ^= *val_2;
}
#define SWAP(x, y) \
do{ \
if(&x == &y) \
break; \
x ^= y; \
y ^= x; \
x ^= y; \
}while(0)
這便是目前能找到最好的交換函數(shù),我們?cè)诖嘶A(chǔ)上可以考慮的更深遠(yuǎn)一些,如何讓這個(gè)交換函數(shù)更加通用?即適用范圍更大?暫不考慮浮點(diǎn)類型。
提示:可用void*
與上面的情況類似,偶爾的不經(jīng)意就會(huì)造成嚴(yán)重的后果:
int combine_1(int* dest, int* add)
{
*dest += *add;
*dest += *add;
return *dest;
}
int combine_2(int* dest, int* add)
{
*dest += 2 * (*add);//在不確定優(yōu)先級(jí)時(shí)用括號(hào)是一個(gè)明智的選擇
return *dest;
}
上述兩個(gè)函數(shù)的功能一樣嗎?恩看起來(lái)是一樣的
int test_3 = 10, test_4 = 100;
combine_1(&test_3, &test_4);
printf("After combine_1, test_3 = %d\n",test_3);
.../*恢復(fù)原值*/
combine_2(&test_3, &test_4);
printf("After combine_2, test_3 = %d\n",test_3);
輸出
$: After combine_1, test_3 = 210
$: After combine_2, test_3 = 210
如果傳入兩個(gè)同一對(duì)象呢?
... /*恢復(fù)test_3原值*/
combine_1(&test_3, &test_3);
printf("After second times combine_1, test_3 = %d\n",test_3);
...
combine_2(&test_3, &test_3);
printf("After second times combine_2, test_3 = %d\n",test_3);
輸出
$: After second times combine_1, test_3 = 40
$: After second times combine_2, test_3 = 30
知道真相總是令人吃驚,指針也是那么令人又愛(ài)又恨。
restrict
,被用于修飾指針,它并沒(méi)有太多的顯式作用,甚至加與不加,在 你自己 看來(lái),效果毫無(wú)區(qū)別。但是反觀標(biāo)準(zhǔn)庫(kù)的代碼中,許多地方都使用了該關(guān)鍵字,這是為何
數(shù)組和指針一樣嗎?
不一樣
要時(shí)刻記住,數(shù)組與指針是不同的東西。但是為什么下面代碼是正確的?
int arr[10] = {10, 9, 8, 7};
int* parr = arr;
我們還是那句話,結(jié)合上下文,編譯器推出 arr
處于賦值操作符的右側(cè),默默的將他轉(zhuǎn)換為對(duì)應(yīng)類型的指針,而我們?cè)谑褂?code>arr時(shí)也總是將其當(dāng)成是指向該數(shù)組內(nèi)存塊首位的指針。
//int function2(const int test_arr[10]
//int function2(const int test_arr[]) 考慮這三種寫法是否一樣
int function2(const int* test_arr)
{
return sizeof(test_arr);
}
...
int size_out = sizeof(arr);
int size_in = function2(arr);
printf("size_out = %d, size_in = %d\n", size_out, size_in);
輸出: size_out = 40, size_in = 8
這就是為什么數(shù)組與指針不同的原因所在,在外部即定義數(shù)組的代碼塊中,編譯器通過(guò)上下文發(fā)覺(jué)此處arr是一個(gè)數(shù)組,而arr
代表的是一個(gè)指向10個(gè)int類型的數(shù)組的指針,只所謂最開(kāi)始的代碼是正確的,只是因?yàn)檫@種用法比較多,就成了標(biāo)準(zhǔn)的一部分。就像世上本沒(méi)有路,走的多了就成了路。"正確"的該怎么寫
int (*p)[10] = &arr;
此時(shí)p
的類型就是一個(gè)指向含有10個(gè)元素的數(shù)組的指針,此時(shí)(*p)[0]
產(chǎn)生的效果是arr[0]
,也就是parr[0]
,但是(*p)
呢?這里不記錄,結(jié)果是會(huì)溢出,為什么?
這就是數(shù)組與指針的區(qū)別與聯(lián)系,但是既然我們可以使用像parr
這樣的指針,又為什么要寫成int (*p)[10]
這樣丑陋不堪的模式呢?原因如下:
回到最開(kāi)始說(shuō)過(guò)的傳遞方式,按值傳遞在傳遞arr
時(shí)只是純粹的將其值進(jìn)行傳遞,而丟失了上下文的它只是一個(gè)普通指針,只不過(guò)我們程序員知道它指向了一塊有意義的內(nèi)存的起始位置,我想要將數(shù)組的信息一起傳遞,除了額外增加一個(gè)參數(shù)用來(lái)記錄數(shù)組的長(zhǎng)度以外,也可以使用這個(gè)方法,傳遞一個(gè)指向數(shù)組的指針
這樣我們就能只傳遞一個(gè)參數(shù)而保留所有信息。但這么做的也有限制:對(duì)于不同大小,或者不同存儲(chǔ)類型的數(shù)組而言,它們的類型也有所不同
int arr_2[5];
int (*p_2)[5] = &arr_2;
float arr_3[5];
float (*p_3)[5] = &arr_3;
如上所示,指向數(shù)組的指針必須明確指定數(shù)組的大小,數(shù)組存儲(chǔ)類型,這就讓指向數(shù)組的指針有了比較大的限制。
這種用法在多維數(shù)組中使用的比較多,但總體來(lái)說(shuō)平常用的并不多,就我而言,更傾向于使用一維數(shù)組來(lái)表示多維數(shù)組,實(shí)際上誠(chéng)如前面所述,C語(yǔ)言是一個(gè)非常簡(jiǎn)潔的語(yǔ)言,它沒(méi)有太多的廢話,就本質(zhì)而言C語(yǔ)言并沒(méi)有多維數(shù)組,因?yàn)閮?nèi)存是一種線性存在,即便是多維數(shù)組也是實(shí)現(xiàn)成一維數(shù)組的形式。
就多維數(shù)組在這里解釋一下。所謂多維數(shù)組就是將若干個(gè)降一維的數(shù)組組合在一起,降一維的數(shù)組又由若干個(gè)更降一維的數(shù)組組合在一起,直到最低的一維數(shù)組,舉個(gè)例子:
int dou_arr[5][3];
就這個(gè)二維數(shù)組而言,將5個(gè)每個(gè)為3個(gè)int
類型的數(shù)組組合在一起,要想指向這個(gè)數(shù)組該怎么做?
int (*p)[3] = &dou_arr[0];
int (*dou_p)[5][3] = &dou_arr;
int (*what_p)[3] = dou_arr;
實(shí)際上多維數(shù)組只是將多個(gè)降一維的數(shù)組組合在一起,令索引時(shí)比較直觀而已。當(dāng)真正理解了內(nèi)存的使用,反而會(huì)覺(jué)得多維數(shù)組帶給自己更多限制 對(duì)于第三句的解釋,當(dāng)數(shù)組名出現(xiàn)在賦值號(hào)右側(cè)時(shí),它將是一個(gè)指針,類型則是 指向該數(shù)組元素的類型,而對(duì)于一個(gè)多維數(shù)組來(lái)說(shuō),其元素類型則是其降一維數(shù)組,即指向該降一維數(shù)組的指針類型。這個(gè)解釋有點(diǎn)繞,自己動(dòng)手寫一寫就好很多。
對(duì)于某種形式下的操作,我們總是自然的將相似的行為結(jié)合在一起考慮。考慮如下代碼:
int* arr_3[5] = {1, 2, 3, 4, 5};
int* p_4 = arr_3;
printf("%d == %d == %d ?\n", arr_3[2], *(p_4 + 2), *(arr_3 + 2));
輸出: 3 == 3 == 3 ?
實(shí)際上對(duì)于數(shù)組與指針而言, []
操作在大多數(shù)情況下都能有相同的結(jié)果,對(duì)于指針而言*(p_4 + 2)
相當(dāng)于p_4[2]
,也就是說(shuō)[]
便是指針運(yùn)算的語(yǔ)法糖,有意思的是2[p_4]
也相當(dāng)于p_4[2]
,"Iamastring"[2] == 'm'
,但這只是娛樂(lè)而已,實(shí)際中請(qǐng)不要這么做,除非是代碼混亂大賽或者某些特殊用途。
在此處,應(yīng)該聲明的是這幾種寫法的執(zhí)行效率完全一致,并不存在一個(gè)指針運(yùn)算便快于[]
運(yùn)算,這些說(shuō)法都是上個(gè)世紀(jì)的說(shuō)法了,隨著時(shí)代的發(fā)展,我們應(yīng)該更加注重代碼整潔之道
在此處還有一種奇異又實(shí)用的技巧,在char數(shù)組中使用指針運(yùn)算進(jìn)行操作,提取不同類型的數(shù)據(jù),或者是在不同類型數(shù)組中,使用char*
指針抽取其中內(nèi)容,才是顯示指針運(yùn)算的用途。但在使用不同類型指針操作內(nèi)存塊的時(shí)候需要注意,不要操作無(wú)意義的區(qū)域或者越界操作。
實(shí)際上,最簡(jiǎn)單的安全研究之一,便是利用溢出進(jìn)行攻擊。
Advance:對(duì)于一個(gè)函數(shù)中的某個(gè)數(shù)組的增長(zhǎng)方向,總是向著返回地址的,中間可能隔著許多其他自動(dòng)變量,我們只需要一直進(jìn)行溢出試驗(yàn),直到某一次,該函數(shù)無(wú)法正常返回了!那就證明我們找到了該函數(shù)的返回地址存儲(chǔ)地區(qū),這時(shí)候我們可以進(jìn)行一些操作,例如將我們想要的返回地址覆蓋掉原先的返回地址,這就是所謂的溢出攻擊中的一種。