學習 C/C++ 語言的過程中一定都多少聽過陣列與指位器 (pointer) 可以互換使用, 但其實內藏玄虛, 若不注意, 就會誤踩陷阱。
位址相同但語意不同的指位器
對於一個陣列, 有多種方式可以取得開頭的位址, 例如以下的範例:
#include <stdio.h> int main(){ int a[10]; printf("%p\n", a); printf("%p\n", &a); printf("%p\n", &a[0]); return 0; }
執行結果如下:
0x7fff15d92640 0x7fff15d92640 0x7fff15d92640
的確可以看到指向的位址都相同, 就字面上來看, &a
是陣列的位址, 而 &a[0]
是陣列第一個元素的位址, 所以這兩個位址相同並沒有問題, 但是為什麼 a
也會是陣列的位址呢?
這是因為在 C 語言的陣列轉指位器規則(C++ 文件)裡明定運算結果是一個陣列, 而且這個運算結果不是 sizeof 也不是取址 & 運算器的運算元時, 會自動將運算結果轉換成指向陣列第一個元素的指位器, 因此, 剛剛範例中的 a
就被轉換成 &(a[0])
了, 如下圖所示:
a | &a --> +-|-+---+---+---+---+---+---+---+---+---+ | V | | | | | | | | | | +---+---+---+---+---+---+---+---+---+---+ 0 1 2 3 4 5 6 7 8 9
你可以透過以下範例確認 a
和 &(a[0])
是相等的 (由於 []
優先權大於 &
, 所以一般會寫成 &a[0]
):
#include <stdio.h> int main(){ int a[10]; printf("%d\n", a==&a[0]); return 0; }
執行結果就是比較成立得到的運算結果 1:
1
你可能會想『咦?那麼難道 a
和 &a
不相等嗎?不是都指向同樣的位址?』我們可以透過以下的範例試試看:
#include <stdio.h> int main(){ int a[10]; printf("%d\n", a==&a); return 0; }
事實上這個程式連編譯都會出現錯誤訊息:
$ g++ test.c test.c: In function ‘int main()’: test.c:6:19: error: comparison between distinct pointer types ‘int*’ and ‘int (*)[10]’ lacks a cast [-fpermissive] 6 | printf("%d\n", a==&a); | ~^~~~
編譯器認為 a
和 &a
是指向不同型別的指位器, 不應該拿來比較, a
因為是指向陣列第一個元素的指位器, 所以型別是 int*
, 但是 &a
是指向陣列的指位器, 所以型別是 int (*)[10]
, 兩者雖然指向相同的位址, 但是語意上並不相同。
註:以 gcc 編譯時, 只會出現警告, 程式仍可編譯執行, 但以 g++ 編譯則會出現錯誤, 停止編譯程序。
以陣列索引取址就是透過指位器間接取值
瞭解上述規則後, 我們就可以來看如何從陣列中取值, 請看以下範例:
#include <stdio.h> int main(){ int a[] = {1, 2, 3, 4, 5}; int *p = a; printf("%d\n", a[3]); printf("%d\n", p[3]); printf("%d\n", *(p + 3)); return 0; }
範例中的三種方式都可以取得正確的元素:
4 4 4
其實根據 []
運算器的說明(C++ 的文件), a[3]
實際的運算等同於 *(a + 3)
, 而 +
運算器的說明(C++ 文件)則規定了指位器與整數的加減是以元素為單位, 因此 a + 3
就會是指向陣列 a
中的第 3 個元素, 所以 *(a + 3)
就可以取得 a
的第 3 個元素了。
也就是說, 編譯器實際上就是把陣列當成指位器處理, 兩者是等義的。
不完整的型別 (incomplete type)
如果想要取得指向陣列的指位器, 可以依照剛剛錯誤訊息看到的型別宣告:
#include <stdio.h> int main(){ int a[10]; int (*p)[10] = &a; printf("%d\n", *p==&a[0]); return 0; }
這裡比較特別的地方是 p
是指向陣列的指位器, 所以 *p
就是陣列本人, 依據前述規則, *p
會被轉換為指向陣列第一個元素的指位器, 因此 *p
和 &a[0]
相等。
其實在宣告指向陣列的指位器時, 並不需要明確標示元素個數, 像是這樣:
#include <stdio.h> int main(){ int a[10]; int (*p)[] = &a; printf("%d\n", *p==&a[0]); return 0; }
也可以得到一樣的結果, 不過其中有個細微的差異, 我們透過以下範例來說明:
#include <stdio.h> int main(){ int a[10]; int (*p1)[10] = &a; int (*p2)[] = &a; printf("%d\n", sizeof a); printf("%d\n", sizeof *p1); printf("%d\n", sizeof *p2); return 0; }
這個程式在編譯時就會出錯, 錯誤訊息如下:
$ g++ test.c test.c: In function ‘int main()’: test.c:10:18: error: invalid application of ‘sizeof’ to incomplete type ‘int []’ 10 | printf("%d\n", sizeof *p2); | ^~~~~~~~~~
由於宣告時是 int (*p2)[]
沒有指明元素個數, 編譯器認為資訊不完整, 這稱為『不完整的型別 (incomplete type)』, 會導致 sizeof
無法判定資料大小, 所以引發編譯錯誤。
多維陣列與指位器的關係
上述的說明也一樣可以使用在多維陣列上, 首先, 陣列本身出現在非 sizeof
與 &
的運算元時, 會被視為是指向以第一個維度為視角的一維陣列中的第一個元素, 我們以底下的範例來說明:
#include<stdio.h> int main(void) { int num[3][4]; // 宣告3×4的二維陣列num printf("%s%p\n", "num =", num); // 印出 num[0] 一維陣列的位址 printf("%s%p\n", "&num =", &num); // 印出 num 陣列的位址 printf("%s%p\n", "num[0] =", &num); // 印出 num[0][0] 的位址 printf("%s%p\n", "*num =", *num); // num 被視為指向 num[0] 這個一維陣列的指標 // *num 就等同 num[0], 視為指向 num[0][0] 的指標 printf("%s%p\n", "num+1 =", num + 1); // num[0] 一維陣列的位址 + 4個 int 的長度 printf("%s%p\n", "&num+1 =", &num + 1); // num 陣列的位址 + 3*4 個 int 的長度 printf("%s%p\n", "num[0]+1=", num[0] + 1); // num[0][0]的位址 + int 的長度 printf("%s%p\n", "*num+1 =", *num + 1); // *num 等同 num[0] return 0; }
由於 num
是一個 3×4 維的陣列, 以第一個維度來看, 就是有 3 個元素的一維陣列, 其中每個元素個別是一個內含 4 個元素的一維陣列, 所以 num
會被編譯器視為是指向第一個一維陣列的指位器, &num
仍是指向整個陣列, 而 num[0]
指的是第一個一維陣列, 不過它沒有作為 sizeof
或是 &
的運算元, 因此會被編譯器轉換成指向陣列內第一個元素的指位器, 如下圖所示:
num[0] | &num --> +-|-+---+---+---+ num --> | v | | | | 0 +---+---+---+---+ num+1 --> | | | | | 1 +---+---+---+---+ num+2 --> | ^ | | | | 2 +-|-+---+---+---+ | num[2]
剛剛程式的輸出結果如下:
num =000000ca16fff6f0 ----+ &num =000000ca16fff6f0 ----+--+ num[0] =000000ca16fff6f0 ----+--+--+ *num =000000ca16fff6f0 | | | num+1 =000000ca16fff700 <-+16 | | &num+1 =000000ca16fff720 <-+48--+ | num[0]+1=000000ca16fff6f4 <-+4------+ *num+1 =000000ca16fff6f4
你可以看到 num
, &num
, num[0]
都會指向同一個位址, 但是他們的語意並不相同, 也就是他們的型別並不相同, 這可以透過對指位器進行加減運算來驗證:
-
num
指向的是有 4 個 int 元素的一維陣列, 型別是
int (*)[4]
所以
num+1
時位址會加上 4×4, 也就是 16。 -
&num
指向的是 3×4 的陣列, 型別是
int (*)[3][4]
所以
&num+1
時位址是加上 3×4×4, 也就是 48。 -
num[0]
是指向一維陣列中的元素, 也就是指向 int, 型別是
int*
因此
num[0]+1
是加上 4。 比較特別的是
*num
, 因為num
是指向一維陣列, 所以*num
就是一維陣列本人, 由於這裡不是sizeof
與&
的運算元, 所以*num
會被編譯器轉換成指向這個一維陣列內第一個元素的指位器, 因此*num
就等同num[0]
了。
這樣的設計使得二維陣列運作起來很像是雙重指位器, 有些教學就直接把 num
當成雙重指位器, 這在部分情況會得到正確的結果, 但是某些情況下卻會有問題, 例如做指位器加減運算時, 若是雙重指位器由於指向的是指位器, num+1
就應該是加 1 個指位器的大小, 而不是一個一維陣列的大小了。
傳遞陣列到引數
瞭解陣列被轉換成指位器的原理後, 現在你就可以知道為什麼傳遞陣列到函數時不是傳遞整個陣列, 而是傳遞位址了, 精確的來說, 實際上傳遞的不是陣列的位址, 而是陣列中第一個元素的位址, 例如:
#include<stdio.h> void f(int *pa, int len) { for(int i=0;i < len;i++) { printf("pa[%d]:%d\n", i, pa[i]); } } int main() { int a[] = {1,2,3}; f(a, sizeof(a)/sizeof(int)); return 0; }
在函式中之所以參數 pa
是 int*
型別, 就是因為叫用 f
時, a
會被轉換成 &a[0]
, 這是一個 int*
型別的資料。
利用這樣的思考方式, 在宣告變數時, 對於要接收陣列的參數, 就可以很清楚的知道該如何表示它的型別了。
結語
透過以上的說明, 希望大家都能了解陣列的實際運作方式, 相同的原理, 可以推展到更多維的陣列, 以後看到再奇怪的用法, 也能夠依循本文說明的規則, 弄清楚程式的意圖。
Top comments (1)
wwwwwww