今天在论坛上看有laphon同学的一个问题,觉得这个问题很有意思。
原帖地址
问题援引如下:
做一个小程序的时候发现的。代码如下,使用的编译器为DEV-C++ 4.9.9.2。
如果先要求输入a,再要求输入b,那么a的值无论输入多少(少于255)输出都会是0;
反过来,如果先要求输入b,再要求输入a,那么就会正常。
这是为什么呢?请教达人解释。#include #include int main(int argc,char **argv) { unsigned char a,b; scanf("%d",&a); scanf("%d",&b); printf("a=%d,b=%dn",a,b); scanf("%d",&b); scanf("%d",&a); printf("a=%d,b=%dn",a,b); system("pause"); }
devcpp用的是gcc编译器,自己用gcc试了一下,果然是这样。为什么出现这种问题呢?开gdb调试,信息如下:
main (argc=1, argv=0xbfbaf3e4) at a.c:7
7 scanf("%d",&a);
(gdb) n
1
8 scanf("%d",&b);
(gdb) p a
$1 = 1 ' 01'
(gdb) n
2
9 printf("a=%d,b=%dn",a,b);
(gdb) p a
$2 = 0 ' '
(gdb) p b
$3 = 2 ' 02'
(gdb)
从第12行可以看出,在第一次scanf读入b后,a的值被清0了,这样原因就好分析了。用%d读b的时候,实际上把b转型成了int,即scanf(“%d”,&b)实际上等于 int * p = &b; *p = 2 ,因为scanf接受的参数是指针。问题明显了,int4个字节,char1个字节,scanf实际上向内存里写了4个字节,高地址的3个字节为0,把a的1个字节和前面的参数区的2个字节给覆盖了。
如果声明顺序反过来,b在高地址,就不会把a覆盖,覆盖掉的是栈头部的参数区的3个字节。
但是无论那种方法,都是危险的,都会造成数据丢失。gcc 在开启 -Wall 开关后会给出警告
warning: format ‘%d’ expects type ‘int *’, but argument 2 has type ‘char *’
(这个warning应该是针对c标准库函数设计的)。
这种转型实际上C++其实是不允许的。比如
char a; int * p = &a;
编译器(g++)会毫不客气的给出一个error:
error: cannot convert ‘char*’ to ‘int*’ in initialization
但是实际上那段“问题代码”仍然可以编译通过。为什么呢?查看了stdio.h,scanf函数的原型如下:
extern int scanf (__const char *__restrict __format, ...)
printf最后一个参数”…”是一个变长参数,传参时实际传了一个void*进去,真正的类型分析和转型动在printf函数定义内的va_arg宏完成。所以编译器在分析这个函数调用时是无法知到真正的参数类型的。而scanf定义部分早被编译成了2进制lib。所以这段代码在c++中也可以编译通过。
另外有人说Intel C编译器没有这个问题,自己试了一下vc也没这个问题,这令我很费解。个人觉得这个问题似乎很难避免,因为这种用变长参数列表传递的参数,参数类型完全由前面的format string决定,可惜搞不到源码,就没法深究了。
4 Responses to 类型转换-无处不在的陷阱