类型转换-无处不在的陷阱

今天在论坛上看有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决定,可惜搞不到源码,就没法深究了。

This entry was posted in 技术学习. Bookmark the permalink.

4 Responses to 类型转换-无处不在的陷阱

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据