C语言期末速成7:数组
一维/二维数组的初始化与内存布局、数组名与指针的关系、脱衣服法则。
本文是《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~4,a[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]:
a是数组名,等于首元素a[0]的地址(与&a[0]等价);a是地址常量——所以a++、a = a + 2都非法,数组名不能赋值;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 跳一列(列指针) |
关键区分:a 和 a[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;
}
重要推论:
- 函数里
arr是指针,所以arr[i]实际操作的是原数组——在函数里改数组元素,实参数组会被修改(这和传值完全不同,因为传的是地址); - 函数内
sizeof(arr)是 8(指针大小),不是数组大小——所以必须把长度当参数传进来; - 因为传的是地址,理论上可以
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] 自增。这是”用数据当索引”的计数套路,常用来统计元素出现次数。
易错点 / 陷阱
int a[x](x 是变量)当合法:数组长度必须是常量。int a[2][]当合法:二维数组第二维不能省略。- 初值个数多于长度:
int a[3]={1,2,3,4}非法。 - 对数组名
a++:a 是地址常量,不能自增、不能被赋值。 a[5]越界:长度为 5 的数组合法下标是 0~4。- 函数里
sizeof(arr)当数组大小:数组形参是指针,sizeof得到的是指针大小。 *(a[1]+2)算错行列:先定位到a[1]这一行,再偏移 2 列。- 以为数组传参是值传递:数组名退化为指针,函数内修改会改到实参。
自测小练习
第 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;,会不会改变实参数组?为什么?
参考答案
- 合法:
a[5]、b[1+1]、c[1/2+4](都是常量);非法:d[x](x 是变量)。 - 共 6 个元素,每行 3 列 → 2 行;
a[1][2] = 6。 a[1]是第 1 行首元素地址,+1跳一列 →a[1][1] = 5。a[2][3]→*(a[2] + 3)→*(*(a + 2) + 3)。- 会。数组作参数退化为指针,
arr[0]操作的是实参数组本身。
下一篇:C语言期末速成 8:指针