数组与指针的纠葛
# 09.数组与指针的纠葛
# 目录介绍
- 1. 案例引入
- 2. 架构概览
- 3. sizeof暴露数组真相
- 4. 衰减规则的完整分析
- 5. 三个不衰减的例外
- 6. 多维数组内存布局
- 7. 数组指针与指针数组
- 8. VLA与柔性数组
- 9. 常见数组陷阱图解
- 10. 综合案例串讲
# 1. 案例引入
# 1.1 一段崩在哪
看一段在嵌入式文件系统驱动中的代码——设备上电一周后日志文件变成了一团乱码,客户报告"文件系统损坏":
// fs_driver.c —— FAT32 文件系统驱动
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#define SECTOR_SIZE 512
#define MAX_SECTORS 64
/* FAT 表项——每个 4 字节 */
static uint32_t fat_table[MAX_SECTORS];
/* 读取一个扇区到用户 buf——返回读取的字节数 */
int read_sector(int sector_idx, uint8_t buf[SECTOR_SIZE]) {
/* 模拟:从 Flash 读取 */
extern int flash_read(int, void *, int);
/* ⚠️ 这里 buf 已经退化为 uint8_t* 了! */
size_t buf_len = sizeof(buf); /* ← 这得到的是什么? */
printf("buf_len = %zu\n", buf_len); /* 输出 8——不是 512! */
int ret = flash_read(sector_idx, buf, buf_len);
return ret;
}
/* 检查 FAT 链 */
int verify_fat_chain(void) {
uint8_t sector_buf[SECTOR_SIZE]; /* 栈上 512 字节 */
uint8_t sector2_buf[SECTOR_SIZE]; /* 另一个 512 字节 */
/* 读取第一个扇区 */
int ret = read_sector(0, sector_buf);
/* 因为 buf_len = 8(指针大小),flash_read 只读了 8 字节!
后面 504 字节全是栈上的垃圾——包括返回地址附近的内存 */
/* 后续代码读到垃圾数据——FAT 链校验失败 */
if (memcmp(sector_buf, fat_table, SECTOR_SIZE) == 0) {
return 0; /* 链完整 */
}
return -1; /* 链损坏——实际是只读了 8 字节垃圾! */
}
int main(void) {
/* 模拟 FAT 表初始化 */
for (int i = 0; i < MAX_SECTORS; i++)
fat_table[i] = i;
int result = verify_fat_chain();
printf("校验: %s\n", result == 0 ? "✓" : "✗ 损坏");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
现象:
gcc -O0下:输出buf_len = 8,校验失败——文件系统报损坏- 开发者加了
printf调试时,发现sizeof(buf)在函数内部变成了 8 - 把函数签名改为
int read_sector(int idx, uint8_t *buf)——结果一样,但至少"诚实"
第一反应:uint8_t buf[SECTOR_SIZE] 在函数参数里不是意味着"一个 512 字节的数组"吗?
# 1.2 顺藤摸到根因
在 C 语言中,数组作为函数参数时自动退化为指针。C 标准 §6.7.6.3 para 7:
A declaration of a parameter as "array of T" shall be adjusted to "qualified pointer to T".
函数签名中的 uint8_t buf[SECTOR_SIZE] → 编译器静默改为 uint8_t *buf。所以 sizeof(buf) 返回的是指针大小(8 字节),而不是数组大小(512 字节)。flash_read 只收到了 8 这个值,于是只读了 8 个字节到 buf 的前 8 字节——后面 504 字节是栈上的垃圾。
但更隐蔽的是——buf[SECTOR_SIZE] 在函数参数中的 [SECTOR_SIZE] 只是一个装饰。它对编译器来说是纯粹的注释——不参与类型检查、不限制参数大小、不影响任何行为。你可以传任意大小的指针进去:
uint8_t tiny[10];
read_sector(0, tiny); /* ✅ 编译通过——buf 只是 uint8_t* */
2
这段代码暴露了 6 个关于数组和指针的核心问题:
① 数组名到底是不是指针?sizeof(arr) 为什么是元素总大小不是 8? → 第 3 章
② 什么情况下数组名会"退化"成指向首元素的指针? → 第 4 章
③ 有没有不退化的情况? → 第 5 章
④ 多维数组在内存里怎么排的?和指针数组有什么本质区别? → 第 6-7 章
⑤ VLA 变长数组是什么?它安全吗? → 第 8 章
⑥ 柔性数组成员 `char data[]` 的设计原理是什么? → 第 8.3
2
3
4
5
6
# 1.3 我们要回答什么
这个案例就是本篇的主线。我们从 sizeof 出发证明数组不是指针,再拆解衰减规则及其三个例外,最后用多维数组和 VLA 完善全景图——在第 10 章回到文件系统驱动,用"显式传长度"根治。
全景图 (第 2 章)
↓
sizeof 暴露真相 (第 3 章) ─→ 解开①
↓
衰减规则完整分析 (第 4 章) ─→ 解开②
↓
三个不衰减例外 (第 5 章) ─→ 解开③
↓
多维数组 → 数组指针 (第 6-7 章) ─→ 解开④
↓
VLA + 柔性数组 (第 8 章) ─→ 解开⑤~⑥
↓
综合案例 (第 9-10 章) ─→ 安全 rewrite
2
3
4
5
6
7
8
9
10
11
12
13
# 2. 架构概览
# 2.1 数组与指针全景图
C 语言中的两个"相似但不等同"的概念:
┌─────────────────────────────────────────────────┐
│ 数组 (array) │
│ int arr[5] = {1,2,3,4,5}; │
│ │
│ 特征: │
│ - 一块连续内存,大小 = N × sizeof(element) │
│ - sizeof(arr) = 5×4 = 20 │
│ - &arr 的类型是 int (*)[5] │
│ - arr 在赋值/传参/运算中退化为 int* │
│ - arr 自身不是左值(不能 arr++) │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 指针 (pointer) │
│ int *p = arr; (或 int *p = &arr[0]) │
│ │
│ 特征: │
│ - 一个变量,存储一个地址值 │
│ - sizeof(p) = 8 (64位系统) │
│ - &p 的类型是 int** │
│ - p 可以自增 (p++) │
│ - p[i] 等价于 *(p+i) —— 指针运算 │
└─────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
核心区别:
| 维度 | 数组 | 指针 |
|---|---|---|
| sizeof | 整个数组的字节数 | 指针变量本身的大小(8) |
| & 操作 | 得到数组指针 T (*)[N] | 得到指针的指针 T ** |
| 自增 | 不可(arr++ 编译错误) | 可以(p++) |
| 赋值 | 不可(arr = xxx 编译错误) | 可以(p = xxx) |
| 退化为指针 | 大多数表达式上下文 | 本来就是指针 |
# 2.2 为什么混淆如此普遍
三个原因:
- 下标语法通用——
arr[2]和p[2]写法完全一样,汇编也相同 - 函数参数静默退化——
void f(int arr[5])等价于void f(int *arr) - 大多数场景下可以互换——作为参数传给期望
int*的函数时,数组名自动退化
这导致了一个广泛的误解:"数组就是指针"。本篇的目标就是用汇编和 C 标准的一手证据,把这个误解彻底摧毁。
# 3. sizeof暴露数组真相
# 3.1 数组vs指针的sizeof
int arr[10];
int *p = arr;
printf("sizeof(arr) = %zu\n", sizeof(arr)); /* 40 (10×4) */
printf("sizeof(p) = %zu\n", sizeof(p)); /* 8 (64位指针) */
2
3
4
5
这是区分数组和指针的终极测试。如果"数组就是指针",那么 sizeof(arr) 应该返回 8——但它返回 40。
# 3.2 汇编层面的一锤定音
void demo(void) {
int arr[10];
int *p = arr;
size_t n1 = sizeof(arr); /* 40 */
size_t n2 = sizeof(p); /* 8 */
}
2
3
4
5
6
7
汇编(gcc -O0):
; size_t n1 = sizeof(arr); → 编译期常量
mov QWORD PTR [rbp-8], 40 ; 直接写 40——不涉及 arr 的内容
; size_t n2 = sizeof(p); → 编译期常量
mov QWORD PTR [rbp-16], 8 ; 直接写 8
2
3
4
5
两个 sizeof 都在编译期求值——不产生任何访存指令。sizeof(arr) 的值来自编译器对 arr 的类型信息(int[10]),而这个类型信息在编译后就消失了——运行时没有人知道 arr 是 10 个 int。
# 3.3 数组名不是左值指针
int arr[10];
int *p = arr;
arr = p; /* ❌ 编译错误:assignment to expression with array type */
arr++; /* ❌ 编译错误 */
/* arr 不是一个可修改的左值——它不是一个变量 */
/* arr 是编译期的地址常量——就像 &arr[0] */
2
3
4
5
6
7
8
汇编证据——arr 在汇编中只是一个地址(lea rax, [rbp-48]),不是一个变量。它在栈上的位置是固定的,不能像指针变量那样被重新赋值。
# 4. 衰减规则的完整分析
# 4.1 何时触发衰减
C11 §6.3.2.1 para 3:
Except when it is the operand of the sizeof operator, or the unary & operator, or is a string literal used to initialize an array, an expression that has type "array of T" is converted to an expression with type "pointer to T" that points to the initial element of the array.
衰减发生的场景:
| 场景 | 示例 | 是否衰减 |
|---|---|---|
sizeof(arr) | int n = sizeof(arr); | ❌ 不衰减 |
&arr | int (*pa)[10] = &arr; | ❌ 不衰减 |
| 字符串字面量初始化 | char s[] = "hello"; | ❌ 不衰减 |
| 函数参数 | void f(int arr[10]) | ✅ 退化为 int* |
| 赋值给指针 | int *p = arr; | ✅ |
| 算术运算 | arr + 1 | ✅ |
| 函数参数传递 | func(arr) | ✅ |
# 4.2 衰减的汇编等价物
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; /* ← 衰减 */
int x = *(arr + 2); /* ← 衰减(arr+2 中 arr 退化为指针) */
2
3
汇编:
; int *p = arr;
lea rax, [rbp-32] ; 加载 arr[0] 的有效地址
mov QWORD PTR [rbp-8], rax ; p = &arr[0]
; int x = *(arr + 2);
mov eax, DWORD PTR [rbp-24] ; 直接 [基址+偏移], arr+2 → arr[2] → [rbp-24]
mov DWORD PTR [rbp-12], eax
2
3
4
5
6
7
在 -O2 下,*(arr+2) 被优化为 arr[2]——直接访问 [rbp-24]。衰减是编译期的类型转换——不产生任何运行时代码。
# 4.3 函数参数中的衰减
/* 以下四个声明完全等价——全部退化为 int* */
void f1(int arr[10]);
void f2(int arr[]);
void f3(int arr[5]); /* [5] 只是装饰——不影响任何事 */
void f4(int *arr);
/* 验证——同一个函数可以接受任意大小的数组 */
int a[3];
f1(a); /* ✅ 通过——arr 已经是 int* */
f1(NULL); /* ✅ 通过 */
2
3
4
5
6
7
8
9
10
汇编验证——四个声明生成完全相同的机器码:
f1:
; arr 在 RDI 中——就是一个普通指针
mov eax, DWORD PTR [rdi] ; arr[0]
ret
2
3
4
这就是第 1 章文件系统 bug 的物理根源——sizeof(buf) 在函数内部返回 8,因为 buf 已经退化成了指针,不再是数组。
# 5. 三个不衰减的例外
# 5.1 sizeof操作数
int arr[10];
/* arr 在 sizeof 中不衰减——sizeof 看到的是整个数组的类型 */
printf("%zu\n", sizeof(arr)); /* 40, 不是 8 */
2
3
# 5.2 取地址运算符&
int arr[10];
int *p1 = arr; /* arr 衰减为 &arr[0] → int* */
int (*p2)[10] = &arr; /* &arr 不衰减 → int (*)[10] */
printf("%p\n", p1); /* arr[0] 的地址 */
printf("%p\n", p2); /* 和 p1 数值相同——但类型不同 */
/* 指针运算差异 */
p1 + 1; /* 跳过 1 个 int → 4 字节 */
p2 + 1; /* 跳过 10 个 int → 40 字节——即整个数组 */
2
3
4
5
6
7
8
9
10
11
关键区别:p2 + 1 跳过了整个数组(40 字节),因为它是指向 int[10] 的指针——步长是 sizeof(int[10])。
# 5.3 字符串字面量初始化
char s[] = "hello"; /* s 是 6 字节的 char 数组——不是指针! */
/* "hello" 没有衰减——直接作为初始化器 */
char *p = "hello"; /* "hello" 衰减为 char*——指针指向 rodata */
2
3
4
汇编差异:
; char s[] = "hello"; → 在栈上分配 6 字节,从 rodata 复制
mov DWORD PTR [rbp-6], 'hell' ; 4 字节
mov WORD PTR [rbp-2], 'o\0' ; 2 字节
; char *p = "hello"; → 栈上 8 字节指针,指向 rodata
lea rax, [rip + .LC0] ; .LC0 = "hello" in rodata
mov QWORD PTR [rbp-16], rax
2
3
4
5
6
7
# 6. 多维数组内存布局
# 6.1 行优先与连续内存
int grid[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
2
3
4
5
内存中的实际排列——行优先,连续内存:
地址: 0 4 8 12 16 20 24 28 32 36 40 44
内容: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12]
行: ├── row 0 ──┤ ├── row 1 ──┤ ├── row 2 ──┤
2
3
验证——内存是一整块:
printf("%zu\n", sizeof(grid)); /* 3×4×4 = 48——一整块连续内存 */
printf("%p\n", &grid[0][0]); /* row 0, col 0 */
printf("%p\n", &grid[1][0]); /* row 1, col 0 = grid[0][0] + 16 */
/* 两地址差 16 = sizeof(int[4])——验证行优先 */
2
3
4
# 6.2 多维数组的指针运算
int grid[3][4];
/* grid 的类型: int[3][4] */
/* grid 衰减为: int (*)[4] —— 指向含有 4 个 int 的数组的指针 */
int (*row_ptr)[4] = grid; /* grid 衰减为 int (*)[4] */
grid[1][2]; /* 等价于 *(*(grid + 1) + 2) */
*(grid + 1); /* 跳过 1 个 int[4] → 16 字节 → 指向第二行首 */
*(*(grid + 1) + 2); /* 在该行内跳过 2 个 int → 8 字节 → 第三列 */
2
3
4
5
6
7
8
9
10
# 6.3 动态二维vs静态二维
/* 静态二维——一块连续内存 */
int grid[3][4];
sizeof(grid); /* 48 */
/* 动态二维(指针数组)——内存分两块 */
int **dyn = malloc(3 * sizeof(int *));
for (int i = 0; i < 3; i++)
dyn[i] = malloc(4 * sizeof(int));
sizeof(dyn); /* 8 (是指针) */
/* ⚠️ 关键区别——静态和动态不能互换! */
int (*p)[4] = grid; /* ✅ 静态二维 → 数组指针 可以 */
// int **q = grid; /* ❌ 静态二维 → 二级指针 不行!类型不兼容 */
2
3
4
5
6
7
8
9
10
11
12
13
14
汇编验证——为什么不能互换:
; 静态二维内 grid[1][2] 的寻址
mov eax, DWORD PTR [rbp - 48 + 1*16 + 2*4]
; 一次 [基址 + 行偏移 + 列偏移] —— 1 条指令
; 动态二维内 dyn[1][2] 的寻址
mov rax, QWORD PTR [rdi + 8] ; 先取第二行的指针
mov eax, DWORD PTR [rax + 8] ; 再通过指针取第二列的 int
; 两次内存访问——2 条指令
2
3
4
5
6
7
8
# 7. 数组指针与指针数组
# 7.1 声明螺旋法则
int *p[10]; → p 是数组(有 10 个元素),每个元素是 int*
int (*p)[10]; → p 是指针,指向 int[10] 的数组
2
螺旋读法——从标识符出发:
int *p[10]:
p → 向右看到 [10] → "p 是数组,10 个元素"
→ 向左看到 * → "每个元素是指针"
→ 向左看到 int → "指向 int"
结论: p 是指针数组
int (*p)[10]:
p → 向右看到 ) → 停止
→ 向左看到 * → "p 是指针"
→ 向右看到 [10] → "指向 10 个元素的数组"
→ 向左看到 int → "元素类型是 int"
结论: p 是数组指针
2
3
4
5
6
7
8
9
10
11
12
# 7.2 数组指针实战用法
/* 指向二维数组的一行 */
int grid[3][4];
int (*row)[4] = grid; /* row 指向整个第一行 */
row++; /* 指向第二行——跳过 16 字节 */
(*row)[2] = 42; /* 第二行第三列 ← grid[1][2] = 42 */
/* 数组指针作为形参——传递真正的多维数组 */
void print_row(int (*row)[4], int n) {
for (int i = 0; i < n; i++)
printf("%d ", (*row)[i]);
}
print_row(&grid[1], 4); /* 打印第二行 */
2
3
4
5
6
7
8
9
10
11
12
13
# 7.3 指针数组常见场景
/* 命令行参数——argv 就是指针数组 */
int main(int argc, char *argv[]) {
/* argv 是 char* 的数组——每个元素指向一个字符串 */
for (int i = 0; i < argc; i++)
printf("%s\n", argv[i]);
}
/* 字符串表——用指针数组管理多字符串 */
const char *error_msgs[] = {
"OK",
"File not found",
"Permission denied",
"Out of memory"
};
const char *msg = error_msgs[2]; /* "Permission denied" */
/* 函数指针表——函数指针数组 */
typedef void (*handler_t)(int);
handler_t handlers[] = { handle_a, handle_b, handle_c };
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 8. VLA与柔性数组
# 8.1 VLA的内存模型
VLA(Variable Length Array)——运行时决定大小的数组(C99 引入,C11 可选):
void process(size_t n) {
int arr[n]; /* VLA——n 只能在运行时确定 */
/* arr 在栈上分配——大小 = n × sizeof(int) */
for (size_t i = 0; i < n; i++)
arr[i] = i;
}
2
3
4
5
6
VLA 在栈上的分配:
; int arr[n] 的汇编
mov rax, QWORD PTR [rbp-8] ; rax = n
sal rax, 2 ; rax = n * 4
sub rsp, rax ; RSP 下移 n*4 字节——分配 VLA
mov QWORD PTR [rbp-16], rsp ; arr = RSP
2
3
4
5
不是 malloc 的替代品——VLA 在栈上,函数返回时自动回收。但它和栈上的固定数组一样,受栈大小限制(默认 8MB)。
# 8.2 VLA的风险与替代
/* ❌ VLA 的危险——没有边界检查 */
void dangerous(size_t n) {
int arr[n]; /* 如果 n 是攻击者提供的——栈溢出 */
}
/* ✅ 安全做法——超过阈值用堆 */
void safe_process(size_t n) {
if (n < 4096) {
int arr[n]; /* 小数据用栈——快 */
/* ... */
} else {
int *arr = malloc(n * sizeof(int)); /* 大数据用堆——安全 */
/* ... */
free(arr);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 8.3 柔性数组成员
C99 引入的柔性数组成员——结构体最后一个成员可以是未指定大小的数组:
typedef struct {
uint16_t length;
uint32_t crc;
uint8_t data[]; /* ← 柔性数组成员——不占结构体本身的空间 */
} Packet;
/* 分配——一次 malloc 搞定头部和有效载荷 */
Packet *pkt = malloc(sizeof(Packet) + 1024); /* 额外 1024 字节给 data */
pkt->length = 1024;
memset(pkt->data, 0, 1024);
/* sizeof(Packet) = 8(不含 data——data 在 sizeof 中算 0) */
2
3
4
5
6
7
8
9
10
11
12
柔性数组 vs 指针:
| 维度 | 柔性数组 data[] | 指针 uint8_t *data |
|---|---|---|
| sizeof(结构体) | 8(不含 data) | 16(含 8 字节指针) |
| 内存块数 | 1 次 malloc | 2 次 malloc(结构体+data) |
| 自由释放 | 1 次 free | 2 次 free(必须先 free data) |
| 缓存局部性 | 好(一块连续内存) | 差(两块分离内存) |
# 9. 常见数组陷阱图解
# 9.1 传参后sizeof失效
/* ❌ 错误——函数内 sizeof(arr) 返回指针大小 */
size_t get_len(int arr[10]) {
return sizeof(arr) / sizeof(arr[0]); /* 8/4 = 2——不是 10 */
}
/* ✅ 纠正——显式传长度 */
size_t get_len_fixed(int *arr, size_t n) { return n; }
/* 或者用宏(仅在 arr 的作用域内有效) */
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
2
3
4
5
6
7
8
9
# 9.2 返回局部数组地址
/* ❌ 经典错误——返回栈上数组的地址 */
int *make_array(void) {
int arr[10] = {0};
return arr; /* arr 退化为 &arr[0]——函数返回后栈帧被回收 */
}
/* ✅ 用 static 或 malloc */
int *make_array_static(void) {
static int arr[10] = {0}; /* static 在 .data/.bss,不在栈上 */
return arr;
}
2
3
4
5
6
7
8
9
10
11
# 9.3 指针强转二维数组
int grid[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
/* ❌ 把二维数组强转为二级指针 */
int **p = (int **)grid;
/* p[1][2] —— 会去 grid[0] 的前 8 字节取内容(那是 1 和 2)
然后以它们作为地址去读——不是 7!这是完全错误的内存访问 */
/* ✅ 正确——用数组指针 */
int (*p2)[4] = grid; /* p2[1][2] = 7 ✓ */
2
3
4
5
6
7
8
9
# 10. 综合案例串讲
# 10.1 案例真相揭晓
回到第 1 章的文件系统驱动,六个疑问逐条作答:
| 疑问 | 答案 |
|---|---|
| ① 数组名是不是指针? | 第 3:不是——sizeof 返回总大小;数组名不可自增/赋值 |
| ② 何时退化? | 第 4:赋值给指针、算术运算、函数传参——都会退化 |
| ③ 不退化例外? | 第 5:sizeof、&、字符串字面量初始化——三个例外 |
| ④ 多维数组 vs 指针数组? | 第 6-7:静态二维一块连续内存;动态是指针数组;类型不兼容 |
| ⑤ VLA 安全吗? | 第 8:能运行时定大小,但无边界检查——大值会栈溢出 |
| ⑥ 柔性数组设计? | 第 8.3:一次 malloc 分配头部+数据——零额外指针开销 |
文件系统驱动的安全 rewrite:
/* 修复要点:
1. 函数签名不假扮数组——诚实用指针 + 显式长度
2. 所有数组操作传长度参数
*/
/* ✅ 修复 1:函数签名诚实——指针 + 显式长度 */
int read_sector_fixed(int sector_idx, uint8_t *buf, size_t buf_len) {
extern int flash_read(int, void *, int);
return flash_read(sector_idx, buf, (int)buf_len);
}
/* ✅ 修复 2:调用侧用 ARRAY_SIZE 宏确保一致性 */
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
int verify_fat_chain_fixed(void) {
uint8_t sector_buf[SECTOR_SIZE];
int ret = read_sector_fixed(0, sector_buf, ARRAY_SIZE(sector_buf));
/* ARRAY_SIZE 在 sector_buf 的作用域内有效 → 512 */
if (ret == SECTOR_SIZE) {
/* 完整读取——可以安全比较 */
if (memcmp(sector_buf, fat_table, SECTOR_SIZE) == 0)
return 0;
}
return -1;
}
/* ✅ 修复 3:用静态断言防止数组大小不匹配 */
#include <assert.h>
_Static_assert(SECTOR_SIZE == 512, "SECTOR_SIZE must be 512");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 10.2 数组与指针速查
| 操作 | 数组 int arr[10] | 指针 int *p |
|---|---|---|
| sizeof | 40 | 8 |
| & | int (*)[10] | int ** |
arr[2] | 等价于 *(arr+2) | 等价于 *(p+2) |
| 自增 | ❌ 编译错误 | ✅ p++ |
| 赋值 | ❌ 编译错误 | ✅ p = ... |
| 传参 | 退化为 int* | 传指针本身 |
| 类型名 | int[10] | int* |
# 10.3 设计哲学回扣
哲学 1:数组不是指针——但 C 的语法制造了"它们相同"的幻觉
arr[2] 和 p[2] 写法完全一样,汇编也完全相同。但这种"语法糖"掩盖了它们本质的区别:数组是一块固定大小的连续内存;指针是一个存了地址的变量。语法糖让它们看起来相同——直到 sizeof 戳穿这个幻觉。
哲学 2:衰减是为了兼容性——但代价是信息丢失
函数参数中的数组退化为指针,是为了兼容旧代码和节省拷贝。但代价是"长度信息"在边界处永久丢失——这就是为什么 C 语言的数组 API 总是需要显式传 size_t n。信息丢失不是 bug——是设计权衡。
哲学 3:例外证实规则——sizeof/&/字面量初始化的不衰减
这三个例外不是 bug 修复——它们是 C 语言类型系统中"数组"这个概念必须保留的最后据点。如果没有它们,数组和指针将完全无法区分。保留这三个例外,是 C 标准委员会对"数组是一个独立类型"的最后坚守。
哲学 4:静态二维和指针数组的不兼容——相同语法背后是不同内存模型
grid[1][2] 对静态二维是一次寻址;对指针数组是两次寻址。编译器不能把它们当成相同的类型,因为生成的内存访问指令完全不同。类型系统保护的不是语法——是内存安全。
# 10.4 速查表
| 声明 | 类型 | 含义 |
|---|---|---|
int arr[10] | int[10] | 10 个 int 的数组 |
int *p[10] | 指针数组 | 10 个 int* 的数组 |
int (*p)[10] | 数组指针 | 指向 int[10] 的指针 |
int f(int arr[10]) | 退化为 int* | 函数参数中的数组装饰 |
char s[] = "hi" | char[3] | 字符串字面量初始化——不退色 |
char *s = "hi" | char* | 字符串字面量退化为指针 |
struct T { int x; char d[]; } | 柔性数组 | d 不占 sizeof |
下一篇:我们已经把数组和指针的纠葛理清了,下一步进入 10.结构体对齐与优化——把结构体的内存对齐规则、padding 成因、
#pragma pack的代价从字节级别拆开。