[C語言] - 指標及多維陣列

指標及一維陣列

在不少的書籍,我們都可以看到可以把陣列看成指標,這不完全正確,但在實作上也不能說完全不正確

本質上陣列名稱代表的是一個位址,而指標代表的是位址的位址,因此在宣告上是不相等的,例如你在某一個地方宣告了int a[10],而在其他地方想extern進來,但如果使用的是extern *a,那會compile error,因為位址和位址的位址並不一樣
再來如果a[10]真的和*a是一樣的,那sizeof(a)就會應該是4

那為什麼我們在實際使用上結果會是一樣的呢呢?下面這個例子都會印出20

int a[] = {10, 20, 30};

printf("%d", a[1]);
printf("%d", *(a + 1));

那是因為陣列在compiler階段都會被轉成指標,當看到a[1]的時候就從a的位址加上1個integer的size,如果看到*(a + 1)那行為會一樣
差別只在如果compiler知道a是陣列那會忽略’*’和’&’也就是不會作dereference,這樣也可以解釋為什麼下面這情況compiler認為是合法的

int b[3] = {10, 20, 30};

b[99] = 40;

那在什麼狀況下會完全相等呢?只有在傳陣列參數到函式的時候,以下三種情況compiler編出來會一模一樣,事實上在傳陣列到函式裡面時,真正傳的是指標並不是整份陣列複製過去

func(int *verctor) {...};
func(int verctor[]) {...};
func(int verctor[10]) {...};

試著想一下以下這段程式碼會印出什麼

#include <stdio.h>

char ga[2] = {'a', 'b'};

void func_in_vec(char ca[2])
{
    printf("sizeof ca : %ld \n", sizeof(ca));
    printf("&ca : %p \n", &ca);
    printf("&(ca[0]) : %p \n", &(ca[0]));
     printf("&(ca[1]) : %p \n", &(ca[1]));
     printf("\n");
}

void func_in_ptr(char *pa)
{
     printf("sizeof pa : %ld \n", sizeof(pa));
     printf("&pa : %p \n", &pa);
     printf("&(pa[0]) : %p \n", &(pa[0]));
     printf("&(pa[1]) : %p \n", &(pa[1]));
     printf("\n");
}

int main(void)
{
     printf("sizeof ga : %ld \n", sizeof(ga));
     printf("&ga : %p \n", &ga);
     printf("ga : %p \n", ga);
     printf("&(ga[0]) : %p \n", &(ga[0]));
     printf("&(ga[1]) : %p \n", &(ga[1]));
    printf("\n");

    func_in_vec(ga);
     func_in_ptr(ga);         

    return 0;
}

結果如下(有可能在不同平台會有不同結果,這邊是在x64平台的結果,不過概念是一樣的)

sizeof ga : 2 -------------(1)    
&ga : 0x601048 ------------(2)
ga : 0x601048 -------------(3)
&(ga[0]) : 0x601048 -------(4)
&(ga[1]) : 0x601049 -------(5)

sizeof ca : 8 -------------(6)
&ca : 0x7fffd7b1c718 ------(7)
&(ca[0]) : 0x601048 -------(8)
&(ca[1]) : 0x601049 -------(9)

sizeof pa : 8 -------------(10)
&pa : 0x7fffd7b1c718 ------(11)
&(pa[0]) : 0x601048 -------(12)
&(pa[1]) : 0x601049 -------(13)

由(1)可看出ga的型別為2byte的陣列,代表不直接相等指標
(2)(3)可看出&ga = ga,代表雖然compiler會將陣列轉成指標,但會忽略’*’和’&’,
(6)(8)可看出若經由funtion傳入參數後皆轉為指標,因此取sizeof為8(64位元平台)

指標及二維陣列(矩陣)

在現實生活上我們有二維陣列的概念,用來作運算或儲存各種資料,但是在記憶體裡是如何儲存這些資料?記憶體只是一大塊連續的位址空間,如何實現二維陣列的概念?

