DEV Community

codemee
codemee

Posted on

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

學習 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 語言的運算式轉換規則裡關於非數學運算元的第三條規則明定運算結果是一個陣列, 而且這個運算結果不是 sizeof 也不是取址 & 運算器的運算元時, 會自動將運算結果轉換成指向陣列第一個元素的指位器, 因此, 剛剛範例中的 a 就被轉換成 &a[0] 了。

你可以透過以下範例確認 a&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

其實根據 [] 運算器的說明, a[3] 實際的運算等同於 *(a + 3), 而 + 運算器的說明則規定了指位器與整數的加減是以元素為單位, 因此 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 無法判定資料大小, 所以引發編譯錯誤。

結語

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

Discussion (0)