指標與陣列


在宣告陣列之後,使用到陣列變數時,會取得首元素的位址,例如在下面的程式中將指出,陣列 arr&arr[0] 的值是相同的:

#include <stdio.h>

int main(void) {
    int arr[10] = {0};

    printf("arr :\t\t%p\n", arr);
    printf("&arr[0] :\t%p\n", &arr[0]);

    return 0;
}

執行結果:

arr :           0061FEA8 
&arr[0] :       0061FEA8

之前也曾經談過,陣列索引其實是相對於首元素位址的位移量,下面這個程式以指標運算與陣列索引操作,顯示出相同的對應位址值:

#include <stdio.h>
#define LEN 10

int main(void) {
    int arr[LEN] = {0};
    int *p = arr;

    for(int i = 0; i < LEN; i++) {
        printf("&arr[%d]: %p", i ,&arr[i]);
        printf("\t\tptr + %d: %p\n", i, p + i);
    }

    return 0;
}

每個元素的位址型態是 int*,執行結果如下:

&arr[0]: 0061FEA0               ptr + 0: 0061FEA0
&arr[1]: 0061FEA4               ptr + 1: 0061FEA4
&arr[2]: 0061FEA8               ptr + 2: 0061FEA8
&arr[3]: 0061FEAC               ptr + 3: 0061FEAC
&arr[4]: 0061FEB0               ptr + 4: 0061FEB0
&arr[5]: 0061FEB4               ptr + 5: 0061FEB4
&arr[6]: 0061FEB8               ptr + 6: 0061FEB8
&arr[7]: 0061FEBC               ptr + 7: 0061FEBC
&arr[8]: 0061FEC0               ptr + 8: 0061FEC0
&arr[9]: 0061FEC4               ptr + 9: 0061FEC4

在這個程式中,將陣列的首元素位址指定給 p,然後對 p 遞增運算,每遞增一個單位,陣列相對應索引的元素之位址都相同。

也可以利用指標運算來取出陣列的元素值,如以下的程式所示:

#include <stdio.h>
#define LEN 5

int main(void) {
    int arr[LEN] = {10, 20, 30, 40, 50};
    int *p = arr;

    // 以指標方式存取 
    for(int i = 0; i < LEN; i++) {
        printf("*(p + %d): %d\n", i , *(p + i));
    }
    putchar('\n');

    // 以指標方式存取資料 
    for(int i = 0; i < LEN; i++) {
        printf("*(arr + %d): %d\n", i , *(arr + i));
    }

    return 0;
}

執行結果:

*(p + 0): 10
*(p + 1): 20
*(p + 2): 30
*(p + 3): 40
*(p + 4): 50

*(arr + 0): 10
*(arr + 1): 20
*(arr + 2): 30
*(arr + 3): 40
*(arr + 4): 50

在上面的程式中,可以使用指標運算配合 * 運算子來取出陣列中的每個元素,也可以配合下標運算子來取出陣列元素。

在〈陣列〉中談過,可以使用 sizeof 來計算陣列長度,在認識指標及其運算後,透過以下也可以計算出陣列長度:

int arr[] = {10, 20, 30, 40, 50}; 
int len = *(&arr + 1) - arr;

來解釋一下為什麼這行得通,如果使用 &arr 會取得 arr 變數的位址值,也就是陣列資料儲存的位址,與首元素位址是相同的值:

#include <stdio.h>

int main(void) {
    int arr[] = {10, 20, 30, 40, 50}; 
    printf("%p\n", arr);   // 顯示 0061FEBC
    printf("%p\n", &arr);  // 顯示 0061FEBC

    return 0;
}

每個陣列元素的位址型態是 int*,這表示對它進行運算時,是以 int 長度為單位,而 arr 變數的位址處就是陣列資料的開端,&arr 型態會是…呃…int (*)[5],5 是陣列長度,如果想宣告相對應的變數,可以如下:

int (*p)[5] = &arr;

int (*)[5] 表示,對它進行運算時,是以 5 個 int 長度為單位,因此 &arr + 1 的結果,會是陣列使用的空間後之位址,而 *(&arr + 1) 的值型態會回到 int*,也就是最後一個元素後之位址,這時就可以與 int*arr 進行相減,也就是與第一個元素之位址相減,就可以得到陣列長度了。

舉這個例子的重點之一是,對於同一個位址,指標的型態決定了該怎麼看得相對應相加、相減計算;另一個重點是,透過陣列變數會取得首元素的位址,將陣列變數指定給指標 p,就只是取得首元素位址並儲存在 p,如果將 p 傳給 sizeof,那使用的會是指標 p 的型態,而不是原陣列的型態,這會令 sizeof、以及方才那神奇計算長度的方式失效,例如:

#include <stdio.h>

int main(void) {
    int arr[] = {10, 20, 30, 40, 50}; 
    int *p = arr;

    printf("%p\n", p);   // 顯示 0061FEBC
    printf("%p\n", &p);  // 顯示 0061FEB8

    printf("%d\n", sizeof(p)/sizeof(*p));  // 顯示 1
    printf("%d\n", *(&p + 1) - p);         // 顯示 -1605549

    return 0;
}

C++ 11 提供了 beginend 函式,可以計算陣列長度:

constexpr int LENGTH = 5;
int arr[LENGTH] = {10, 20, 30, 40, 50}; 
cout << end(arr) - begin(arr) << endl;     // 顯示 5

在 C 語言中,可以這麼做:

int arr[] = {10, 20, 30, 40, 50}; 
int *begin = arr;
int *end = *(&arr + 1);
printf("%d\n", end - begin);

