DEV Community

Cover image for C/C++ 語言的陣列與指位器 (pointer)
codemee
codemee

Posted on • Edited on

C/C++ 語言的陣列與指位器 (pointer)

學習 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; } 
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

0x7fff15d92640 0x7fff15d92640 0x7fff15d92640 
Enter fullscreen mode Exit fullscreen mode

的確可以看到指向的位址都相同, 就字面上來看, &a 是陣列的位址, 而 &a[0] 是陣列第一個元素的位址, 所以這兩個位址相同並沒有問題, 但是為什麼 a 也會是陣列的位址呢?

這是因為在 C 語言的陣列轉指位器規則(C++ 文件)裡明定運算結果是一個陣列, 而且這個運算結果不是 sizeof 也不是取址 & 運算器的運算元時, 會自動將運算結果轉換成指向陣列第一個元素的指位器, 因此, 剛剛範例中的 a 就被轉換成 &(a[0]) 了, 如下圖所示:

 a | &a --> +-|-+---+---+---+---+---+---+---+---+---+ | V | | | | | | | | | | +---+---+---+---+---+---+---+---+---+---+ 0 1 2 3 4 5 6 7 8 9 
Enter fullscreen mode Exit fullscreen mode

你可以透過以下範例確認 a&(a[0]) 是相等的 (由於 [] 優先權大於 &, 所以一般會寫成 &a[0]):

#include <stdio.h>  int main(){ int a[10]; printf("%d\n", a==&a[0]); return 0; } 
Enter fullscreen mode Exit fullscreen mode

執行結果就是比較成立得到的運算結果 1:

1 
Enter fullscreen mode Exit fullscreen mode

你可能會想『咦?那麼難道 a&a 不相等嗎?不是都指向同樣的位址?』我們可以透過以下的範例試試看:

#include <stdio.h>  int main(){ int a[10]; printf("%d\n", a==&a); return 0; } 
Enter fullscreen mode Exit fullscreen mode

事實上這個程式連編譯都會出現錯誤訊息:

$ 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); | ~^~~~ 
Enter fullscreen mode Exit fullscreen mode

編譯器認為 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; } 
Enter fullscreen mode Exit fullscreen mode

範例中的三種方式都可以取得正確的元素:

4 4 4 
Enter fullscreen mode Exit fullscreen mode

其實根據 [] 運算器的說明(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; } 
Enter fullscreen mode Exit fullscreen mode

這裡比較特別的地方是 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; } 
Enter fullscreen mode Exit fullscreen mode

也可以得到一樣的結果, 不過其中有個細微的差異, 我們透過以下範例來說明:

#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; } 
Enter fullscreen mode Exit fullscreen mode

這個程式在編譯時就會出錯, 錯誤訊息如下:

$ 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); | ^~~~~~~~~~ 
Enter fullscreen mode Exit fullscreen mode

由於宣告時是 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; } 
Enter fullscreen mode Exit fullscreen mode

由於 num 是一個 3×4 維的陣列, 以第一個維度來看, 就是有 3 個元素的一維陣列, 其中每個元素個別是一個內含 4 個元素的一維陣列, 所以 num 會被編譯器視為是指向第一個一維陣列的指位器, &num 仍是指向整個陣列, 而 num[0] 指的是第一個一維陣列, 不過它沒有作為 sizeof 或是 & 的運算元, 因此會被編譯器轉換成指向陣列內第一個元素的指位器, 如下圖所示:

 num[0] | &num --> +-|-+---+---+---+ num --> | v | | | | 0 +---+---+---+---+ num+1 --> | | | | | 1 +---+---+---+---+ num+2 --> | ^ | | | | 2 +-|-+---+---+---+ | num[2] 
Enter fullscreen mode Exit fullscreen mode

剛剛程式的輸出結果如下:

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 
Enter fullscreen mode Exit fullscreen mode

你可以看到 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; } 
Enter fullscreen mode Exit fullscreen mode

在函式中之所以參數 paint* 型別, 就是因為叫用 f 時, a 會被轉換成 &a[0], 這是一個 int* 型別的資料。

利用這樣的思考方式, 在宣告變數時, 對於要接收陣列的參數, 就可以很清楚的知道該如何表示它的型別了。

結語

透過以上的說明, 希望大家都能了解陣列的實際運作方式, 相同的原理, 可以推展到更多維的陣列, 以後看到再奇怪的用法, 也能夠依循本文說明的規則, 弄清楚程式的意圖。

Top comments (1)

Collapse
 
marta_gonzales_94ba2bdb96 profile image
Marta Gonzales

wwwwwww