數(shù)組的某個(gè)成員可以用數(shù)組的基地址加上一個(gè)偏移量來(lái)表示。我們可以聲明一個(gè)指針double *p;,把它作為基地址,然后就可以像數(shù)組一樣在這個(gè)基地址上使用偏移量。在基地址上,我們可以找到第1個(gè)成員p[0]的內(nèi)容,在基地址上前進(jìn)一步可以找到第2個(gè)成員p[1]的內(nèi)容,接下來(lái)以此類(lèi)推。因此,只要提供一個(gè)指針以及兩個(gè)相鄰成員之間的距離,就可以把它作為數(shù)組使用了。
我們可以直接采用基地址加偏移量的書(shū)面形式,類(lèi)似(p+1)。正如教科書(shū)所描述的那樣,p[1]等同于 *(p+1),這就解釋了為什么數(shù)組的第1個(gè)成員是p[0] == *(p+0)。
這個(gè)理論提示了一些規(guī)則,用于在實(shí)際應(yīng)用中表述數(shù)組和它們的成員。
- 可以通過(guò)顯式的指針形式double *p,或靜態(tài)/自動(dòng)形式double p[100]來(lái)聲明數(shù)組。
- 不管是哪種情況,第n + 1個(gè)數(shù)組成員都是p[n]。不要忘了第一項(xiàng)是0而不是1,這樣就可以采用特殊形式p[0] == *p。
- 如果需要第n個(gè)成員的地址(而不是實(shí)際值),使用&符號(hào):&p[n]。當(dāng)然,第1個(gè)成員的地址就是&p[0] == p。
例1展示了這些規(guī)則的一些實(shí)際應(yīng)用。
例1 一些簡(jiǎn)單的指針運(yùn)算(arithmetic.c)
? 使用特殊形式*evens寫(xiě)入到evens[0]。
? 成員1的地址,賦值給一個(gè)新指針。
? 引用數(shù)組第1個(gè)成員的通常方式。
下面我再送你一個(gè)很好的技巧,這個(gè)技巧建立在指針運(yùn)算規(guī)則“p+1表示數(shù)組中下一個(gè)成員的地址(&p[1])”的基礎(chǔ)上。根據(jù)這個(gè)規(guī)則,我們不需要在遍歷數(shù)組的循環(huán)中使用下標(biāo)。在例2中我們就使用了一個(gè)備用指針來(lái)指向list的頭部,然后用p++在數(shù)組中向前遍歷,直到數(shù)組尾部的NULL標(biāo)記,從而獲得了整個(gè)數(shù)組值。如果你查看了接下來(lái)的指針聲明的提示,會(huì)更容易理解這種用法。
例2我們可以利用p++表示“前進(jìn)到下一個(gè)指針”實(shí)現(xiàn)循環(huán)的流水化
自己動(dòng)手
如果不了解p++,你打算怎樣實(shí)現(xiàn)這個(gè)目標(biāo)?
如果目標(biāo)是為了實(shí)現(xiàn)簡(jiǎn)潔的語(yǔ)法表示形式,基地址加偏移量這個(gè)技巧并不能提供太多的幫助,但它確實(shí)解釋了C的許多工作原理。事實(shí)上,我們可以考慮一下使用結(jié)構(gòu),例如:
作為一種智力模型來(lái)分析,我們可以把list看成是基地址,list[0].b與基地址的距離正好用來(lái)表示b。也就是說(shuō),假設(shè)list的位置是整數(shù)(size_t)&list,b位于(size_t)&list + sizeof(int);,這樣list[2].d的位置將是(size_t)&list + 6*sizeof(int) + 5*sizeof(double)。根據(jù)這種思路,結(jié)構(gòu)就與數(shù)組非常相似了,區(qū)別是結(jié)構(gòu)的成員是用名稱(chēng)而不是序號(hào)表示的,并且它們具有不同的類(lèi)型和長(zhǎng)度。
這個(gè)思路并不是非常正確,因?yàn)榇嬖趯?duì)齊這個(gè)因素,系統(tǒng)可能會(huì)決定數(shù)據(jù)需要位于某個(gè)特定長(zhǎng)度的內(nèi)存塊中,因此字段尾部可能會(huì)填充一些額外的空間,使下一個(gè)字符從正確的位置開(kāi)始,并且結(jié)構(gòu)的尾部可能也會(huì)進(jìn)行填充,使結(jié)構(gòu)列表中的每個(gè)結(jié)構(gòu)能夠大致對(duì)齊[C99和C11,§6.7.2.1(15)和(17)]。stddef.h頭文件定義了offsetof宏,它精確地描述了基地址加領(lǐng)偏移量的思路:list[2].d的實(shí)際地址是(size_t)&list + 2*sizeof(abcd_s) + offsetof(abcd_s, d)。
順便說(shuō)一下,在結(jié)構(gòu)的起始處不可能出現(xiàn)填充,因此list[2].a肯定等于(size_t)&list+ 2*sizeof(abcd_s)。
下面是個(gè)笨拙的函數(shù),它以遞歸的方式對(duì)列表中的成員進(jìn)行計(jì)數(shù),直到遇到值為0的成員。假設(shè)我們想把這個(gè)函數(shù)用于零值為合理數(shù)據(jù)的任何類(lèi)型的列表,因此我們讓它接受一個(gè)void指針(當(dāng)然這不是一種好的思路)。
基地址加偏移量的規(guī)則解釋了為什么這種做法是不行的。為了表示a_list[1],編譯器需要知道a_list[0]的準(zhǔn)確長(zhǎng)度,這樣才能知道應(yīng)該從基地址偏移多少。但是,由于沒(méi)有與之相關(guān)聯(lián)的類(lèi)型,它無(wú)法計(jì)算這個(gè)長(zhǎng)度。
typedef作為一種教學(xué)工具
任何時(shí)候當(dāng)我們遇到一種復(fù)雜的類(lèi)型時(shí),類(lèi)似于指向某種類(lèi)型的指針的指針的指針等情況,可以考慮用typedef進(jìn)行簡(jiǎn)化。
例如,下面這個(gè)常見(jiàn)的定義:
有效地減少了字符串?dāng)?shù)組的視覺(jué)混亂,使它們的意圖變得清晰。
在前面的指針運(yùn)算p++例子中,char *list[]這樣的聲明是否很清楚地告訴你它表示一個(gè)字符串列表而*p是一個(gè)字符串?
例3對(duì)例2的for循環(huán)進(jìn)行了重寫(xiě),用string替換了char *。
例3 添加一個(gè)typedef聲明使笨拙的代碼稍稍變得清晰
list的聲明行現(xiàn)在變得簡(jiǎn)單,很清晰地表示它是個(gè)字符串列表,并且string *p也很清晰地表示p是個(gè)指向字符串的指針。因此,*p表示一個(gè)字符串。
最后,我們?nèi)匀恍枰涀∽址莻€(gè)指向字符的指針。例如,NULL是個(gè)合法的字符串值。
我們甚至可以更進(jìn)一步,例如使用上面的typedef加上typedef stringlist string*,聲明一個(gè)字符串的二維數(shù)組。這種方法有時(shí)候非常實(shí)用,但有時(shí)候只會(huì)增加記憶的負(fù)擔(dān)。
從概念上講,函數(shù)類(lèi)型的語(yǔ)法實(shí)際上是指向一個(gè)特定類(lèi)型的函數(shù)的指針。如果我們有一個(gè)頭部類(lèi)似下面這樣的函數(shù):
然后只要添加一個(gè)星號(hào)(并加上括號(hào)以保證優(yōu)先級(jí)),就可以描述一個(gè)指向這種類(lèi)型的函數(shù)的指針:
然后在前面加上typedef來(lái)定義一種類(lèi)型:
現(xiàn)在我們可以把它當(dāng)作一種類(lèi)型使用,例如聲明一個(gè)接受另一個(gè)函數(shù)作為其輸入?yún)?shù)的函數(shù),可以這樣:
通過(guò)對(duì)函數(shù)指針類(lèi)型的重新定義,那些接受其他函數(shù)作為輸入的函數(shù)的表達(dá)—其中連環(huán)星號(hào)的書(shū)寫(xiě)曾是令人生畏的考驗(yàn)變得不再可怕。
最后需要說(shuō)明的是,指針實(shí)際上要比教科書(shū)所描述的簡(jiǎn)單得多,因?yàn)樗鼘?shí)際上只是一個(gè)位置或別名,根本不需要涉及不同類(lèi)型的內(nèi)存管理。像指向字符串的指針的指針這樣的復(fù)雜構(gòu)造總是會(huì)讓人感到迷惑,但這只不過(guò)是因?yàn)槲覀円葬鳙C為生的祖先從來(lái)沒(méi)有見(jiàn)到過(guò)這玩意而已。至少,C提供了typedef這個(gè)工具來(lái)處理它們。
本文節(jié)選自《C程序設(shè)計(jì)新思維》






