數組的某個成員可以用數組的基地址加上一個偏移量來表示。我們可以聲明一個指針double *p;,把它作為基地址,然后就可以像數組一樣在這個基地址上使用偏移量。在基地址上,我們可以找到第1個成員p[0]的內容,在基地址上前進一步可以找到第2個成員p[1]的內容,接下來以此類推。因此,只要提供一個指針以及兩個相鄰成員之間的距離,就可以把它作為數組使用了。
我們可以直接采用基地址加偏移量的書面形式,類似(p+1)。正如教科書所描述的那樣,p[1]等同于 *(p+1),這就解釋了為什么數組的第1個成員是p[0] == *(p+0)。
這個理論提示了一些規則,用于在實際應用中表述數組和它們的成員。
- 可以通過顯式的指針形式double *p,或靜態/自動形式double p[100]來聲明數組。
- 不管是哪種情況,第n + 1個數組成員都是p[n]。不要忘了第一項是0而不是1,這樣就可以采用特殊形式p[0] == *p。
- 如果需要第n個成員的地址(而不是實際值),使用&符號:&p[n]。當然,第1個成員的地址就是&p[0] == p。
例1展示了這些規則的一些實際應用。
例1 一些簡單的指針運算(arithmetic.c)

? 使用特殊形式*evens寫入到evens[0]。
? 成員1的地址,賦值給一個新指針。
? 引用數組第1個成員的通常方式。
下面我再送你一個很好的技巧,這個技巧建立在指針運算規則“p+1表示數組中下一個成員的地址(&p[1])”的基礎上。根據這個規則,我們不需要在遍歷數組的循環中使用下標。在例2中我們就使用了一個備用指針來指向list的頭部,然后用p++在數組中向前遍歷,直到數組尾部的NULL標記,從而獲得了整個數組值。如果你查看了接下來的指針聲明的提示,會更容易理解這種用法。
例2我們可以利用p++表示“前進到下一個指針”實現循環的流水化

自己動手
如果不了解p++,你打算怎樣實現這個目標?
如果目標是為了實現簡潔的語法表示形式,基地址加偏移量這個技巧并不能提供太多的幫助,但它確實解釋了C的許多工作原理。事實上,我們可以考慮一下使用結構,例如:

作為一種智力模型來分析,我們可以把list看成是基地址,list[0].b與基地址的距離正好用來表示b。也就是說,假設list的位置是整數(size_t)&list,b位于(size_t)&list + sizeof(int);,這樣list[2].d的位置將是(size_t)&list + 6*sizeof(int) + 5*sizeof(double)。根據這種思路,結構就與數組非常相似了,區別是結構的成員是用名稱而不是序號表示的,并且它們具有不同的類型和長度。
這個思路并不是非常正確,因為存在對齊這個因素,系統可能會決定數據需要位于某個特定長度的內存塊中,因此字段尾部可能會填充一些額外的空間,使下一個字符從正確的位置開始,并且結構的尾部可能也會進行填充,使結構列表中的每個結構能夠大致對齊[C99和C11,§6.7.2.1(15)和(17)]。stddef.h頭文件定義了offsetof宏,它精確地描述了基地址加領偏移量的思路:list[2].d的實際地址是(size_t)&list + 2*sizeof(abcd_s) + offsetof(abcd_s, d)。
順便說一下,在結構的起始處不可能出現填充,因此list[2].a肯定等于(size_t)&list+ 2*sizeof(abcd_s)。
下面是個笨拙的函數,它以遞歸的方式對列表中的成員進行計數,直到遇到值為0的成員。假設我們想把這個函數用于零值為合理數據的任何類型的列表,因此我們讓它接受一個void指針(當然這不是一種好的思路)。

基地址加偏移量的規則解釋了為什么這種做法是不行的。為了表示a_list[1],編譯器需要知道a_list[0]的準確長度,這樣才能知道應該從基地址偏移多少。但是,由于沒有與之相關聯的類型,它無法計算這個長度。
typedef作為一種教學工具
任何時候當我們遇到一種復雜的類型時,類似于指向某種類型的指針的指針的指針等情況,可以考慮用typedef進行簡化。
例如,下面這個常見的定義:

有效地減少了字符串數組的視覺混亂,使它們的意圖變得清晰。
在前面的指針運算p++例子中,char *list[]這樣的聲明是否很清楚地告訴你它表示一個字符串列表而*p是一個字符串?
例3對例2的for循環進行了重寫,用string替換了char *。
例3 添加一個typedef聲明使笨拙的代碼稍稍變得清晰

list的聲明行現在變得簡單,很清晰地表示它是個字符串列表,并且string *p也很清晰地表示p是個指向字符串的指針。因此,*p表示一個字符串。
最后,我們仍然需要記住字符串是個指向字符的指針。例如,NULL是個合法的字符串值。
我們甚至可以更進一步,例如使用上面的typedef加上typedef stringlist string*,聲明一個字符串的二維數組。這種方法有時候非常實用,但有時候只會增加記憶的負擔。
從概念上講,函數類型的語法實際上是指向一個特定類型的函數的指針。如果我們有一個頭部類似下面這樣的函數:

然后只要添加一個星號(并加上括號以保證優先級),就可以描述一個指向這種類型的函數的指針:

然后在前面加上typedef來定義一種類型:

現在我們可以把它當作一種類型使用,例如聲明一個接受另一個函數作為其輸入參數的函數,可以這樣:

通過對函數指針類型的重新定義,那些接受其他函數作為輸入的函數的表達—其中連環星號的書寫曾是令人生畏的考驗變得不再可怕。
最后需要說明的是,指針實際上要比教科書所描述的簡單得多,因為它實際上只是一個位置或別名,根本不需要涉及不同類型的內存管理。像指向字符串的指針的指針這樣的復雜構造總是會讓人感到迷惑,但這只不過是因為我們以狩獵為生的祖先從來沒有見到過這玩意而已。至少,C提供了typedef這個工具來處理它們。
本文節選自《C程序設計新思維》