學習 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;
}
執行結果如下:
0x7fff15d92640
0x7fff15d92640
0x7fff15d92640
的確可以看到指向的位址都相同, 就字面上來看, &a
是陣列的位址, 而 &a[0]
是陣列第一個元素的位址, 所以這兩個位址相同並沒有問題, 但是為什麼 a
也會是陣列的位址呢?
這是因為在 C 語言的陣列轉指位器規則(C++ 文件)裡明定運算結果是一個陣列, 而且這個運算結果不是 sizeof 也不是取址 & 運算器的運算元時, 會自動將運算結果轉換成指向陣列第一個元素的指位器, 因此, 剛剛範例中的 a
就被轉換成 &(a[0])
了, 如下圖所示:
a
|
&a --> +-|-+---+---+---+---+---+---+---+---+---+
| V | | | | | | | | | |
+---+---+---+---+---+---+---+---+---+---+
0 1 2 3 4 5 6 7 8 9
你可以透過以下範例確認 a
和 &(a[0])
是相等的 (由於 []
優先權大於 &
, 所以一般會寫成 &a[0]
):
#include <stdio.h>
int main(){
int a[10];
printf("%d\n", a==&a[0]);
return 0;
}
執行結果就是比較成立得到的運算結果 1:
1
你可能會想『咦?那麼難道 a
和 &a
不相等嗎?不是都指向同樣的位址?』我們可以透過以下的範例試試看:
#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
是指向不同型別的指位器, 不應該拿來比較, 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;
}
範例中的三種方式都可以取得正確的元素:
4
4
4
其實根據 []
運算器的說明(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;
}
這裡比較特別的地方是 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;
}
也可以得到一樣的結果, 不過其中有個細微的差異, 我們透過以下範例來說明:
#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
與 &
的運算元時, 會被視為是指向以第一個維度為視角的一維陣列中的第一個元素, 我們以底下的範例來說明:
#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 個指位器的大小, 而不是一個一維陣列的大小了。
傳遞陣列到引數
瞭解陣列被轉換成指位器的原理後, 現在你就可以知道為什麼傳遞陣列到函數時不是傳遞整個陣列, 而是傳遞位址了, 精確的來說, 實際上傳遞的不是陣列的位址, 而是陣列中第一個元素的位址, 例如:
#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;
}
在函式中之所以參數 pa
是 int*
型別, 就是因為叫用 f
時, a
會被轉換成 &a[0]
, 這是一個 int*
型別的資料。
利用這樣的思考方式, 在宣告變數時, 對於要接收陣列的參數, 就可以很清楚的知道該如何表示它的型別了。
結語
透過以上的說明, 希望大家都能了解陣列的實際運作方式, 相同的原理, 可以推展到更多維的陣列, 以後看到再奇怪的用法, 也能夠依循本文說明的規則, 弄清楚程式的意圖。
Top comments (0)