《C陷阱与缺陷》第四章 4.4 节提到了一个有趣的问题,代码如下:
#include<stdio.h>
int main()
{
int i;
char c;
for(i = 0; i < 5; i++)
{
scanf("%d",&c);
printf("i=%d\n",i);
}
printf("\n");
}
这段代码很简单,它的本意是循环5次,向 char 类型变量中不停输入一个值。
但是实际运行(gcc version 9.3.0 + ubuntu 20)结果却如下:
1
i=0
2
i=0
3
i=0
4
i=0
5
i=0
6
i=0
7
i=0
8
i=0
11111111111111
i=119985
可以看到,当输入数据时,i 会一直被置为 0,导致循环结束不了;只有当输入的 c 很大时,才会结束。
问题的根源在于这里的 scanf ,使用的类型是 “%d” 而不是 “%c”;虽然会有警告表示类型不正确,但毕竟只是警告。所以执行 scanf 时,scanf 认为用户的输入是个 int,因此它实际要写入的数据是 4 个而不是 1 个字节。
计算机存储数据有大小端法的区别,本人的机器是小端类型。小端法的机器,变量的地址与最低字节的地址是一样的,如以下代码所示:
#include<stdio.h>
int main()
{
int a = 0x12345678;
printf("&a=%u\n",&a);
char *c = &a;
for(int i = 0; i < 4;i++)
{
printf("c=%u,*c=%x\n",c,*c);
c++;
}
printf("%u\n",*(char*)(&a));
}
上面代码的执行如下:
&a=3201732040
c=3201732040,*c=78
c=3201732041,*c=56
c=3201732042,*c=34
c=3201732043,*c=12
120
可以看到,最低位数对应的字节地址与变量的地址一样,而且地址从低到高,即变量 a 在内存布局如下:
而在问题代码中,i 和 c 是连续存储在栈上的,它们的存储类似下边这样:
本来 i 与 c 井水不犯河水,但是在 scanf 时,&c 被解释成了一个 *int,而 *int 又是指向一个 int 的最低字节,于是从 i 最低字节开始往高地址方向的 3 个连续字节,被认为是 (*int)(&c) 高地址的一部分。
于是,当我们执行输入时,如果输入的数据不超过 255,会把这个值赋给 c,同时又往 i 的 3 个低地址字节里边填充 0,于是 i 的值每次在循环结束时都变成 0,下次循环开始时又变成 1,只要 输入的 c 足够小,就永远出不了循环;而当 c 输入足够大时,哪怕只循环一次,也能立马退出,这个最小值在这里是 16 进制的 0x0400,即 10 进制的 1024,。
这里还有一个有意思的问题,上面是因为 i 和 c 两个变量连续挨着且 i 在高地址,所以 scanf 时会溢出到 i 的低字节上;如果 c 和 i 交换声明顺序,是不是就能避免这个问题?使用 gcc 默认的方式并不能避免:
如代码改为:
#include<stdio.h>
int main(){
//int i;
char c;
int i;
printf("&i=%u\n",&i);
printf("&c=%u\n",&c);
for(i=0;i<5;i++){
scanf("%d",&c);
printf("i=%d\n",i);
}
printf("\n");
}
上面的代码中,c 先于 i 声明,直观上 c 更靠近栈底,然而单纯使用 gcc 编译出来显示 c 依然在更靠近栈顶的位置:
&i=2379985396
&c=2379985395
1
i=0
123
i=0
111111111
i=434027
本人猜测 c 在编译时做了内存优化,由于 c 和 i 的声明顺序并不影响执行,而先 i 后 c 的方式更节省内存,所以依然使用了未调整前的内存布局。(这一段纯属根据结果的猜测,不靠谱)。如果运行下面的代码,会更懵逼:
#include<stdio.h>
int main()
{
int i;
char c1,c2,c3,c4;
//char c1,c2,c3;
int j;
//printf("c1=%p c2=%p c3=%p c4=%p i=%p\n",&c1,&c2,&c3,&c4,&i);
printf("c1=%p c2=%p c3=%p c4=%p i=%p j=%p\n",&c1,&c2,&c3,&c4,&i,&j);
/**
for(i = 0; i < 5; i++)
{
scanf("%d",&c4);
printf("c1=%c c2=%c c3=%c c4=%c i=%d\n",c1,c2,c3,c4,i);
}
printf("\n");
**/
}
上面的代码中,无论怎么调整 6 个变量的声明顺序,似乎 i 和 j 都永远在一起,且都在 4 个 char 的高地址方向。