DEV Community

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

Posted on • Edited on

4 1 1

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* 型別的資料。

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

結語

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

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (1)

Collapse
 
marta_gonzales_94ba2bdb96 profile image
Marta Gonzales

wwwwwww

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay