DEV Community

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

Posted on • Updated 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 (0)