【感謝@Hacker_YHJ 的熱心翻譯。如果其他朋友也有不錯的原創或譯文,可以嘗試推薦給伯樂線上。】
關於C語言中指標和陣列的爭論就像是一場惡戰。一方面,有些人覺得,所有人都必須承認指標與陣列是不同的。而另一些人則認為陣列被當成指標來處理,因此它們不應該有什麼區別。這種現象讓人迷惑。然而,這兩種說法其實都是正確的。
陣列不是指標,指標也不能說是陣列。在C語言中,指標僅在記憶體中代表一個地址,而陣列是許多連續的記憶體塊,多個型別相似的元素儲存在其中。更深入的解釋,請參考我之前的博文《C語言記憶體地址》。在C語言的絕大多數情況下,陣列被當作指標來處理,這也是使人困惑的地方。
陣列表示法vs指標表示法
陣列被當作指標來處理,具體指的下面兩條:
- 陣列名變數代表了陣列中第一個元素的地址。它並不是一個指標,但卻表現得像一個不能被修改的常指標一樣。
- 程式在與陣列互動的時候,用指標表示法代替陣列表示法。
我們來看點程式碼吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 初始化陣列 int numbers[5] = {1,2,3,4,5}; // 標準陣列表示法 int *ptr1 = numbers; int val1 = numbers[0]; // 陣列表示法取地址 int *ptr2 = &numbers[0]; int val2 = *(&numbers[0]); // 指標加偏移表示法 int *ptr3 = numbers + 0; int val3 = *(numbers + 0); // 輸出指標中的地址 printf("*ptr1 = %p\n", (void *)ptr1); printf("*ptr2 = %p\n", (void *)ptr2); printf("*ptr3 = %p\n", (void *)ptr3); // 輸出地址指向的int值 printf("val1 = %d\n", val1); printf("val2 = %d\n", val1); printf("val3 = %d\n", val1); |
我們宣告瞭一個包含5個int的陣列,並將陣列名變數numbers賦給了一個int指標,ptr1。numbers代表了這個陣列第一個元素的地址,將其賦給ptr1正是把它當成了指標來使用。接著我們用陣列表示法訪問了第一個元素的值。
第二個例子中,我們用陣列表示法取了陣列中第一個元素的地址,之後我們用解引用第一個元素所在地址的方法訪問了它。
第三個例子中,我們用指標運算將陣列中第一個元素的地址賦值給ptr3,之後我們解引用相同的地址來得到它的值。
最後我們將所有儲存在指標中的地址和所有在這些地址的int值輸出到螢幕上。執行這段程式碼,你會得到類似下面的輸出:
1 2 3 4 5 6 |
*ptr1 = 0x7fff6be1de60 *ptr2 = 0x7fff6be1de60 *ptr3 = 0x7fff6be1de60 val1 = 1 val2 = 1 val3 = 1 |
所有值都是相同的。接下來再看看下面的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 初始化陣列 int numbers[5] = {1,2,3,4,5}; int i = 0; // 用陣列表示法輸出元素 for (i = 0; i < 5; i++ ) { int value = numbers[i]; printf("numbers[%d] = %d\n", i, value); } // 用指標加偏移輸出元素(真討厭) for (i = 0; i < 5; i++ ) { int value = *(numbers + i); printf("*(numbers + %d) = %d\n", i, value); } // 僅用一個指標輸出元素 int *ptr = numbers; for (i = 0; i < 5; i++ ) { int value = *ptr++; printf("%d, *ptr++ = %d\n", i, value); } |
執行它,你會得到以下輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
numbers[0] = 1 numbers[1] = 2 numbers[2] = 3 numbers[3] = 4 numbers[4] = 5 *(numbers + 0) = 1 *(numbers + 1) = 2 *(numbers + 2) = 3 *(numbers + 3) = 4 *(numbers + 4) = 5 0, *ptr++ = 1 1, *ptr++ = 2 2, *ptr++ = 3 3, *ptr++ = 4 4, *ptr++ = 5 |
就像你看到的那樣,所有過程得到了相同的結果。
陣列表示法實際上就是指標運算。C語言標準只是將numbers[0]定義為*(numbers + 0)的語法糖。(譯者注:語法糖,它意指那些沒有給計算機語言新增新功能,而只是對人類來說更容易理解的語法。)無論何時,你寫下一個陣列表示法,比方說numbers[2],都會被編譯器轉換為*(numbers + 2)。這裡,numbers表示陣列中第一個元素的地址,+2則表示用於指標運算的偏移量。
陣列變數
我們已經展示了,陣列常被當作指標來處理,而且對於C編譯器而言陣列表示法就是指標運算。一些人自然而然地就做出了這樣的假設:既然陣列能被當成指標,指標也應該能賦值給陣列。這是不對的,陣列名變數不能被改變。我們看看下面的程式碼吧。
1 2 3 4 5 6 7 8 9 |
// 初始化陣列 int numbers[5] = {1,2,3,4,5}; int numbers2[5] = {6,7,8,9,0}; int *ptr = numbers2; // 這不能通過編譯 numbers = numbers2; numbers = &numbers2; numbers = ptr; |
這段程式碼不能通過編譯。試一試,你會得到以下輸出。
1 2 3 |
incompatible types when assigning to type ‘int[5]’ from type ‘int *’ incompatible types when assigning to type ‘int[5]’ from type ‘int (*)[5]’ incompatible types when assigning to type ‘int[5]’ from type ‘int *’ |
雖然陣列名變數代表了陣列第一個元素的地址,它卻表現得像一個不能被更改的常指標一樣。它不能接受一個別的陣列名變數或是指向另一個陣列的指標的賦值。思考一下,如果你有一個陣列名變數A代表一個陣列,而且你能夠改變A的地址,那被A指向的記憶體將會發生生麼?
接下來看看這段能夠編譯的程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 初始化陣列 int numbers[5] = {1,2,3,4,5}; int numbers2[5] = {6,7,8,9,0}; int *ptr1 = numbers; int *ptr2 = numbers2; // 能通過編譯 ptr1 = ptr2; // 輸出地址 printf("numbers = %p\n", (void *)numbers); printf("numbers2 = %p\n", (void *)numbers2); printf("ptr1 = %p\n", (void *)ptr1); printf("ptr2 = %p\n", (void *)ptr2); 它會輸出這樣的結果: numbers = 0x7fff5ea3d230 numbers2 = 0x7fff5ea3d250 ptr1 = 0x7fff5ea3d250 ptr2 = 0x7fff5ea3d250 |
雖然不能直接改變陣列名變數,我們仍然改變一個指向這個陣列的指標。程式碼中,我們建立了兩個陣列,兩個int指標。我們將numbers賦給了ptr1,將numbers2賦給了ptr2。接著我們將ptr2賦給了ptr1,最後輸出結果。可以看到,ptr1和ptr2都指向了numbers2陣列的第一個元素。
總結
我希望你們能夠喜歡這篇對C語言中陣列和指標的概述。我們沒有囊括關於指標和陣列的一切知識,但足以作為一個開始。跟往常一樣,我非常願意接受大家的評論和建議。