在理解指针与数组的各自含义之后,需要弄清二者在表达式中的联系:数组名在多数情况下表示首元素地址,因而可与指针算术、解引用配合使用;但数组与指针在类型与 sizeof 等行为上并不完全相同。

数组名与首元素地址

下列程序用三种方式打印字符数组各元素的地址与值:

#include <stdio.h>

int main(void) {
    char vowels[] = {'A', 'E', 'I', 'O', 'U'};
    char *pvowels = vowels;
    int i;

    for (i = 0; i < 5; i++) {
        printf("&vowels[%d]: %p | pvowels+%d: %p | vowels+%d: %p\n",
               i, (void *)&vowels[i], i, (void *)(pvowels + i), i, (void *)(vowels + i));
    }

    for (i = 0; i < 5; i++) {
        printf("vowels[%d]: %c | *(pvowels+%d): %c | *(vowels+%d): %c\n",
               i, vowels[i], i, *(pvowels + i), i, *(vowels + i));
    }
    return 0;
}

典型输出中,&vowels[i]、pvowels + i、vowels + i 的地址一致;相邻 char 元素地址相差 1 字节。

可归纳两点:

  • vowels、&vowels[0]、vowels + 0 均指向首元素。
  • 在表达式中,数组名常退化为指向首元素的指针;该指针为指针常量,不能对数组名重新赋值(例如 vowels = … 非法)。

数组与指针并非同一类型

  • 数组名不是可修改的指针变量;指针变量可以改指向其他对象。
  • 在定义数组的作用域内,sizeof(数组名) 为整个数组的字节数;数组作为函数参数传入后退化,sizeof 不再表示元素个数,见数组作为函数参数。
  • 遍历与访问时,arr[i] 与 *(arr + i) 写法可互换,但类型与生命周期规则仍须区分。

用指针遍历数组

int arr[5] = {2, 4, 8, 12, 16};
int *p = arr;

for (int i = 0; i < 5; i++) {
    printf("%d ", *(p + i));
}
printf("\n");

也可移动指针本身,注意循环上界应基于元素个数,避免越过数组末尾:

for (int *p = arr; p < arr + 5; p++) {
    printf("%d ", *p);
}
printf("\n");

指针加 1 时,地址增加量为所指类型的大小(例如 int * 加 1 跳过 sizeof(int) 字节)。

边界与安全

无论使用下标还是指针算术,都必须保证访问范围在数组有效元素之内;越界读写在 C 语言中属于未定义行为。

本节说明了数组名与指针在访问上的对应关系及二者差异。下一节介绍如何通过指针形参在函数中修改调用方的变量,见按地址传参。