从变量的类型转换看C语言的思维模式
怪异的语法
C语言的类型转换语法对大多数人是非常奇怪的,尤其是对于接触的第一门语言不是C语言的人来说。在大多数“正常”的语言中,类型转换的语法应该是这样的(这里以Python为例):
a = 1
b = float(a)
但C语言中的类型转换是这个样子的:
unsigned int b = 0x1812;
unsigned char a = (unsigned char)b;
不同的语言模式
造成这种差异的原因并不是因为语法设计的不同。而是语言设计的思路不一样。Python是面向对象的语言,int和float都是内置的类,表达式 float(a)
实际上是将int类型的a作为参数,生成一个float类的实例,这种类型转换实际上是生成了一个新的对象。
而在C语言中,是没有对象的,而且不同的类型之间也似乎没有什么明确的界限,一个变量不过是一个字节序列。C语言中,有符号整型int和无符号整型unsigned都是4个字节大小。字节序列0xffffffff,如果看成int类型的话,是-1,而看成unsigned类型则是4294967295。C语言中的printf函数可以明显地看出这一特点,printf函数并不检查参数的类型,而是根据描述符说明的类型去解释变量,如果描述的类型和实际的类型不匹配,很可能出现一些奇怪的现象。
例如如下代码
#include <stdio.h>
int main()
{
int d = -1;
printf("%d\n", d);
printf("%u\n", d);
return 0;
}
输出结果为:
-1
4294967295
变量d本来是int类型的,字节序列为0xffffffff,用%u描述符输出时,C编译器直接将0xffffffff作为无符号整型解释,得到4294967295. 因此我们可以理解到以下的原理:
C语言类型转换的原理
C语言的强制类型转换中,变量的字节序列不变,只是字节的解释方式发生了改变。
为了进一步体会这种特性,我们将程序编译后,观察编译器生成的汇编代码,进一步进行验证。
#include <stdio.h>
int main()
{
int a = -4;
unsigned b = (unsigned)a;
unsigned char c = (unsigned char)b;
printf("%d %u\n", a, b);
printf("%x %x\n", a, c);
return 0;
}
输出结果如下:
-4 4294967292
fffffffc fc
对.o文件的反汇编结果如下:
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 e4 f0 and $0xfffffff0,%esp
6: 83 ec 20 sub $0x20,%esp
9: c7 44 24 1c fc ff ff movl $0xfffffffc,0x1c(%esp)
10: ff
11: 8b 44 24 1c mov 0x1c(%esp),%eax
15: 89 44 24 18 mov %eax,0x18(%esp)
19: 8b 44 24 18 mov 0x18(%esp),%eax
1d: 88 44 24 17 mov %al,0x17(%esp)
21: 8b 44 24 18 mov 0x18(%esp),%eax
25: 89 44 24 08 mov %eax,0x8(%esp)
29: 8b 44 24 1c mov 0x1c(%esp),%eax
2d: 89 44 24 04 mov %eax,0x4(%esp)
31: c7 04 24 00 00 00 00 movl $0x0,(%esp)
38: e8 fc ff ff ff call 39 <main+0x39>
3d: 0f b6 44 24 17 movzbl 0x17(%esp),%eax
42: 89 44 24 08 mov %eax,0x8(%esp)
46: 8b 44 24 1c mov 0x1c(%esp),%eax
4a: 89 44 24 04 mov %eax,0x4(%esp)
4e: c7 04 24 07 00 00 00 movl $0x7,(%esp)
55: e8 fc ff ff ff call 56 <main+0x56>
5a: b8 00 00 00 00 mov $0x0,%eax
5f: c9 leave
60: c3 ret
从汇编代码中可以看出,将unsigned类型的变量赋值给unsigned char类型,不过是将unsigned类型变量的最低字节取出;将int类型转换为unsigned类型,不过是原封不动的mov.
总结
C语言类型转换的语法设计成在变量前加带括号的类型名,这样的语法正体现了类型转换在C语言中实际发生的情况:用新的类型来“修饰”原先的变量,将变量按照新的类型来解释,并没有产生一个新的变量。
附:所用软件版本
gcc: gcc (Debian 4.7.2-5) 4.7.2
objdump: GNU objdump (GNU Binutils for Debian) 2.22