C 語言的陣列與指位器 (pointer)
Tags: c, cpp
Published: 2022-02-03 12:02:31

學習 C 語言的過程中一定都多少聽過陣列與指位器 (pointer) 可以互換使用, 但其實內藏玄虛, 若不注意, 就會誤踩陷阱。

## 位址相同但語意不同的指位器

對於一個陣列, 有多種方式可以取得開頭的位址, 例如以下的範例：

```c
#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 語言的[陣列轉指位器規則](https://en.cppreference.com/w/c/language/array#Array_to_pointer_conversion)裡明定**運算結果是一個陣列, 而且這個運算結果不是 sizeof 也不是取址 & 運算器的運算元時, 會自動將運算結果轉換成指向陣列第一個元素的指位器**, 因此, 剛剛範例中的 `a` 就被轉換成 `&(a[0])` 了, 如下圖所示：

```
         a
         |
&a --> +-|-+---+---+---+---+---+---+---+---+---+
       | V |   |   |   |   |   |   |   |   |   |
       +---+---+---+---+---+---+---+---+---+---+
         0   1   2   3   4   5   6   7   8   9
```

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

```C
#include <stdio.h>

int main(){
  int a[10];

  printf("%d\n", a==&a[0]);

  return 0;
}
```

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

```
1
```

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

```c
#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` 是[指向不同型別的指位器, 不應該拿來比較](http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf#page=99), `a` 因為是指向陣列第一個元素的指位器, 所以型別是 `int*`, 但是 `&a` 是指向陣列的指位器, 所以型別是 `int (*)[10]`, 兩者雖然指向相同的位址, 但是語意上並不相同。

註：以 gcc 編譯時, 只會出現警告, 程式仍可編譯執行, 但以 g++ 編譯則會出現錯誤, 停止編譯程序。

## 以陣列索引取址就是透過指位器間接取值

瞭解上述規則後, 我們就可以來看如何從陣列中取值, 請看以下範例：

```C
#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
```

其實根據 [`[]` 運算器的說明](http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf#page=82), `a[3]` 實際的運算等同於 `*(a + 3)`, 而 [`+` 運算器的說明](http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf#page=95)則規定了指位器與整數的加減是以元素為單位, 因此 `a + 3` 就會是指向陣列 `a` 中的第 3 個元素, 所以 `*(a + 3)` 就可以取得 `a` 的第 3 個元素了。

也就是說, 編譯器實際上就是把陣列當成指位器處理, 兩者是等義的。

## 不完整的型別 (incomplete type)

如果想要取得指向陣列的指位器, 可以依照剛剛錯誤訊息看到的型別宣告：

```C
#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]` 相等。

其實在宣告指向陣列的指位器時, 並不需要明確標示元素個數, 像是這樣：

```C
#include <stdio.h>

int main(){
  int a[10];
  int (*p)[] = &a;

  printf("%d\n", *p==&a[0]);

  return 0;
}
```

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

```C
#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` 與 `&` 的運算元時, 會被視為是指向以第一個維度為視角的一維陣列中的第一個元素, 我們以底下的範例來說明：

```c
#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 個指位器的大小, 而不是一個一維陣列的大小了。

## 結語

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