一个关于c语言的指针移动问题
背景
在近期聊天的时候,有一个人(这个人嵌入式水平非常高,大家一般叫他 嵌入式之神)发了一个选择题说自己不会,我也看了看。
📝 题目
单选题 C 语言
28. 下面程序的输出是什么?
int main(void) {
int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf("%d %d", *(a + 1), *(ptr - 1));
return 0;
}
- A 2 1
- B 2 5
- C 1 2
- D 5 2
TIP
我这里直接把答案写出来了,答案是 B,大部分人可能会去选择 A,可能也些人比较扎实可以选择正确。
🔍 解析
好的,我们来详细解析这段 C 语言代码。
代码分步解析
让我们逐行分析代码,特别是涉及指针操作的部分:
int main(void) {
// 1. 声明并初始化一个包含 5 个整数的数组
int a[5] = {1, 2, 3, 4, 5};
// 2. 声明一个整型指针 ptr,这是最关键的一行
int *ptr = (int *)(&a + 1);
// 3. 打印两个整数的值
printf("%d %d", *(a + 1), *(ptr - 1));
return 0;
}
关键点 1:&a 和 a 的区别
这是理解这道题的核心。
a:在大多数表达式中(包括*(a + 1)中),数组名a会"衰变"为一个指向数组第一个元素的指针。它的类型是int *。&a:这是对整个数组取地址。它是一个指向"包含 5 个整数的数组"的指针。它的类型是int (*)[5]。
WARNING
很多人误解为这样,如果衰变了,a 的类型就是 int,那难道 &a 的类型是 int ** 吗?很显然这里有问题。
a 的真实类型自始至终都是 int[5](一个包含 5 个整数的数组)。它不是指针。在大多数使用到数组名的表达式中,编译器会自动将数组名 a 隐式地转换为一个指向其第一个元素的指针。这个转换后的指针,其类型是 int *,其值是 a[0] 的地址。
关键点 2:指针的算术运算
指针进行加减运算时,移动的步长取决于指针指向的类型的大小。
| 表达式 | 类型 | +1 移动的字节数 |
|---|---|---|
a | int * | sizeof(int),指向下一个整数 a[1] |
&a | int (*)[5] | sizeof(int[5]),整个数组的大小 |
详细分析 int *ptr = (int *)(&a + 1);
&a:获取整个数组a的地址&a + 1:对这个指向整个数组的指针加 1,使其指向数组a内存区域之后紧邻的位置。可以想象成它指向了a[5]这个虚拟元素的地址(int *):这是一个强制类型转换。它将&a + 1这个int (*)[5]类型的地址,转换成了一个普通的int *类型的指针- 赋值后,
ptr就成了一个int *指针,它指向了数组a末尾的下一个内存位置
详细分析 printf 语句
printf 会计算所有参数的值,然后进行输出。
1. 计算第一个要打印的值 *(a + 1):
a衰变为指向a[0]的指针a + 1指向数组的第二个元素a[1]*(a + 1)对该地址解引用,得到a[1]的值,也就是2
2. 计算第二个要打印的值 *(ptr - 1):
- 我们已经知道
ptr指向数组a末尾的下一个位置(即a[4]之后的位置) ptr是一个int *类型的指针,所以ptr - 1会将指针地址向前移动一个sizeof(int)的大小- 这使得
ptr - 1正好指向了数组的最后一个元素a[4] *(ptr - 1)对该地址解引用,得到a[4]的值,也就是5
✅ 总结
因此,printf 语句最终会打印出 2 和 5,格式为 %d %d,所以输出是:
2 5
