先看個例子,有如下語句:
int a[10]; int *pa = &a[0]; pa++;
首先指針pa
指向a[0]
的地址,注意尾碼運算符的優先順序高於單目運算符,所以是取a[0]
的地址,而不是取a
的地址。然後pa++
讓pa
指向下一個元素(也就是a[1]
),由於pa
是int *
指針,一個int
型元素占4個位元組,所以pa++
使pa
所指向的地址加4,注意不是加1。
下面畫圖理解。從前面的例子我們發現,地址的具體數值其實無關緊要,關鍵是要說明地址之間的關係(a[1]
位於a[0]
之後4個位元組處)以及指針與變數之間的關係(指針保存的是變數的地址),現在我們換一種畫法,省略地址的具體數值,用方框表示存儲空間,用箭頭表示指針和變數之間的關係。
既然指針可以用++
運算符,當然也可以用+
、-
運算符,pa+2
這個表達式也是有意義的,如上圖所示,pa
指向a[1]
,那麼pa+2
指向a[3]。事實上,E1[E2]
這種寫法和(*((E1)+(E2)))
是等價的,*(pa+2)
也可以寫成pa[2]
,pa
就像數組名一樣,其實數組名也沒有什麼特殊的,a[2]
之所以能取數組的第2個元素,是因為它等價于*(a+2)
,在第 1 節 “數組的基本概念”講過數組名做右值時自動轉換成指向首元素的指針,所以a[2]
和pa[2]
本質上是一樣的,都是通過指針間接定址訪問元素。由於(*((E1)+(E2)))
顯然可以寫成(*((E2)+(E1)))
,所以E1[E2]
也可以寫成E2[E1]
,這意味着2[a]
、2[pa]
這種寫法也是對的,但一般不這麼寫。另外,由於a
做右值使用時和&a[0]
是一個意思,所以int *pa = &a[0];
通常不這麼寫,而是寫成更簡潔的形式int *pa = a;
。
在第 1 節 “數組的基本概念”還講過C語言允許數組下標是負數,現在你該明白為什麼這樣規定了。在上面的例子中,表達式pa[-1]
是合法的,它和a[0]
表示同一個元素。
現在猜一下,兩個指針變數做比較運算(>
、>=
、<
、<=
、==
、!=
)表示什麼意義?兩個指針變數做減法運算又表示什麼意義?
根據什麼來猜?根據第 3 節 “形參和實參”講過的Rule of Least Surprise原則。你理解了指針和常數加減的概念,再根據以往使用比較運算的經驗,就應該猜到pa + 2 > pa
,pa - 1 == a
,所以指針之間的比較運算比的是地址,C語言正是這樣規定的,不過C語言的規定更為嚴謹,只有指向同一個數組中元素的指針之間相互比較才有意義,否則沒有意義。那麼兩個指針相減表示什麼?pa - a
等於幾?因為pa - 1 == a
,所以pa - a
顯然應該等於1,指針相減表示兩個指針之間相差的元素個數,同樣只有指向同一個數組中元素的指針之間相減才有意義。兩個指針相加表示什麼?想不出來它能有什麼意義,因此C語言也規定兩個指針不能相加。假如C語言為指針相加也規定了一種意義,那就相當Surprise了,不符合一般的經驗。無論是設計編程語言還是設計函數介面或人機界面都是這個道理,應該儘可能讓用戶根據以往的經驗知識就能推斷出該系統的基本用法。
在取數組元素時用數組名和用指針的語法一樣,但如果把數組名做左值使用,和指針就有區別了。例如pa++
是合法的,但a++
就不合法,pa = a + 1
是合法的,但a = pa + 1
就不合法。數組名做右值時轉換成指向首元素的指針,但做左值仍然表示整個數組的存儲空間,而不是首元素的存儲空間,數組名做左值還有一點特殊之處,不支持++
、賦值這些運算符,但支持取地址運算符&
,所以&a
是合法的,我們將在第 7 節 “指向數組的指針與多維數組”介紹這種語法。
在函數原型中,如果參數是數組,則等價于參數是指針的形式,例如:
void func(int a[10]) { ... }
等價于:
void func(int *a) { ... }
第一種形式方括號中的數字可以不寫,仍然是等價的:
void func(int a[]) { ... }
參數寫成指針形式還是數組形式對編譯器來說沒區別,都表示這個參數是指針,之所以規定兩種形式是為了給讀代碼的人提供有用的信息,如果這個參數指向一個元素,通常寫成指針的形式,如果這個參數指向一串元素中的首元素,則經常寫成數組的形式。