事實上在記憶體裡面二維陣列就是陣列的陣列,而三維陣列則是陣列的陣列的陣列,依此類推

舉個例子來說,一個[3][2]的陣列代表的是有一個大小3的陣列裡面有大小2的陣列,共6個,參考下圖即可清楚看出,裡面的數字代表的就是程式取值時需給的二維陣列位址,橘色代表的是第一層陣列,藍色代表第二層陣列(陣列的陣列)

試著想一下以下這段程式碼會印出什麼

int main(void)
{
    int a[3][2] = {{10, 20}, {30, 40}, {50, 60}};
    int *b = a[0];

    printf("a[0][0] = %d\n", a[0][0]);
    printf("a[0][1] = %d\n", a[0][1]);
    printf("a[1][1] = %d\n", a[1][1]);
    printf("a[2][1] = %d\n", a[2][1]);

    printf("*(b + 0) = %d\n", *(b + 0));
     printf("*(b + 1) = %d\n", *(b + 1));
     printf("*(b + 3) = %d\n", *(b + 3));
     printf("*(b + 5) = %d\n", *(b + 5));

     return 0;
}

結果如下

a[0][0] = 10 ---(1)
a[0][1] = 20 ---(2)
a[1][1] = 40 ---(3)
a[2][1] = 60 ---(4)
*(b + 0) = 10 ---(5)
*(b + 1) = 20 ---(6)
*(b + 3) = 40 ---(7)
*(b + 5) = 60 ---(8)

從下圖可看出此段程式將b指向第一層陣列a[0]的位址,搭配下圖和以上的結果可清楚看出(1) = (5)、(2) = (6)、(3) = (7)、(4) = (8),也可看出記憶體擺放的位址和二維陣列的關係

指標及三維陣列

了解指標與陣列關係之後,我們可以來看更為抽象的三維以上的陣列,事實上如果能轉換成記憶體實際擺放的位址來看,也就不會覺得抽象了,假如有一個a[2][3][4]的三維陣列,那在記憶體擺放的情形如下,橘色代表第一維陣列,藍色代表第二維陣列,綠色代表第三維陣列,
因此此陣列為一個大小為2的陣列裡面有大小為3的陣列裡面有大小為4的陣列,共24個

試著想一下以下這段程式碼會印出什麼

int main(void)
{
    int a[2][3][4] = {0};
    int *b = a[0][0];
    int i, j, k;

     for (i = 0; i < 2; i++)
        for (j = 0; j < 3; j++)
             for (k = 0; k < 4; k++)
                 a[i][j][k] = i + j + k;

     printf("a[0][0][0] = %d\n", a[0][0][0]);
     printf("a[0][0][2] = %d\n", a[0][0][2]);
     printf("a[0][1][1] = %d\n", a[0][1][1]);
     printf("a[0][2][3] = %d\n", a[0][2][3]);
     printf("a[1][0][0] = %d\n", a[1][0][0]);
     printf("a[1][2][2] = %d\n", a[1][2][2]);

     printf("*(b + 0) = %d\n", *(b + 0));
     printf("*(b + 2) = %d\n", *(b + 2));
     printf("*(b + 5) = %d\n", *(b + 5));
     printf("*(b + 11) = %d\n", *(b + 11));
     printf("*(b + 12) = %d\n", *(b + 12));
     printf("*(b + 22) = %d\n", *(b + 22));

     return 0;
}

結果如下

a[0][0][0] = 0
a[0][0][2] = 2
a[0][1][1] = 2
a[0][2][3] = 5
a[1][0][0] = 1
a[1][2][2] = 5
*(b + 0) = 0
*(b + 2) = 2
*(b + 5) = 2
*(b + 11) = 5
*(b + 12) = 1
*(b + 22) = 5

從下圖可看出三維陣列和記憶體的關係,將指標b指到三維陣列的頭,四維以上的陣列和二維三維陣列的原理一樣,可以此類推

