本文是《C语言期末速成》系列第 7 篇。数组是”一排类型相同、地址连续的变量”。这一章两个重心:一是初始化和下标规则(一堆”合法/非法”判断题的出处),二是数组名和指针的纠缠——理解了 a[i] 本质是 *(a+i),二维数组和指针的关系就不攻自破。


上一篇C语言期末速成 6:函数

考点清单

  • 数组存放同类型数据,元素地址连续
  • 数组长度必须由常量(或常量表达式、宏)确定,不能是变量
  • 一维数组初始化:int a[] = {1,2,3}; 由初值个数决定长度;不能给多于长度的初值
  • 二维数组初始化:第一维(行)可以省略,第二维(列)不能省略
  • 数组名是首元素的地址,是地址常量,不能 a++a = ...
  • a[i] 等价于 *(a+i)
  • 一维数组名是列指针a+1 跳一个元素);二维数组名是行指针a+1 跳一行)
  • 数组作函数参数时退化为指针,函数里改数组会改到原数组

一、一维数组

1.1 定义与初始化

int a[5] = {1, 2, 3, 4, 5};   /* 合法:5 个元素 */
int a[5] = {1, 2, 3};         /* 合法:后面补 0 */
int a[]  = {1, 2, 3, 4, 5};   /* 合法:由初值定长度 = 5(常考) */
int a[5] = {1, 2, 3, 4, 5, 6};/* 非法:初值比长度多 */

下标从 0 开始:a[0]a[4]。注意 int a[5] 里合法的下标是 0~4a[5] 越界(访问了数组外面的内存,是未定义行为)。

1.2 长度必须用常量(必考)

定义时数组长度必须是一个确定的常量,不能用运行时变量:

int a[5];            /* 合法 */
int a[1+1];          /* 合法:常量表达式,长度 2 */
int a[1/2+4];        /* 合法:0+4 = 4 */

int x = 5;
int a[x];            /* 非法:x 是变量 */

#define P 5
int a[P];            /* 合法:P 是符号常量,只是长得像变量 */

#define P 5 之后 P 在编译前就被替换成字面量 5,所以长度仍是常量。

1.3 数组名的真相:首元素地址 + 地址常量

int a[10]

  1. a 是数组名,等于首元素 a[0] 的地址(与 &a[0] 等价);
  2. a地址常量——所以 a++a = a + 2 都非法,数组名不能赋值;
  3. a 是一维数组名,是列指针a + 1 跳过一个元素(一个 int,4 字节)。

核心等式(务必刻进脑子):

a[i]*(a + i)[] 就是”取第 i 个元素”,等价于从 a 出发移动 i 个元素再解引用)

int a[5] = {10, 20, 30, 40, 50};
printf("%d\n", a[2]);     /* 30 */
printf("%d\n", *(a + 2)); /* 30:完全等价 */

由此还能推出 a[i] == *(a+i) == *(i+a) == i[a]——i[a] 这种怪写法虽然合法但没人会写。


二、二维数组

2.1 定义与初始化

int a[2][3] 表示 2 行 3 列,共 6 个元素。初始化方式百花齐放:

int a[2][3] = {1,2,3,4,5,6};          /* 合法:按行填 */
int a[2][3] = {1,2,3,4,5};            /* 合法:最后一个补 0 */
int a[2][3] = {{1,2,3},{4,5,6}};      /* 合法:每行一组 */
int a[2][3] = {{1,2},{3,4,5}};        /* 合法:第一行补 0 */
int a[2][3] = {1,2,3,4,5,6,7};        /* 非法:初值个数超了 */

缺省规则(必考)

  • int a[][3] = {...} 合法:第一维(行数)可由初值推出来;
  • int a[2][] = {...} 非法:第二维(列数)不能省略。

原因:二维数组在内存里是行优先连续存储,只有知道每行多少列(第二维),编译器才能算出每行的起点、确定总长度。

2.2 内存布局:行优先(加深)

int a[2][3] = {1,2,3,4,5,6} 在内存里是这样连续排列的:

地址增大 →
a[0][0] a[0][1] a[0][2] | a[1][0] a[1][1] a[1][2]
   1       2       3    |   4       5       6
 └──────── 第 0 行 ─────┘ └──────── 第 1 行 ─────┘

先排完一整行,再排下一行——这就是行优先。所以 a[1][2] 是第 6 个元素。

2.3 行指针与列指针(重难点)

int a[3][3]

写法含义+1 跳什么
a二维数组名 = 第 0 行整体地址a+1 跳一行(行指针)
a[0]a[1]a[2]各行的首元素地址a[0]+1 跳一列(列指针)

关键区分aa[0] 数值上可能相同(都指向首地址),但类型不同

  • a 指向”一行”(3 个 int),a + 1 跨过一整行(12 字节);
  • a[0] 指向”一个 int”,a[0] + 1 跨一个元素(4 字节)。
int a[3][3] = {1,2,3,4,5,6,7,8,9};
*(a[0] + 1)   /* a[0] 是第 0 行首元素地址,+1 跳一列 → a[0][1] = 2 */
*(a[1] + 2)   /* 第 1 行首元素地址,+2 跳两列 → a[1][2] = 6 */

做题技巧:先把二维数组按”行”摊开成表格,再按”先选行、再偏移列”去读 *(a[i]+j)

