这一章在干嘛?
一维数组的本质(下标就是指针算术的语法糖)、数组作参数的退化、多维数组的存储与指针,以及指针数组。吃透本章,第 6 章的指针运算才算真正落地。
8.1 一维数组:下标即指针算术
int a[5] = {10, 20, 30, 40, 50};
a[2] /* 语法糖,等价于 *(a + 2) */
*(a + 2) /* 30:数组名当首元素地址用 */
数组名在表达式中退化为「指向首元素的指针常量」(不能 a++,因为它是地址不是变量),只有两处例外:sizeof(a) 得到整个数组的大小,&a 得到「指向整个数组」的指针(值同、类型不同)。
/* 下标与指针两种遍历:语义相同 */
for (i = 0; i < 5; i++) sum += a[i];
for (p = a; p < a + 5; p++) sum += *p; /* p++ 按元素步进 */
初始化规则:花括号列表;不给长度则自动按元素个数定;不完整初始化则剩余补 0;char msg[] = "hi" 自动含结尾 ‘\0’(3 字节),而 char msg[2] = "hi" 就装不下了——别省。
8.2 数组与指针:名同实异的边界
| 声明 | 本质 | sizeof | 能否被赋值 |
|---|---|---|---|
| int a[10] | 分配 40 字节的数组实体 | 40(整个数组) | 不能(a 是地址常量般的标识符) |
| int *p | 分配一个指针变量 | 8(指针本身) | 能(p++ 合法) |
两者在「表达式里」可互换(都退化为首元素地址),在「声明处」是天壤之别。函数参数处数组名一律退化为指针,所以函数内改 a[0] 影响原数组(第 7 章的 bump3)。
边界检查是调用者的责任:
C 不做 a[10](对 int a[10] 而言)越界检查,越界读写是未定义行为——可能踩坏相邻变量、改坏栈帧返回地址。循环边界
i < 10而非i <= 10,从第一天就养成。
8.3 多维数组:连续存储与数组指针
int m[3][4] 在内存里是一条线:按行主序铺开 12 个 int。m[1][2] 的地址 = 首地址 + (1×4 + 2)×sizeof(int):
int m[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
int (*p)[4] = m; /* 数组指针:指向「int[4]」,p+1 跳一整行 */
printf("%d\n", p[1][2]); /* 7 */
printf("%d\n", (*(m + 1))[2]); /* 同上:先下移一行,再取第 2 列 */
传参时行数可省、列数必须写明——编译器要靠列数算「跳一行」的步长:
void scan(int mat[][4], int rows); /* 等价于 int (*mat)[4], int rows */
vs int (*p)[4]:
前者是「4 个 int* 的数组」(方括号优先结合),后者是「指向 int[4] 的指针」(括号强令 * 先结合)。读声明先看括号,再看左右。
8.4 指针数组:字符串表的标配
const char *names[] = { "STM32", "ESP32", "GD32" };
for (int i = 0; i < 3; i++)
printf("%s\n", names[i]); /* names[i] 是 char*,指向第 i 个串 */
names 是 3 个指针(各自指向字符串常量),字符串本身散落在静态区——总开销 = 3×指针 + 各串长度,比二维字符数组 char names[3][8](按最长串对齐、整块连续)更省空间、但串不可修改。选择标准:只读字符串表用指针数组;要修改内容用二维数组。
本章通关标准:
能秒答 a[i] 与 *(a+i) 的等价性;看到函数参数 int a[][4] 知道列数不能省;分得清指针数组和数组指针。
1. 数组名和指针变量的两点本质区别?
① sizeof:数组名给出整个数组大小,指针只给指针自身大小;② 数组名不是变量、不能被赋值(a++ 非法),指针可以随意改指。表达式用法上数组名退化为首元素地址,与指针互换。
2. 二维数组传参为什么列数必须写明?
数组退化为「指向一行的指针」,p+1 的步长 = 列数×元素大小。编译器需要列数计算跳行步长和 a[i][j] 的地址,行数则可由额外参数传入。
和二维数组 char tab[][8] 各适合什么场景?
指针数组:各串独立存放、按实际长度占内存,适合只读字符串表;二维数组:整块连续、每行定长,可修改内容,但按最长串对齐可能浪费空间。