接下來我們可以看一下不同type的指標和陣列的關係,首先先看int的指標,試著想一下以下這段程式碼會印出什麼

int main(void)
{
    int a[2][3][4] = {0};
    int *b = a[0][0];
    int *c = a[0][2];
    int *d = a[1][1];
    int i, j, k;
    int cnt = 0;

    for (i = 0; i < 2; i++)
        for (j = 0; j < 3; j++)
            for (k = 0; k < 4; k++) {
                a[i][j][k] = cnt;
                cnt++;
            }

    printf("b = %p, b + 1 = %p\n", b, b + 1);

    printf("*(b + 0) = %d, *(b + 1) = %d\n", *(b + 0), *(b + 1));
    printf("a[0][0][0] = %d, a[0][0][1] = %d\n\n", a[0][0][0], a[0][0][1]);

    printf("*(c + 0) = %d, *(c + 6) = %d\n", *(c + 0), *(c + 6));
    printf("a[0][2][0] = %d, a[1][0][2] = %d\n\n", a[0][2][0], a[1][0][2]);

    printf("*(d + 0) = %d, *(d + 7) = %d\n", *(d + 0), *(d + 7));
    printf("a[1][1][0] = %d, a[1][2][3] = %d\n\n", a[1][1][0], a[1][2][3]);

    return 0;
}

結果如下

b = 0x7fffedac8880, b + 1 = 0x7fffedac8884
*(b + 0) = 0, *(b + 1) = 1
a[0][0][0] = 0, a[0][0][1] = 1

*(c + 0) = 8, *(c + 6) = 14
a[0][2][0] = 8, a[1][0][2] = 14

*(d + 0) = 16, *(d + 7) = 23
a[1][1][0] = 16, a[1][2][3] = 23

b因為指到的是int type,所以b和b+1的差距為4byte

b, c, d皆為指到int的指標,因此可指到此三維陣列的第三維陣列,大小為4(藍色框住的綠色部份)

b指到a[0][0]的大小為4的陣列,因此*(b + 0) = a[0][0][0],*(b + 1)會加一個int size(4byte)之後取值所以*(b + 1) = a[0][0][1]

c指到a[0][2]的大小為4的陣列,因此*(c + 0) = a[0][2][0],*(c + 6)會加六個int size(4byte)之後取值所以*(c + 6) = a[1][0][2]

d指到a[1][1]的大小為4的陣列,因此*(d + 0) = a[1][1][0],*(d + 7)會加七個int size(4byte)之後取值所以*(d + 7) = a[1][2][3]

再來看int[]的指標,試著想一下以下這段程式碼會印出什麼

int main(void)
{
    int a[2][3][4] = {0};
    int (*b)[4] = a[0];
     int (*c)[4] = a[1];
     int i, j, k;
     int cnt = 0;

     for (i = 0; i < 2; i++)
         for (j = 0; j < 3; j++)
             for (k = 0; k < 4; k++) {
                 a[i][j][k] = cnt;
                 cnt++;
             }

     printf("b = %p, b + 1 = %p\n", b, b + 1);

     printf("(*(b + 0))[0] = %d, (*(b + 1))[0] = %d\n", (*(b + 0))[0], (*(b + 1))[0]);
     printf("a[0][0][0] = %d, a[0][0][1] = %d\n\n", a[0][0][0], a[0][1][0]);

     printf("(*(b + 2))[2] = %d, (*(b + 4))[3] = %d\n", (*(b + 2))[2], (*(b + 4))[3]);
     printf("a[0][2][2] = %d, a[1][1][3] = %d\n\n", a[0][2][2], a[1][1][3]);

     printf("(*(c + 1))[1] = %d, (*(c + 2))[2] = %d\n", (*(c + 1))[1], (*(c + 2))[2]);
     printf("a[1][1][1] = %d, a[1][2][2] = %d\n\n", a[1][1][1], a[1][2][2]);    

     return 0;
}

結果如下

