指针与数组


在定义数组之后,使用到数组变量时,会获取首元素的地址,例如在下面的程序中将指出,数组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来定义,当然,这些挑战的写法不容易理解,纯粹就是探讨,程序基本上还是选择各自情境下易懂的写法会比较好。


展开阅读全文