c 中指针乱用造成的一个越界问题

《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 在内存布局如下:
int 变量内存布局

而在问题代码中,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 的高地址方向。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值