程序员面试宝典中关于 char * b=(char *)&a的解释有错吗?

 
图示文章地址: http://www.cppblog.com/wuzimian/archive/2013/05/07/175925.html  
 
上图中列出的《宝典》中对于那两行代码的解释(上图中绿色字),是完全正解的。
char *b = (char*)p;
这里确实是char类型指针的转换,而不是char类型的转换,影响的只是指针的寻址。
最终b所指向的内存地址,确是和&a一样。只不过在内存访问时,指针的类型会对寻址进行限制而已。关于这一点,可通过下面的实验证明。
 
《宝典》中原文:
 
  
 
由此可见,关于输出 FF FF FF FF7 ,确是因printf参数 %08x 的影响。《宝典》中并没有说清楚。
宝典中在这里大谈b 的指向问题,还把 char* b = (char*)&a; 这句代码写成两句以进一步解释这个问题,确实有
很大的嫌疑让用户认为最终的结果是由于b的指向和&a一样造成的。
 
另外此博客文章作者说把a的值修改后,输出同样不会变,这一点是正确的,可简单通过如下程序测试:
 
#include <stdio.h>
 
int main()
{
     unsigned int a = 0xDEADBEF7;
     unsigned char i = (unsigned char)a;
     char* b = (char*)&a;
     printf("%08x, %x\n", i, *b);
    //输出结果同样为  000000f7, fffffff7
    return 0;
}
 
好了,开始关于指针b的指向和&a一样这个问题的验证:
这是上图中的原程序,顺便说下博客文章的代码有些不标准,如,明明是C程序,却用 CPP的头文件,还有,指定了main函数应该返回int类型的数据,却没有返回。当然,在这里我们不是讨论这个,而是关于b的指向的问题。
#include <stdio.h>
 
int main()
{
     unsigned int a = 0xFFFFFFF7;
     unsigned char i = (unsigned char)a;
     char* b = (char*)&a;
     printf("%08x, %08x\n", i, *b);
    //输出  000000f7, fffffff7
    return 0;
}
 
 
用GNU GCC 编译成 exe 文件后,用IDA得到汇编代码,我们看下main函数:
 

.text:00401334 ; =============== S U B R O U T I N E =======================================
.text:00401334
.text:00401334 ; Attributes: bp-based frame
.text:00401334
.text:00401334 sub_401334      proc near               ; CODE XREF: sub_401000+F8p
.text:00401334
.text:00401334 var_20          = dword ptr -20h
.text:00401334 var_1C          = dword ptr -1Ch
.text:00401334 var_18          = dword ptr -18h
.text:00401334 var_C           = dword ptr -0Ch
.text:00401334 var_8           = dword ptr -8
.text:00401334 var_1           = byte ptr -1
.text:00401334
.text:00401334                 push    ebp
.text:00401335                 mov     ebp, esp
.text:00401337                 and     esp, 0FFFFFFF0h
.text:0040133A                 sub     esp, 20h
.text:0040133D                 call    sub_401940
.text:00401342                 mov     [esp+20h+var_C], 0FFFFFFF7h ;unsigned int a = 0xFFFFFFF7;
.text:0040134A                 mov     eax, [esp+20h+var_C]
.text:0040134E                 mov     [esp+20h+var_1], al    ;取32位无符号整型变量a中的低8位给无符号字符变量i,即unsigned char i = (unsigned char)a;
.text:00401352                 lea     eax, [esp+20h+var_C]        ;取变量a的地址,即 &a ,丢给eax寄存器
.text:00401356                 mov     [esp+20h+var_8], eax        ;将这个地址保存在指针char *b中,即 char* b = (char*)&a;

.text:0040135A                 mov     eax, [esp+20h+var_8]        ;指针char *b丢给eax
.text:0040135E                 mov     al, [eax]                   ;计算*b,因指针b所指向的是单个字节,故此处即取最低位0F7h,存入al中
.text:00401360                 movsx   edx, al                     ;signed char *b中的数(补码),按符号位扩充到32位signed int (变成FF FF FF F7了)
.text:00401363                 movzx   eax, [esp+20h+var_1]   ;对于unsigned char i,将单个字节i (byte ptr )前面填充0扩展到32位的unsigned int
.text:00401368                 mov     [esp+20h+var_18], edx   ;  FFFFFFF7 ,虽然应该理解为signed int ,不过这里我们指定printf要按unsigned hex int来理解
.text:0040136C                 mov     [esp+20h+var_1C], eax   ;  000000F7
.text:00401370                 mov     [esp+20h+var_20], offset a08x08x ; "%08x, %08x\n"
.text:00401377                 call    printf
.text:0040137C                 mov     eax, 0                   ;main函数返回值
.text:00401381                 leave
.text:00401382                 retn
.text:00401382 sub_401334      endp
.text:00401382
.text:00401382 ; ---------------------------------------------------------------------------
 
注意到标红色的5句汇编代码:
mov     [esp+20h+var_C], 0FFFFFFF7h  ;unsigned int a = 0xFFFFFFF7;
mov     eax, [esp+20h+var_C]
mov     [esp+20h+var_1], al          ;unsigned char i = (unsigned char)a;
lea     eax, [esp+20h+var_C]      
mov     [esp+20h+var_8], eax
            ;char* b = (char*)&a;
可以看到,指针b的指向(esp+20h+var_C) ,最终和&a 相同。
 
我们可以进一步将上面的printf语句去掉,再看下:
int main()
{
     unsigned int a = 0xFFFFFFF7;
     unsigned char i = (unsigned char)a;
     char* b = (char*)&a;
     return 0;
}
 