2.4 “脱衣服法则”:把下标一层层剥成指针(必背)

二维数组下标本质是两重指针运算,转换规则:

a[2]      →  *(a + 2)
a[2][3]   →  *(a[2] + 3)        ← 先剥一层下标
          →  *(*(a + 2) + 3)    ← 再剥一层
int a[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
a[2][1]        /* = 8 */
*(*(a + 2) + 1)  /* 等价:a+2 跳到第 2 行,解引用取行首,+1 跳一列,再解引用 → 8 */

记忆:每去掉一个 [k],就变成 *(... + k),一层一层往外套括号。这是数组和指针联合考点的万能解法。


三、数组作函数参数:退化为指针

数组传参时,数组名退化为指向首元素的指针,长度信息丢失:

int sum(int arr[], int n) {     /* int arr[] 等价于 int *arr */
    int s = 0;
    for (int i = 0; i < n; i++) s += arr[i];
    return s;
}

int main(void) {
    int a[4] = {1, 2, 3, 4};
    printf("%d\n", sum(a, 4));   /* 10 */
    return 0;
}

重要推论:

  1. 函数里 arr指针,所以 arr[i] 实际操作的是原数组——在函数里改数组元素,实参数组会被修改(这和传值完全不同,因为传的是地址);
  2. 函数内 sizeof(arr) 是 8(指针大小),不是数组大小——所以必须把长度当参数传进来;
  3. 因为传的是地址,理论上可以 arr++ 移动指针(但数组名 a 本身不行)。

第二条可以直接验证:

void len(int arr[]) {
    printf("main 里的数组:%zu\n", sizeof(arr));
}

int main(void) {
    int a[4] = {1, 2, 3, 4};
    printf("main 里的数组:%zu\n", sizeof(a));   /* 16:4 个 int */
    len(a);                                      /* 8:退化成指针了 */
    return 0;
}

同一个 a,在 main 里 sizeof(a) 是 16(数组本身),传进函数后 sizeof(arr) 却只有 8(指针)——这就是”长度信息丢失”的直观证据。

void setAll(int b[], int v, int n) {
    for (int i = 0; i < n; i++) b[i] = v;   /* 会改到实参数组 */
}

四、经典数组应用

求最大值及其下标

int a[5] = {3, 9, 2, 7, 4};
int max = a[0], pos = 0;
for (int i = 1; i < 5; i++)
    if (a[i] > max) { max = a[i]; pos = i; }
printf("max=%d at %d\n", max, pos);   /* max=9 at 1 */

冒泡排序(两重循环,理解交换 + 内层递减范围):

int a[5] = {5, 3, 8, 1, 2};
for (int i = 0; i < 4; i++)          /* n-1 趟 */
    for (int j = 0; j < 4 - i; j++)  /* 每趟少比较一个 */
        if (a[j] > a[j+1]) {
            int t = a[j]; a[j] = a[j+1]; a[j+1] = t;
        }
/* 结果 1 2 3 5 8 */

b[a[i]]++ 数组计数的技巧:以 a[i] 为下标去给 b 计数:

int a[8] = {1,2,3,4,4,3,2,2};
int b[5] = {0};
b[a[3]]++;       /* a[3]=4,所以 b[4]++ → b[4] 从 0 变 1 */

b[a[3]] 的语义:先取 a[3] 的值(4)当下标,再对 b[4] 自增。这是”用数据当索引”的计数套路,常用来统计元素出现次数。


易错点 / 陷阱

  1. int a[x](x 是变量)当合法:数组长度必须是常量。
  2. int a[2][] 当合法:二维数组第二维不能省略。
  3. 初值个数多于长度int a[3]={1,2,3,4} 非法。
  4. 对数组名 a++:a 是地址常量,不能自增、不能被赋值。
  5. a[5] 越界:长度为 5 的数组合法下标是 0~4。
  6. 函数里 sizeof(arr) 当数组大小:数组形参是指针,sizeof 得到的是指针大小。
  7. *(a[1]+2) 算错行列:先定位到 a[1] 这一行,再偏移 2 列。
  8. 以为数组传参是值传递:数组名退化为指针,函数内修改会改到实参。

自测小练习

第 1 题:下列哪些合法?int a[5];int b[1+1];int c[1/2+4];int x=3; int d[x];

第 2 题int a[][3]={1,2,3,4,5,6}; 这个数组有几行?a[1][2] 是多少?

第 3 题int a[2][3]={{1,2,3},{4,5,6}};*(a[1]+1)

第 4 题:把 a[2][3] 用指针双层运算写出来。

第 5 题:在函数 void f(int arr[]) 里写 arr[0]=100;,会不会改变实参数组?为什么?

参考答案
  1. 合法:a[5]b[1+1]c[1/2+4](都是常量);非法:d[x](x 是变量)。
  2. 共 6 个元素,每行 3 列 → 2 行;a[1][2] = 6
  3. a[1] 是第 1 行首元素地址,+1 跳一列 → a[1][1] = 5
  4. a[2][3]*(a[2] + 3)*(*(a + 2) + 3)
  5. 会。数组作参数退化为指针,arr[0] 操作的是实参数组本身。

下一篇C语言期末速成 8:指针

评论区