因此基於指標,也可以使用以下的風格來迭代陣列,而不是使用索引:

#include <stdio.h>

int main(void) {
    int arr[] = {10, 20, 30, 40, 50}; 
    int *begin = arr;
    int *end = *(&arr + 1);

    for(int *it = begin; it < end; it++) {
        printf("%d ", *it);
    }

    return 0;
}

在〈二維(多維)陣列〉中談過,C 沒有二維陣列這種東西,二維或多維陣列的概念,是以陣列的陣列(arrays of arrays)來實現,例如,底下可以分別求得 maze 的列數與每列的長度:

#include <stdio.h>
#define ROWS 2
#define LEN 3

int main(void) {
    int maze[ROWS][LEN] = {
                                {1, 2, 3},
                                {4, 5, 6}
                          };

    printf("ROWS: %d\n", sizeof(maze) / sizeof(maze[0]));
    printf("LEN: %d\n", sizeof(maze[0]) / sizeof(maze[0][0]));

    return 0;
}

或者是使用以下程式:

#include <stdio.h>
#define ROWS 2
#define LEN 3

int main(void) {
    int maze[ROWS][LEN] = {
                                {1, 2, 3},
                                {4, 5, 6}
                          };

    printf("ROWS: %d\n", *(&maze + 1) - maze);
    printf("LEN: %d\n", *(&maze[0] + 1) - maze[0]);

    return 0;
}

執行結果都是:

ROWS: 2
LEN: 3

二維(多維)陣列〉也曾經舉了個例子:

#include <stdio.h>
#define ROWS 2
#define LEN 3

int main(void) {
    int maze[ROWS][LEN] = {
                              {1, 2, 3},
                              {4, 5, 6}
                          };

    for(int i = 0; i < ROWS; i++) {
        int *row = maze[i];
        for(int j = 0; j < LEN; j++) {
            printf("%d\t", row[j]); 
        }
        printf("\n");
    } 
}

現在已經認識指標了,上例中的 maze[i] 取得其實是每列一維陣列的首元素位址,然而指定給 int*row 的話,如稍早談到的,row 就只會儲存位址,也就是 row 並沒有每列一維陣列的長度資訊。

雖說如此,對多數情境來說,這種從二維陣列中取得每列的方式已經足夠,類似地,若不管長度資訊會失去的問題,也可以如下模擬二維陣列:

#include <stdio.h>
#define ROWS 2
#define LEN 3

int main(void) {
    int row1[LEN] = {1, 2, 3};
    int row2[LEN] = {4, 5, 6};
    int* maze[ROWS] = {row1, row2};

    for(int i = 0; i < ROWS; i++) {
        int *row = maze[i];
        for(int j = 0; j < LEN; j++) {
            printf("%d\t", row[j]); 
        }
        printf("\n");
    } 

    return 0;
}

說是模擬的原因在於,maze 實際上是 int* 的一維陣列,maze[0]maze[1] 僅儲存 row1row2 首元素的位址,並沒有 row1row2 的長度資訊,雖說如此,對大多數情境來說,想用一維陣列組合出二維陣列,以上的方式也已經足夠。

接下來純粹是挑戰,可以自行研究一下,就不多做說明了。以下程式示範了如何取得二維陣列中的每一列,並保留長度資訊:

#include <stdio.h>
#define ROWS 2
#define LEN 3

int main(void) {
    int maze[ROWS][LEN] = {
                              {1, 2, 3},
                              {4, 5, 6}
                          };

    for(int i = 0; i < ROWS; i++) {
        int (*row)[LEN] = &maze[i];
        for(int j = 0; j < LEN; j++) {
            printf("%d\t", (*row)[j]); 
        }
        printf("\n");
    } 
}

有沒有辦法完全基於指標來迭代陣列,而不是依靠索引呢?

#include <stdio.h>
#define ROWS 2
#define LEN 3

int main(void) {
    int maze[ROWS][LEN] = {
                                {1, 2, 3},
                                {4, 5, 6}
                          };

    int(*mazeBegin)[LEN] = maze;
    int(*mazeEnd)[LEN] = *(&maze + 1);
    for(int(*row)[LEN] = mazeBegin; row < mazeEnd; row++) {
        int *begin = *row;
        int *end = *(row + 1);
        for(int* it = begin; it < end; it++) {
            printf("%d\t", *it); 
        }
        printf("\n");
    } 

    return 0;
}

底下的寫法,maze[0]maze[1] 會失去 row1row2 的長度資訊:

int row1[LEN] = {1, 2, 3};
int row2[LEN] = {4, 5, 6};
int* maze[ROWS] = {row1, row2};

有沒有辦法不失去長度資訊呢?

#include <stdio.h>
#define ROWS 2
#define LEN 3

typedef int(*Row)[LEN];

int main(void) {
    int row1[LEN] = {1, 2, 3};
    int row2[LEN] = {4, 5, 6};
    Row maze[ROWS] = {&row1, &row2};

    int rows = *(&maze + 1) - maze;
    for(int i = 0; i < rows; i++) {
        Row row = maze[i];
        int len = *(row + 1) - *row;
        for(int j = 0; j < len; j++) {
            printf("%d\t", *(*row + j)); 
        }
        printf("\n");       
    } 

    return 0;
}

typedef 可用來為指定的型態取別名,就上例來說,為 int(*)[LEN] 取了個別名 ROW,這樣比較便於使用 ROW 來宣告,當然,這些挑戰的寫法不容易理解,純粹就是探討,程式基本上還是選擇各自情境下易懂的寫法會比較好。