前不久和同事谈论起 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; }
下面我们看一下它的反汇编:(不想看的童鞋可以跳过)
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)