.text:00401330 ; ---------------------------------------------------------------------------
.text:00401331                 align 4
.text:00401334
.text:00401334 ; =============== S U B R O U T I N E =======================================
.text:00401334
.text:00401334 ; Attributes: bp-based frame
.text:00401334
.text:00401334 sub_401334      proc near               ; CODE XREF: sub_401000+F8p
.text:00401334
.text:00401334 var_C           = dword ptr -0Ch    ;unsigned int a
.text:00401334 var_8           = dword ptr -8      ;char* b
.text:00401334 var_1           = byte ptr -1       ;unsigned char i  
.text:00401334
.text:00401334                 push    ebp
.text:00401335                 mov     ebp, esp
.text:00401337                 and     esp, 0FFFFFFF0h
.text:0040133A                 sub     esp, 10h
.text:0040133D                 call    sub_401920
.text:00401342                 mov     [esp+10h+var_C], 0FFFFFFF7h ;unsigned int a = 0xFFFFFFF7;
.text:0040134A                 mov     eax, [esp+10h+var_C]
.text:0040134E                 mov     [esp+10h+var_1], al         ;unsigned char i = (unsigned char)a;
.text:00401352                 lea     eax, [esp+10h+var_C]
.text:00401356                 mov     [esp+10h+var_8], eax        ;char* b = (char*)&a;

.text:0040135A                 mov     eax, 0
.text:0040135F                 leave
.text:00401360                 retn
.text:00401360 sub_401334      endp
.text:00401360
.text:00401360 ; ---------------------------------------------------------------------------
 
x表示: Unsigned hexadecimal integer
因此即使(%08x)前面不加上08,也会按32位的结果输出。
 
如果修改为 %d ,那么输出为 -0000009
如果修改为 %u ,那么输出为 4294967287
如果修改为 %x ,那么输出还是为 FFFFFFF7,因为hex格式的也是按无符号整型的。
 
尽管%x 是按无符号整型输出,不过这并不影响 signed char隐式转换为 signed int,而后,printf将这个signed int值按照unsigned int输出。
如果按照signed int来输出,那么就是 -9.
 
为了测试下C语言的这种转换规则,这里再写个程序测试下(重要是标红的部分):
 
int main()
{
     unsigned int a = 0xDEADBEF7;
     unsigned char i = (unsigned char)a;
     char* b = (char*)&a;
     char c = *b;
     int d = *b;
     unsigned int e = *b;

     return 0;
}
 
这里我们并不输出结果,而是直接在调试器里验证结果:
 
 
 
 
00401334  /$  55            PUSH EBP
00401335  |.  89E5          MOV EBP,ESP
00401337  |.  83E4 F0       AND ESP,FFFFFFF0
0040133A  |.  83EC 20       SUB ESP,20
0040133D  |.  E8 FE050000   CALL p4.00401940
00401342  |.  C74424 08 F7B>MOV DWORD PTR SS:[ESP+8],DEADBEF7        ;  unsigned int a = 0xDEADBEF7;
0040134A  |.  8B4424 08     MOV EAX,DWORD PTR SS:[ESP+8]
0040134E  |.  884424 1F     MOV BYTE PTR SS:[ESP+1F],AL              ;  unsigned char i = (unsigned char)a;
00401352  |.  8D4424 08     LEA EAX,DWORD PTR SS:[ESP+8]
00401356  |.  894424 18     MOV DWORD PTR SS:[ESP+18],EAX            ;  char* b = (char*)&a;
0040135A  |.  8B4424 18     MOV EAX,DWORD PTR SS:[ESP+18]
0040135E  |.  8A00          MOV AL,BYTE PTR DS:[EAX]
00401360  |.  884424 17     MOV BYTE PTR SS:[ESP+17],AL              ;  char c = *b;
00401364  |.  8B4424 18     MOV EAX,DWORD PTR SS:[ESP+18]
00401368  |.  8A00          MOV AL,BYTE PTR DS:[EAX]
0040136A  |.  0FBEC0        MOVSX EAX,AL
0040136D  |.  894424 10     MOV DWORD PTR SS:[ESP+10],EAX            ;  int d = *b;
00401371  |.  8B4424 18     MOV EAX,DWORD PTR SS:[ESP+18]
00401375  |.  8A00          MOV AL,BYTE PTR DS:[EAX]
00401377  |.  0FBEC0        MOVSX EAX,AL
0040137A  |.  894424 0C     MOV DWORD PTR SS:[ESP+C],EAX             ;  unsigned int e = *b;

0040137E  |.  B8 00000000   MOV EAX,0
00401383  |.  C9            LEAVE
00401384  \.  C3            RETN
 
 
从以上汇编代码可以看到,
     int d = *b;
     unsigned int e = *b;
这两条语句,d 和 e的值在内存中是二进制相等的。当然,相等是必须,这里需要注意的就是按符号扩展,右值原来是signed,因此,不管左值是signed还是unsigned ,都按符号位扩展了。
因此, d == e == 0xFFFFFFF7 .
 
结论:截断只是发生在寻址进行时。指针的类型,只是在解引用访问内存时进行寻址限制。执行char* b = (char*)&a 时并无截断。
程序员面试宝典中关于 char * b=(char *)&a的解释并无不当之处,只是书中并没有把程序的结果产生的原因讲清楚,输出fffffff7的原因,只因截断之后,由于一般编译器下的char类型默认为signed char,因此f7在内部被当作一个负数了,按printf的参数要求,signed char类型的数据扩展为了int型(这个扩展,跟printf参数%08x中的x有关,而跟08无关。在这里是signed int,因数据原来为signed),最后按我们指定的格式(unsigned int hex)输出。要注意到隐形转换是从signed char 到 signed int,最终因%08x参数指示,按unsigned int输出了。