b = 0x7fff2c1a96c0, b + 1 = 0x7fff2c1a96d0
(*(b + 0))[0] = 0, (*(b + 1))[0] = 4
a[0][0][0] = 0, a[0][1][0] = 4

(*(b + 2))[2] = 10, (*(b + 4))[3] = 19
a[0][2][2] = 10, a[1][1][3] = 19

(*(c + 1))[1] = 17, (*(c + 2))[2] = 22
a[1][1][1] = 17, a[1][2][2] = 22

b因為是指到int[4]的指標因此b和b+1的差距為16byte(4byte(int) * 4(size))

b, c, d皆為指到int[4]的指標,因此可指到此三維陣列的第二維陣列,大小為3(橘色框住的藍色部份)

b指到a[0]的大小為3的陣列,因此(*(b + 0))[0] = a[0][0][0],*(b + 1)會加四個int size(16byte)之後取值所以(*(b + 1))[0] = a[0][1][0],*(b + 2)會加八個int size(32byte)之後取值所以(*(b + 2))[2] = a[0][2][2],*(b + 4)會加十六個int size(64byte)之後取值所以(*(b + 4))[3] = a[1][1][3]

c指到a[1]的大小為4的陣列,因此(*(c + 1))[1] = a[1][1][1],*(c + 2)會加八個int size(32byte)之後取值所以(*(c + 2))[2] = a[1][2][2]

再來看int[][]的指標,試著想一下以下這段程式碼會印出什麼

int main(void)
{
    int a[2][3][4] = {0};
     int (*b)[3][4] = a;
     int i, j, k;
     int cnt = 0;

     for (i = 0; i < 2; i++)
         for (j = 0; j < 3; j++)
             for (k = 0; k < 4; k++) {
                 a[i][j][k] = cnt;
                 cnt++;
             }

     printf("b = %p, b + 1 = %p\n", b, b + 1);

     printf("(*(b + 0))[0][0] = %d, (*(b + 0))[0][2] = %d\n", (*(b + 0))[0][0], (*(b + 0))[0][2]);
     printf("a[0][0][0] = %d, a[0][0][2] = %d\n\n", a[0][0][0], a[0][0][2]);
     printf("(*(b + 0))[2][1] = %d, (*(b + 0))[2][3] = %d\n", (*(b + 0))[2][1], (*(b + 0))[2][3]);
     printf("a[0][2][1] = %d, a[0][2][3] = %d\n\n", a[0][2][1], a[0][2][3]);

     printf("(*(b + 1))[0][1] = %d, (*(b + 1))[2][3] = %d\n", (*(b + 1))[0][1], (*(b + 1))[2][3]);
     printf("a[1][0][1] = %d, a[1][2][3] = %d\n\n", a[1][0][1], a[1][2][3]);

     return 0;
}

結果如下

b = 0x7fff1604c440, b + 1 = 0x7fff1604c470
(*(b + 0))[0][0] = 0, (*(b + 0))[0][2] = 2
a[0][0][0] = 0, a[0][0][2] = 2

(*(b + 0))[2][1] = 9, (*(b + 0))[2][3] = 11
a[0][2][1] = 9, a[0][2][3] = 11

(*(b + 1))[0][1] = 13, (*(b + 1))[2][3] = 23
a[1][0][1] = 13, a[1][2][3] = 23

b因為是指到int[3][4]的指標因此b和b+1的差距為48byte(4byte(int) (3 4)(size))

b, c, d皆為指到int[3][4]的指標,因此可指到此三維陣列的第一維陣列,大小為3 * 4(橘色部份)

b指到a的大小為3 4的陣列,因此(\(b + 0))[0][0] = a[0][0][0],(*(b + 0))[0][2] = a[0][0][2],(*(b + 0))[2][1] = a[0][2][1],(*(b + 0))[2][3] = a[0][2][3],*(b + 1)會加十二個int size(48byte)之後取值所以(*(b + 1))[0][1] = a[1][0][1],(*(b + 1))[2][3] = a[1][2][3],