unsigned 关键字,你真的懂了吗?

前不久和同事谈论起 unsigned 关键字,故今天小结一下。

以32位机为例,int 分为无符号 unsigned 和有符号 signed 两种类型,默认为signed。二者的区别就是无符号类型能保存2倍于有符号类型的数据。32位下,signed int 的表示范围为:-2147483648 ~ 2147483647 (最高位做符号位)。unsigned int 的表示范围为:0 ~ 4294967295 (不保留符号位)。我们都知道,两个不同的数据类型在进行混合使用时,会自动进行类型转换。其转换原则就是:向着精度更高、长度更长的方向转换。也就是我们平常见到的 char 转为 int ,int 转为 long,float 转为 double  . etc. 那么当涉及到unsigned 类型时,又会进行怎样转换呢?下面来看个小例子:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int arr[]={1,2,3,4};
    if(-1<sizeof(arr)/sizeof(arr[0]))
        printf("Hello world!\n");
    else
        printf("Error!\n");
    return 0;
}

按照我们平时的逻辑:sizeof(arr)=4 (数组长度), sizeof(arr[0]) =4 (int 所占字节),所以,if(-1<1) 成立,执行hello world。可事实却相反,输出为Error。

下面我们看一下它的反汇编:(不想看的童鞋可以跳过)

int main()
{
00F31690  push        ebp  
00F31691  mov         ebp,esp  
00F31693  sub         esp,0DCh  
00F31699  push        ebx  
00F3169A  push        esi  
00F3169B  push        edi  
00F3169C  lea         edi,[ebp-0DCh]  
00F316A2  mov         ecx,37h  
00F316A7  mov         eax,0CCCCCCCCh  
00F316AC  rep stos    dword ptr es:[edi]  
00F316AE  mov         eax,dword ptr [___security_cookie (0F37000h)]  
00F316B3  xor         eax,ebp  
00F316B5  mov         dword ptr [ebp-4],eax  
    int arr[]={1,2,3,4};
00F316B8  mov         dword ptr [ebp-18h],1  
00F316BF  mov         dword ptr [ebp-14h],2  
00F316C6  mov         dword ptr [ebp-10h],3  
00F316CD  mov         dword ptr [ebp-0Ch],4  
    if(-1<sizeof(arr)/sizeof(arr[0]))
00F316D4  xor         eax,eax  
00F316D6  je          main+61h (0F316F1h)  
        printf("Hello world!\n");
00F316D8  mov         esi,esp  
00F316DA  push        offset string "Hello world!\n" (0F3577Ch)  
00F316DF  call        dword ptr [__imp__printf (0F382CCh)]  
00F316E5  add         esp,4  
00F316E8  cmp         esi,esp  
00F316EA  call        @ILT+320(__RTC_CheckEsp) (0F31145h)  
    else
00F316EF  jmp         main+78h (0F31708h)  
        printf("error\n");
00F316F1  mov         esi,esp  
00F316F3  push        offset string "error\n" (0F35774h)  
00F316F8  call        dword ptr [__imp__printf (0F382CCh)]  
00F316FE  add         esp,4  
00F31701  cmp         esi,esp  
00F31703  call        @ILT+320(__RTC_CheckEsp) (0F31145h)  
    return 0;
00F31708  xor         eax,eax  
}

注意一下第22,23行,xor     eax,eax  异或运算的结果为0,所以总会跳转至0F316F1h。执行Error语句。

下面我们再修改一下这个程序:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int arr[]={1,2,3,4};
    if((double)-1<sizeof(arr)/sizeof(arr[0]))
        printf("Hello world!\n");
    else
        printf("error\n");
    return 0;
}
注意,这次我们在if语句里,将-1转换成了double类型。编译运行一下:hello world 。我们想要的结果终于出来了,在我们欣喜的同时,不禁要问:why ? 

我们再来看一下它的汇编:

int main()
{
00BE1690  push        ebp  
00BE1691  mov         ebp,esp  
00BE1693  sub         esp,0DCh  
00BE1699  push        ebx  
00BE169A  push        esi  
00BE169B  push        edi  
00BE169C  lea         edi,[ebp-0DCh]  
00BE16A2  mov         ecx,37h  
00BE16A7  mov         eax,0CCCCCCCCh  
00BE16AC  rep stos    dword ptr es:[edi]  
00BE16AE  mov         eax,dword ptr [___security_cookie (0BE7000h)]  
00BE16B3  xor         eax,ebp  
00BE16B5  mov         dword ptr [ebp-4],eax  
    int arr[]={1,2,3,4};
00BE16B8  mov         dword ptr [ebp-18h],1  
00BE16BF  mov         dword ptr [ebp-14h],2  
00BE16C6  mov         dword ptr [ebp-10h],3  
00BE16CD  mov         dword ptr [ebp-0Ch],4  
    if((double)-1<sizeof(arr)/sizeof(arr[0]))
00BE16D4  mov         eax,1  
00BE16D9  test        eax,eax  
00BE16DB  je          main+66h (0BE16F6h)  
        printf("Hello world!\n");
00BE16DD  mov         esi,esp  
00BE16DF  push        offset string "Hello world!\n" (0BE577Ch)  
00BE16E4  call        dword ptr [__imp__printf (0BE82CCh)]  
00BE16EA  add         esp,4  
00BE16ED  cmp         esi,esp  
00BE16EF  call        @ILT+320(__RTC_CheckEsp) (0BE1145h)  
    else
00BE16F4  jmp         main+7Dh (0BE170Dh)  
        printf("error\n");
00BE16F6  mov         esi,esp  
00BE16F8  push        offset string "error\n" (0BE5774h)  
00BE16FD  call        dword ptr [__imp__printf (0BE82CCh)]  
00BE1703  add         esp,4  
00BE1706  cmp         esi,esp  
00BE1708  call        @ILT+320(__RTC_CheckEsp) (0BE1145h)  
    return 0;
00BE170D  xor         eax,eax  
}
注意第22,23,24行,test  eax, eax  与运算的结果为1,je不跳转,就继续执行了hello world。

如此看来,是编译器在编译源代码的时候,做了一些我们不知道的工作。查一下文档,找到这么一段:

ANSI C 标准采用值保留(value preserving)原则,就是当把几个整型操作数混合使用时,其结果的类型可能是有符号数,也可能是无符号数,这取决于操作数的类型的相对大小。(通俗点说,就是两个整型数,如果都转换为signed不会丢失信息,就转换为signed;否则就转换为unsigned。)

在例一中,if(-1<sizeof(arr)/sizeof(arr[0])) 由于signed int 和unsigned int 都转换为 signed 时,unsigned 会丢失信息(unsigned正数表示范围是signed的2倍,当然会丢失信息了)。故根据ANSI C 标准,两者都转换为 unsigned 类型。而-1 转为无符号类型为:4294967296 。是的,你没看错,-1 在计算机内以补码形式存放,转换成十进制后,就变成了这个巨大的正数:4294967296。所以,if(4294967296<1) 永远为假,一直输出Error了。

在例二中,我们将-1先转换成了8个字节的double类型。然后再和4个字节的unsigned类型比较时,由于double完全容得下unsigned。所以,此时,操作数都转换为有符号的double类型。所以 if( (double)-1 < (double)1) 成立。此时,就输出 hello world了。

总结:尽量不要在你的代码中使用无符号类型,以免增加不必要的复杂性;或者使用时,在涉及混合运算时,进行强制类型转换,这样就不必由编译器来选择转换类型了。

------------------------------------------------------------------------------------------------------

补充一点:

1,ANSI C 标准中采用的是值保留原则。而K&R C 标准中,采用的是无符号保留(unsigned preserving)原则,即:不区分操作数大小,统一转换为无符号类型。如果采用K&R C 标准的话,那么例一、例二就都会输出Error了。

2,当前编译器基本都遵循ANSI C 标准。如:gcc默认GNU C。

即:GNU C = C90 + GNU Extensions = C90 + (some features in C99 + some features in C11)

你可能感兴趣的:(c,c,ansi,unsigned,signed,KR)