这一章跟着柯南追踪"数据流":数组在内存里到底怎么铺?指针 +1 到底加了多少?为什么数组一传进函数就"缩水"?每一处结论都用本机实测取证
核心方法论:追踪数据流
「案发现场是一段连续的内存,受害者是一个数组,唯一的线索是每个元素头顶上的地址。很多程序员写了几年代码,却说不清 a[1] 和 *(a+1) 为什么是同一件事,也说不清 p+1 到底往前走了几步。真相只有一个,但这次我们不猜——把地址一个一个打出来,跟着数据流走一遍,案就破了。」
第 7、8 章我们研究透了单个变量在内存里的"长相"(大小端、补码、IEEE 754),这一章开始追踪更复杂的猎物——数组。说人话:数组就是一段连续的内存格子,每个格子装一个同类型的元素,格子之间首尾相接、不留缝隙。当编译器看到 int a[3],它会在栈上一次性划出 3 × 4 = 12 字节的连续空间(4 是 int 的大小),并且向你保证:这 12 个字节是"一整块"的——绝不是东一块西一块拼出来的。这个"连续"不是细节,它决定了后面所有地址计算的规则。
既然是一整块,三兄弟 a[0]、a[1]、a[2] 的地址就应该像排队一样挨着。怎么证明?把地址打出来。在 printf 里用 %p(pointer 的缩写,以十六进制打印指针/地址的格式化占位符),就能看到每个元素住在哪:
// addr.cpp —— 打印数组每个元素的地址
#include <cstdio>
int main() {
int a[3] = {10, 20, 30};
printf("&a[0] = %p\n", (void*)&a[0]);
printf("&a[1] = %p\n", (void*)&a[1]);
printf("&a[2] = %p\n", (void*)&a[2]);
printf("a = %p\n", (void*)a); // 数组名在表达式中退化为指针
printf("&a = %p\n", (void*)&a); // 取整个数组的地址
printf("sizeof(a) = %zu\n", sizeof(a));
printf("a[1] = %d, *(a+1) = %d\n", a[1], *(a+1)); // 下标 ≡ 指针
return 0;
}
# 运行输出(本机真实输出):
&a[0] = 0x16fcb60d8
&a[1] = 0x16fcb60dc
&a[2] = 0x16fcb60e0
a = 0x16fcb60d8
&a = 0x16fcb60d8
sizeof(a) = 12
a[1] = 20, *(a+1) = 20
把输出画成格子图,一切一目了然:三个元素从 0x…d8 开始一个接一个排到 0x…e0,每个格子正好 4 字节(int 的大小),相邻地址的差恒为 4。注意 a 和 &a[0] 打印出来是同一个数值,而 sizeof(a) 是 12——"整个数组"的大小。最后一行更关键:a[1] 和 *(a+1) 结果一样——编译器眼里根本没有"下标",下标只是"指针加偏移再解引用"的语法糖:a[i] 等价于 *(a+i),这是标准写死的等价关系。
# 内存布局(每个格子 = 4 字节;地址取自上面这次真实运行)
0x16fcb60d8 ┌──────────┐
│ a[0] = 10 │ <- &a[0]
0x16fcb60dc ├──────────┤
│ a[1] = 20 │ <- &a[0] + 4 字节
0x16fcb60e0 ├──────────┤
│ a[2] = 30 │ <- &a[0] + 8 字节
0x16fcb60e4 └──────────┘ <- 数组结束(12 字节用尽)
@连续 三个格子首尾相接,地址差恒等于元素大小 4 字节
| 元素 | 地址(一次实测) | 与 &a[0] 的差 | 结论 |
|---|---|---|---|
| a[0] | 0x16fcb60d8 | 0 | 起点(基地址) |
| a[1] | 0x16fcb60dc | 4 字节 | 相差一个 int |
| a[2] | 0x16fcb60e0 | 8 字节 | 相差两个 int |
细心的你会问:为什么我运行的结果和你贴的地址不一样?因为现代操作系统有 ASLR(地址空间布局随机化,每次启动程序都随机挑一块内存区域放栈和堆,用于防攻击)。所以柯南的取证原则是:看关系,不看绝对值——地址每次都在变,但"相邻差 4、总共 12"这些关系永远不变。规律才是线索,数字只是线索的载体。
注意:用 %p 打印地址,必须把指针强转成 (void*) 再传进去,否则是未定义行为(undefined behavior,标准不承诺任何结果的程序行为)——多数编译器会警告,但标准允许它打印出任何东西。写 printf("%p", (void*)&a[0]),别偷懒。
柯南的放大镜对准下一个疑点:p + 1 到底在地址上加了多少?直觉会说是"1 个字节"——错了。指针的加法是"带单位的加法",单位就是它指向的类型的大小。这个"走一步跨多远"的距离叫步长(stride,指针算术中一次位移对应的字节数):int* 的 +1 是 +4 字节,char* 的 +1 是 +1 字节,double* 的 +1 是 +8 字节。同样是"加 1",三种指针在内存里走的距离完全不同。
光说不练假把式。用 sizeof 查类型大小,再用"指针减指针"实测三种指针的步长——注意先把指针转成 char* 再相减,因为指针相减的结果单位也是"元素",只有 char 的步长是 1 字节,才能直接读出字节数:
// stride.cpp —— p+1 到底加了多少?sizeof + 指针减法实测
#include <cstdio>
int main() {
printf("sizeof(int) = %zu 字节\n", sizeof(int));
printf("sizeof(char) = %zu 字节\n", sizeof(char));
printf("sizeof(double) = %zu 字节\n", sizeof(double));
printf("sizeof(int*) = %zu 字节(指针本身大小,本机 64 位)\n\n", sizeof(int*));
int arr_i[3] = {1, 2, 3};
char arr_c[3] = {'a', 'b', 'c'};
double arr_d[3] = {1.0, 2.0, 3.0};
int* p_i = arr_i;
char* p_c = arr_c;
double* p_d = arr_d;
printf("int 指针 p_i: %p -> %p, 步长 %ld 字节\n",
(void*)p_i, (void*)(p_i + 1), (long)((char*)(p_i+1) - (char*)p_i));
printf("char 指针 p_c: %p -> %p, 步长 %ld 字节\n",
(void*)p_c, (void*)(p_c + 1), (long)((char*)(p_c+1) - (char*)p_c));
printf("double 指针 p_d: %p -> %p, 步长 %ld 字节\n",
(void*)p_d, (void*)(p_d + 1), (long)((char*)(p_d+1) - (char*)p_d));
printf("\n&arr_i[0] = %p, arr_i = %p, 相等: %d\n",
(void*)&arr_i[0], (void*)arr_i, &arr_i[0] == arr_i);
return 0;
}
# 运行输出(本机真实输出):
sizeof(int) = 4 字节;sizeof(char) = 1 字节;sizeof(double) = 8 字节
sizeof(int*) = 8 字节(指针本身大小,本机 64 位)
int 指针 p_i: 0x16b8aa0c8 -> 0x16b8aa0cc, 步长 4 字节
char 指针 p_c: 0x16b8aa0a8 -> 0x16b8aa0a9, 步长 1 字节
double 指针 p_d: 0x16b8aa0b0 -> 0x16b8aa0b8, 步长 8 字节
&arr_i[0] = 0x16b8aa0c8, arr_i = 0x16b8aa0c8, 相等: 1
| 指针类型 | 指向类型大小 sizeof | p+1 跳过的字节 | 说人话 |
|---|---|---|---|
int* | sizeof(int) = 4 | 4 字节 | 每格 4 字节,一步跨一格 |
char* | sizeof(char) = 1 | 1 字节 | 每格 1 字节,逐字节勘察 |
double* | sizeof(double) = 8 | 8 字节 | 每格 8 字节,一步跨一大格 |
char* 是逐字节勘察内存的"一格格走"工具:网络协议解析、文件格式解析、内存 dump(把一段内存原样打印出来)全指着它——步长 1 字节,可以精确落到内存的每一个字节上。而 &a[0] 和 a 数值相等,是因为数组名在表达式里自动退化为指向首元素的指针——这个"退化"(decay)机制是 9.4 节的主角,先记住结论:打印出来一样,类型不一样。
注意:指针越界是静默杀手。p + 100 编译器不拦,因为标准认为"你比编译器更懂你的数据";真要访问那个地址(解引用)才是未定义行为——可能碰巧正常,也可能触发段错误(segmentation fault,程序访问了不属于自己的内存,操作系统直接开枪)当场崩溃。指针算术本身是安全的,危险的是拿着算出来的地址去解引用。
顺带破一个小案:&a[0] 和 a 是同一个数,但类型完全不同——a 的类型是 int[3](数组类型),&a 的类型是 int(*)[3](指向整个数组的指针)。类型不同,步长就不同:&a[0] + 1 只跳 4 字节,而 &a + 1 直接跳过整个数组 12 字节。实测为证:
// probe.cpp —— &a+1 与 &a[0]+1 的步长对决
#include <cstdio>
int main() {
int a[3] = {10, 20, 30};
printf("&a = %p\n", (void*)&a);
printf("&a+1 = %p (跳过整个数组)\n", (void*)(&a + 1));
printf("跳过字节数 = %ld\n", (long)((char*)(&a+1) - (char*)&a));
printf("&a[0]+1 = %p (只跳一个元素)\n", (void)(&a[0] + 1));
return 0;
}
# 运行输出(本机真实输出):
&a = 0x16af6a0c8;&a+1 = 0x16af6a0d4;跳过字节数 = 12
&a[0]+1 = 0x16af6a0cc(只跳一个元素)
把数组叠成二维:int a[3][4]。先别被"二维"骗了——内存是线性的,根本没有"面"。编译器把 a[3][4] 理解为"3 个一维数组,每个一维数组里有 4 个 int",即数组的数组。整块内存依然连续,总共 3 × 4 × 4 = 48 字节。关键问题:第 0 行铺完之后,第 1 行从哪个地址开始?
答案是按行优先(row-major,先把一整行铺完,再铺下一行)。于是每个元素的地址都能用一条公式算出来:&a[i][j] = 基地址 + (i × 列数 + j) × sizeof(int)。对 a[3][4] 来说,a[1][2] 的偏移就是 1×4+2 = 6 个元素。公式记不住没关系,下面把 12 个格子的地址全部打出来,与公式逐一核对——柯南办案讲究证据链完整:
// md.cpp —— 二维数组按行优先平铺,地址公式逐一验证
#include <cstdio>
int main() {
int a[3][4] = {
{ 0, 1, 2, 3},
{10, 11, 12, 13},
{20, 21, 22, 23}
};
printf("sizeof(a) = %zu 字节(整个 3x4 数组)\n", sizeof(a));
printf("sizeof(a[0]) = %zu 字节(一行,4 个 int)\n", sizeof(a[0]));
printf("sizeof(a[0][0])= %zu 字节(一个元素)\n\n", sizeof(a[0][0]));
printf("第 0 行首地址 &a[0][0] = %p\n", (void*)&a[0][0]);
printf("第 1 行首地址 &a[1][0] = %p\n", (void*)&a[1][0]);
printf("第 2 行首地址 &a[2][0] = %p\n\n", (void*)&a[2][0]);
printf("行间差 = %ld 字节(= 4 个 int)\n\n",
(long)&a[1][0] - (long)&a[0][0]);
for (int i = 0; i < 3; i++)
for (int j = 0; j < 4; j++)
printf("a[%d][%d] 地址 %p 元素值 %2d 公式验证偏移 = %2ld\n",
i, j, (void*)&a[i][j], a[i][j],
(long)(&a[i][j] - &a[0][0]));
printf("\na[1][2] vs *(*(a+1)+2) = %d / %d\n", a[1][2], *(*(a+1)+2));
return 0;
}
# 运行输出(本机真实输出,节选):
sizeof(a) = 48 字节;sizeof(a[0]) = 16 字节;sizeof(a[0][0]) = 4 字节
第 0 行首地址 &a[0][0] = 0x16f7a60a8;第 1 行 = 0x16f7a60b8;第 2 行 = 0x16f7a60c8
行间差 = 16 字节(= 4 个 int);偏移 0..11 全部与公式吻合(见下表)
a[1][2] vs *(*(a+1)+2) = 12 / 12
# 地址公式:一维与二维的通式(T 是元素类型,如 int)
&a[i] = 基地址 + i × sizeof(T) // 一维数组
&a[i][j] = 基地址 + (i × 列数 + j) × sizeof(T) // 二维数组,行优先
@例 int a[3][4],a[1][2] 偏移 = 1×4+2 = 6 个 int = 24 字节
| 元素 a[i][j] | 公式偏移 i×4+j | 实测偏移(元素差) | 验证 |
|---|---|---|---|
| a[0][0] | 0 | 0 | 吻合 |
| a[0][1] | 1 | 1 | 吻合 |
| a[1][0] | 4 | 4 | 吻合(第 1 行紧接第 0 行) |
| a[1][2] | 6 | 6 | 吻合 |
| a[2][0] | 8 | 8 | 吻合 |
| a[2][3] | 11 | 11 | 吻合(最后一块) |
为什么"按行遍历"比"按列遍历"快?因为 Cache(高速缓存,第 1 章讲过:主存的"常用抽屉")是成块搬运数据的。按行访问时 a[0][0]、a[0][1]、a[0][2] 在内存里紧挨着,一次就被整块读进缓存,后面几个元素直接命中;按列访问则在 48 字节里跳来跳去,每次都得去慢吞吞的主存现取。内存布局决定性能直觉——这就是为什么要把地址算得明明白白。
注意:公式里的"列数"千万别写错。把 a[3][4] 的偏移写成 i×3+j(拿行数当了列数),程序不会报错,但你会读到"隔壁家"的数据——多维数组越界是静默的,编译器根本不检查。同理,访问不存在的 a[3][0](第 4 行)也是越界。写下标时心里默念:偏移 = 行 × 列数 + 列。
数组名用起来像指针:能赋值给 int*,能下标访问,打印出来还是个地址。但柯南要揭穿这个"伪装":数组名根本不是指针变量。真相是:数组名在绝大多数表达式中会退化(decay,自动转换成指向首元素的指针)——所以 int* p = a; 合法。但数组名本身没有自己的存储空间、不是左值,因此不能 a++、不能 a = p;。实测编译器对 a++ 的判决,铁证如山:
# clang++ aplus.cpp(真实编译错误):
error: cannot increment value of type 'int[3]'
2 | int main(){ int a[3]={1,2,3}; a++; return 0; }
| ~^
@解读 "cannot increment value of type 'int[3]'"——数组名不是可修改的左值,想挪它?没门
比 a++ 更阴险的是 sizeof 陷阱。在 main 里,sizeof(a) 是"整个数组"的大小 20 字节(5 个 int × 4);但一旦把数组传进函数,形参 int arr[] 与 int* arr 完全等价——参数退化为指针,sizeof(arr) 只剩 8 字节(指针大小)。同一个名字,在函数内外"体型"完全不同,编译器甚至会专门发一条警告提醒你:
// trap.cpp —— 数组名陷阱:sizeof 双面性与参数退化
#include <cstdio>
void f(int arr[]) { // 形参 int arr[] 与 int* arr 完全等价
printf(" 函数内 sizeof(arr) = %zu —— 参数已退化为指针!\n", sizeof(arr));
}
void g(int* p) {
printf(" 函数内 sizeof(p) = %zu —— 与 int* 参数完全一致\n", sizeof(p));
}
int main() {
int a[5] = {1, 2, 3, 4, 5};
printf("main 中 sizeof(a) = %zu(整个数组 5*4 字节)\n", sizeof(a));
printf("main 中 sizeof(&a[0]) = %zu(一个指针)\n\n", sizeof(&a[0]));
printf("调用 f(a):\n"); f(a);
printf("调用 g(a):\n"); g(a);
int* p = a;
printf("\np = a 之后:p[2] = %d, *(p+2) = %d, a[2] = %d\n", p[2], *(p+2), a[2]);
printf("p++ 之后 p 指向 a[1]:*p = %d\n", *(p++));
return 0;
}
# 编译(真实警告,clang++):
warning: sizeof on array function parameter will return size of 'int *' instead of 'int[]' [-Wsizeof-array-argument]
# 运行输出(本机真实输出):
main 中 sizeof(a) = 20(整个数组);main 中 sizeof(&a[0]) = 8(一个指针)
调用 f(a):函数内 sizeof(arr) = 8;调用 g(a):函数内 sizeof(p) = 8
p = a 之后:p[2] = 3, *(p+2) = 3, a[2] = 3;p++ 之后 *p = 1
| 表达式 / 场景 | sizeof 结果 | 原因 |
|---|---|---|
main 中 sizeof(a)(a 是 int a[5]) | 20 | 类型是 int[5],5×4 字节 |
main 中 sizeof(&a[0]) | 8 | &a[0] 是指针,不是数组 |
函数内 sizeof(arr)(形参 int arr[]) | 8 | 形参已退化为 int* |
函数内 sizeof(p)(形参 int* p) | 8 | 本来就是指针 |
想保住"数组类型"?把参数声明成引用:void f(int (&arr)[5]),这样 sizeof(arr) 还是 20,传错长度编译器直接报错。但更实用、更 C++ 的做法是:传数组时把长度一起传进去(void f(int* arr, int n))——标准库的 std::vector 也是这个思路:数据和长度永不分离。从今往后,凡是"数组参数",第一反应就是问"长度传了没"。
注意:那条 -Wsizeof-array-argument 警告是友军,它明明白白告诉你"这里 sizeof 的是指针"。无视它的下场很经典:函数里写 for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) 想遍历"整个数组",结果 sizeof(arr)/sizeof(arr[0]) 是 8/4 = 2——只处理了前两个元素,后面三个悄悄失踪。这是初学者最容易踩的坑,没有之一。
理论归理论,跟着柯南做一个小案子:一个班 6 个学生的成绩,老师心软统一加 5 分,然后算总分和平均分,最后把数组整个倒过来。全程指针操作——不对,下标也一起用,实战里两者本就混着来,谁方便用谁:
// practice.cpp —— 用指针遍历、修改、反转数组
#include <cstdio>
int main() {
int scores[6] = {72, 85, 91, 66, 78, 88};
int n = 6;
printf("原始成绩:");
for (int i = 0; i < n; i++) printf("%d ", scores[i]);
printf("\n");
// 1) 指针遍历:每个元素 +5(老师心软,全员加 5 分)
for (int* p = scores; p < scores + n; p++) {
*p += 5; // 解引用后自增,等价 scores[i] += 5
}
printf("加 5 分后:");
for (int* p = scores; p < scores + n; p++) printf("%d ", *p);
printf("\n");
// 2) 指针求和
int sum = 0;
for (int* p = scores; p < scores + n; p++) sum += *p;
printf("总分 = %d,平均 = %.1f\n", sum, (double)sum / n);
// 3) 双指针原地反转(头尾交换,相向而行)
for (int* lo = scores, *hi = scores + n - 1; lo < hi; lo++, hi--) {
int t = *lo; *lo = *hi; *hi = t; // 经典三行交换
}
printf("反转后:");
for (int i = 0; i < n; i++) printf("%d ", scores[i]);
printf("\n");
// 4) 指针 + 下标混用
printf("scores[2] = %d,*(scores+2) = %d\n", scores[2], *(scores + 2));
return 0;
}
# 运行输出(本机真实输出):
原始成绩:72 85 91 66 78 88
加 5 分后:77 90 96 71 83 93
总分 = 510,平均 = 85.0
反转后:93 83 71 96 90 77
scores[2] = 71,*(scores+2) = 71
三个要点。第一,遍历的循环边界是哨兵指针(sentinel pointer,指向"数组末尾之后一个位置"的指针)scores + n:条件写 p < scores + n,指针一路 +1 直到越过最后一个元素——比数下标更贴近"内存视角",也天然免疫"数组长度改变"的麻烦。第二,反转用双指针:lo 从前往后、hi 从后往前,每次交换两端值,相向而行直到交叉(lo < hi 不成立)。第三,*p += 5 是"解引用 + 自增"的组合拳:先拿到格子里的值,加 5 再写回去——和 scores[i] += 5 是同一件事的两种写法。
// 反转的核心骨架(上面第 3 步的提炼)
for (int* lo = arr, *hi = arr + n - 1; lo < hi; lo++, hi--) {
int t = *lo; *lo = *hi; *hi = t;
}
@边界 n == 0 时 arr + n - 1 越界;条件必须写 lo < hi,不能写 lo != hi
| 操作 | 指针写法 | 下标写法 | 等价性 |
|---|---|---|---|
| 第 k 个元素加 5 | *p += 5 | a[k] += 5 | 完全相同 |
| 求和 | sum += *p | sum += a[i] | 完全相同 |
| 取第 k 个元素 | *(p + k) | a[k] | 标准等价 |
| 反转 | 双指针 lo/hi 交换 | 临时数组拷贝 | 指针版省内存 |
"begin/end 哨兵"正是 C++ 标准库迭代器的雏形:std::vector 的 begin() 和 end() 就是"指向第一个元素"和"指向末尾之后一个位置"这两个指针的类型安全封装,std::sort、std::for_each 等算法全部建立在这个约定上。学透指针遍历,迭代器对你来说就只是一层语法糖——这也是为什么前辈们敢说"学会指针,你就学会了一半 C++"。
注意:双指针反转有两个边界要命。一是空数组:n == 0 时 scores + n - 1 直接指向数组前面的未知内存,是野指针;二是循环条件:写成 lo != hi 而不是 lo < hi,当数组长度为偶数时两个指针会"擦肩而过"(lo 越过 hi 却不相等),循环停不下来直接越界。反转前先问自己:数组可能为空吗?长度是奇数还是偶数?
把这一章的"数据流"完整串一遍:声明数组 → 编译器划出一整块连续内存 → 数组名在表达式中退化为指针 → 指针算术按"类型单位"走步长 → 多维数组按行优先平铺成一维 → 传参时数组退化成指针、sizeof 随之缩水。整章其实只回答了三类问题:这块内存多大(sizeof)、从哪开始(地址)、走一步多远(步长)。这三问答全了,任何数组/指针代码在你眼里都是透明的。
柯南的结案陈词:以后遇到数组或指针,条件反射地问自己三个问题——内存多大?首地址在哪?指针 +1 跳多远?程序出了问题,取证顺序也一样:先打印 &x[0],再打印 &x[1],看差值对不对——两步就能锁定是"步长错"还是"越界"。
# 柯南的取证口诀:四问四答
内存多大? sizeof 一测便知 -> int a[3] 就是 12 字节
首址在哪? &a[0] / a 打印即见 -> 两者数值相同、类型不同
一步多远? 看指向类型 -> int 4 / char 1 / double 8
多维怎么铺? 按行优先 -> 偏移 = i × 列数 + j
传参之后? 退化成指针 -> sizeof(arr) 只剩 8
// selfcheck.cpp —— 三行代码,把本章结论一次性验完
#include <cstdio>
int main() {
int a[3][4] = {0};
printf("sizeof(a) = %zu 字节\n", sizeof(a));
printf("a[1] 相对 &a[0][0] 偏移 %ld 字节\n",
(long)((char*)a[1] - (char*)&a[0][0]));
printf("a[1][2] = %d, *(*(a+1)+2) = %d\n", a[1][2], *(*(a+1)+2));
return 0;
}
# 本机真实输出:sizeof(a) = 48;偏移 16 字节;a[1][2] = 0,*(*(a+1)+2) = 0 —— 全部吻合
| 三问 | 答案 | 出处 |
|---|---|---|
| 这块内存多大? | sizeof(a) = 12 / 48 | 9.1、9.3 |
| 首地址在哪? | &a[0] 与 a 数值相同 | 9.2 |
| 指针 +1 跳多远? | int 4 / char 1 / double 8 字节 | 9.2 |
养成一个习惯:怀疑一切结论,验证一切规律。本章的每条结论(步长、sizeof、行优先)都只用两行代码就能自己验出来,而且换机器、换编译器结果一致(只有绝对地址会变)。下次被问"指针和数组什么关系",别背定义——直接说:数组名退化成指针,但 sizeof 和 & 面前它还是数组,我用两行代码就能证明。
注意:别死记口诀。口诀是为了调试时快速回忆,不是背完就完——把 selfcheck.cpp 自己编译运行一遍,亲眼看到 48、16、0 这三个数,比读十遍文章都管用。柯南的办案原则永远是:证据优先于记忆,实测优先于直觉。下一章开始,我们会遇到比数组更"有结构"的对象——struct 和 class,那时候"内存多大、首址在哪、步长多少"这三问,会以更精彩的方式继续发挥作用。
一次实测中,int a[3] 的 &a[0] = 0x16fcb60d8。请推算:① &a[1] 和 &a[2];② a 和 &a 打印出来是多少;③ sizeof(a)。
回顾 9.1 节:int 是 4 字节,三个格子首尾相接;数组名在表达式中退化为指针,&a 取整个数组的地址。
① &a[1] = 0x16fcb60dc、&a[2] = 0x16fcb60e0(每次 +4);② a 和 &a 都打印为 0x16fcb60d8(数值相等,类型不同:前者退化为 int*,后者是 int(*)[3]);③ sizeof(a) = 12(3 × 4 字节)。
double b[5];,double* p = b;。① p + 1 与 p 相差多少字节?② &b 与 &b[0] 数值上相等吗?③ &b + 1 跳过多少字节?④ b[2] 用指针怎么写?
double 是 8 字节;&b 的类型是 double(*)[5],指向整个数组;下标 b[i] 等价 *(b+i)。
① 8 字节(步长 = sizeof(double));② 相等(都是数组首地址,类型不同);③ 40 字节(整个数组 5 × 8,一步跨完整条街);④ *(p + 2) 或 *(b + 2)。
有函数 void f(int arr[10]) { printf("%zu", sizeof(arr)); },main 里声明 int a[10];。① main 中 sizeof(a)?② main 中 sizeof(&a[0])?③ f 内 sizeof(arr)?④ 为什么③和①不一样?
回顾 9.4 节:形参 int arr[10] 与 int* arr 完全等价,参数退化为指针。
① 40(整个数组 10 × 4);② 8(&a[0] 是指针);③ 8(形参已退化为 int*);④ 因为标准规定数组作为函数参数时退化为指向首元素的指针,函数只拿到"首地址"、拿不到"长度"——这正是编译器发 -Wsizeof-array-argument 警告的原因。所以传数组必须同时传长度。
① 写一个 void reverse(int* arr, int n) 函数,用双指针原地反转数组(不申请新数组),并解释为什么函数内不能用 sizeof(arr) 求元素个数。② 加分项:把 9.5 的反转改成"一边反转一边打印每次交换后的数组",观察双指针从两端向中间收拢的过程。
双指针模板:lo 指向 arr[0],hi 指向 arr[n-1],循环条件 lo < hi,交换后 lo++、hi--;n 的奇偶和空数组都要考虑。
① for (int* lo = arr, *hi = arr + n - 1; lo < hi; lo++, hi--) { int t = *lo; *lo = *hi; *hi = t; }。不能用 sizeof(arr) 求长度:arr 是形参,已退化为 int*,sizeof(arr) 恒为 8(指针大小),所以必须靠参数 n 传递长度。② 在交换后加一行 printf 打印数组即可——你会看到头尾逐渐"翻转",这正是双指针追踪数据流的过程。