尽管C语言标准没有指定有符号数要采用某种表示,但是几乎所有的机器都采用补码。大多数C 语言的实现而言,类型转换意味着“把这些二进制位看作另一种类型,并作相应的对待”,即位模式不变(无溢出发生时)。《CSAPP》
1. 二进制码的含义
计算机内存中的数据以二进制的方式存储,一段二进制序列可能表示整型数据、浮点型数据及指令,这段二进制的具体含义由访问它的指令决定。在没有数据类型转换的前提下,访问二进制数据的指令由编译器产生(编译器根据定义二进制段的类型决定产生何种访问指令)。如用整型(int)声明一段二进制序列(内存),单纯的访问此段二进制序列时编译器就会产生整型指令去访问这段内存,以得到这段二进制所表示的整数值。
例:
#include <stdio.h>
int main(void)
{
union _test{
float f;
int i;
}test;
//printf("%d\t%d\n", sizeof(float), sizeof(int));
test.f = 3.14;
printf("test.f= %f\ttest.i = %x\ttest.f=%f\n", test.f, test.i, test.f);
return 0;
}
在Debian GNU/Linux终端下验证sizeof(float) = sizeof(int) = 4。当给test.f赋值3.14后,3.14将以补码形式存于内存中。由C联合的特点知,test.f与test.i共用一段内存,当访问整型类型的test.i时编译器产生整型指令去操作这段内存,将原本表示3.14的二进制序列当成表示整型的二进制访问,得到对应的整型值并将其输出。编译并执行得到结果:
lly7@debian:~/C_Proble$ gcc data_transform.c -o data_transform lly7@debian:~/C_Proble$ ./data_transform test.f=3.140000 test.i = 4048f5c3 test.f=3.140000 |
2. 转换符
转换符会改变内存中的数据。
例:
#include <stdio.h>
int main(void)
{
union _test{
float f;
int i;
}test;
test.f = 3.14;
printf("test.f= %f\ttest.i = %x\ttest.f=%d\ttest.i=%x\n", test.f, test.i, test.f, test.i);
return 0;
}
编译并执行得到结果:
lly7@debian:~/C_Proble$ gcc data_transform.c -o data_transform lly7@debian:~/C_Proble$ ./data_transform test.f=3.140000 test.i = 4048f5c3 test.f=1610612736 test.i=40091eb8 |
3. 隐式类型转换
C语言中,在进行运算时, 不同类型的数据要先转换成同一类型,然后进行运算。以下是转换规则:
Figure1:数据类型转换
图中横向的左箭头表示必定的转换,如字符(char)参加运算时,不论另一个操作数是什么类型,必定先转换为整型(int);单精度(float)型数据在运算时一律先转换成双精度(double),以提高运算精度(即使两个float型数据相加,也要先转换成double型再相加)。图中纵向的上箭头表示当运算对象为不同类型时的转换方向。
(1) 混合运算时,同类型的有符号数向无符号数转换
例:#include <stdio.h>
int main(void)
{
unsigned char uch, sum;
char ch;
uch = 0x01;
ch = -128;
sum = uch + ch;
printf("uch = %d\tch=%d\tsum = %d\tch = %d\n", uch, ch, sum, ch);
return 0;
}
编译并执行得到结果:
lly7@debian:~/C_Proble$ gccdata_transform.c -o data_transform lly7@debian:~/C_Proble$ ./data_transform uch= 1 ch=-128 sum = 129 ch = -128 |
(2) 要避免无符号数据变量的最高位扩展为有符号数据变量的符号位
例:#include <stdio.h>
int main(void)
{
unsigned char uch;
char ch;
uch = 0x80;
ch = uch;
printf("uch = %d\tch=%d\n", uch, ch);
return 0;
}
编译并执行得到结果:
lly7@debian:~/C_Proble$ gccdata_transform.c -o data_transform lly7@debian:~/C_Proble$ ./data_transform uch= 128 ch=-128 |
将uch值(0x80)截取给ch变量,0x80以正数的补码(1000000,为-0的补码,被规定为-128的补码)形式存在ch代表的内存中,故而ch输出-128。而uch所有位都为数据为,故而输出128。如果ch是想要赋值uch的值,则ch应该被声明为无符号字符类型。[2014.6.13-21:22]
-------补充,摘抄《Linux_C编程一站式学习 》-----------
x86/linux/gcc
(1) Integer Promotion
- 如果一个函数的形参类型未知,或者函数的参数列表中有…,那么调用函数时要对相应的实参做Integer Promotion。
- 算术运算中的类型转换。有符号或无符号的char型、short型和Bit-field在做算术运算之前首先要做Integer Promotion,然后才能参与计算。
(2) Usual Arithmetic Conversion
P.242:两个算术类型的操作数做算术运算,比如a + b(+ - * / ==等运算符要求两个操作数的类型一致,而移位运算符不需要两个操作数的类型一致,但两边操作数都要做Integer Promotion),如果两边操作数的类型不同,编译器会自动做类型转换,使两边类型相同之后才做运算,这称为Usual Arithmetic Conversion。转换规则如下:
1. 如果有一边的类型是long double,则把另一边也转成long double。
2. 否则,如果有一边的类型是double,则把另一边也转成double。
3. 否则,如果有一边的类型是float,则把另一边也转成float。
4. 否则,两边应该都是整型,首先按上一小节讲过的规则对a和b做Integer Promotion,然后如果类型仍不相同,则需要继续转换。首先我们规定char、short、int、long、long long的转换级别(Integer Conversion Rank)一个比一个高,同一类型的有符号和无符号数具有相同的Rank。转换规则如下:
a. 如果两边都是有符号数,或者都是无符号数,那么较低Rank的类型转换成较高Rank的类型。例如unsigned int和unsigne dlong做算术运算时都转成unsigned long。
b. 否则,如果一边是无符号数另一边是有符号数,无符号数的Rank不低于有符号数的Rank,则把有符号数转成另一边的无符号类型。例如unsigned long和int做算术运算时都转成unsigned long,unsigned long和long做算术运算时也都转成unsigned long。
c. 剩下的情况是:一边有符号另一边无符号,并且无符号数的Rank低于有符号数的Rank。这时又分为两种情况,如果这个有符号数类型能够覆盖这个无符号数类型的取值范围,则把无符号数转成另一边的有符号类型。例如遵循LP64的平台上unsigned int和long在做算术运算时都转成long。
d. 否则,也就是这个有符号数类型不足以覆盖这个无符号数类型的取值范围,则把两边都转成有符号数的Rank对应的无符号类型。例如在遵循ILP32的平台上unsigned int和long在做算术运算时都转成unsigned long。
P.255:运算符+ - * / % > < >= <= == != & | ^ 以及各种复合赋值运算符要求两边的操作数类型一致,条件运算符?:要求后两个操作数类型一致,这些运算符在计算之前都需要做Usual Arithmetic Conversion。
(3) 由赋值产生的类型转换(P.242)
如果赋值或初始化时等号两边的类型不相同,则编译器会把等号右边的类型转换成等号左边的类型再做赋值。
&、|、^运算符都是要做Usual Arithmetic Conversion的(其中有一步是Integer Promotion),~运算符也要做Integer Promotion,所以在C语言中其实并不存在8位整数的位运算,操作数在做位运算之前都至少被提升为int型了。P.246
-------补充,摘抄《Linux_C编程一站式学习 》-----------
4. 强制类型转换
(1) 一般的强制类型转换
例:
#include <stdio.h>
int main(void)
{
unsigned char uch;
char ch;
ch = -128;
uch = (unsigned char)ch;
printf("uch = %d\tch=%d\n", uch, ch);
return 0;
}
编译并执行得到结果:
lly7@debian:~/C_Proble$ gccdata_transform.c -o data_transform lly7@debian:~/C_Proble$ ./data_transform uch= 128 ch=-128 |
强制类型转换不改变原内存之上的数据。
(2) 指针的强制类型转换
例:#include <stdio.h>
int main(void)
{
int i = 0x101, *pI = NULL;
char ch = 2, *pCH = NULL;
pI = &i;
pCH = (char *)pI;
printf("*pI = %d\t*pCH=%d\n", *pI, *pCH);
return 0;
}
编译并执行得到结果:
lly7@debian:~/C_Proble$ gccdata_transform.c -o data_transform lly7@debian:~/C_Proble$ ./data_transform *pI= 257 *pCH=1 |
pI的4字节内存保存的是整型变量i的地址,pCH = (char*)pI:不管char *还是int *类型都是占用4字节,所以pCH的值跟pI拥有同一个地址值。只是此时地址的含义不再一样,pI表示一个整型(i)的地址,*pI访问sizeof(int)字节内容;pCH表示一个字符型(char)的地址,*pCH将访问sizeof(char)字节内容。
(3) 二级指针的强制类型转换
例:#include <stdio.h>
int main(void)
{
int i = 0x101, *pI = NULL, **ppI = NULL;
char *pCH = NULL, **ppCH = NULL;
pI = &i;
ppI = &pI;
printf("pI = %p\tppI=%p\n", pI, ppI);
pCH = (char *)pI;
ppCH = &pCH;
printf("pCH = %p\tppCH = %p\tppCH=%d\n", &pCH, ppCH, **ppCH);
ppCH = (char **)ppI;
printf("ppI=%p\tppCH = %p\tppCH=%d\n", ppI, ppCH, **ppCH);
return 0;
}
编译并执行得到结果:
lly7@debian:~/C_Proble$ gccdata_transform.c -o data_transform lly7@debian:~/C_Proble$ ./data_transform pI = 0xbfdcb3f8 ppI=0xbfdcb3f4 pCH = 0xbfdcb3f0 ppCH = 0xbfdcb3f0 ppCH=1 ppI=0xbfdcb3f4 ppCH = 0xbfdcb3f4 ppCH=1 |
结合代码和结果可看出,各指针原内容未发生变化,只是将指针的含义改变,跟指针的强制类型转换一样。
(4) 含const的强制类型转换
C语言中的const限定词的真正含义是“只读的”。在Debian GNU/Linux下,可以向接受const-T变量的地方传入T类型的变量。你可以向接受const-T 的指针的地方传入T 的指针(任何类型T 都适用)。但是, 这个允许在带修饰的指针类型上轻微不匹配的规则(明显的例外) 却不能递归应用, 而只能用于最上层。如果你必须赋值或传递除了在最上层还有修饰符不匹配的指针, 你必须明确使用类型转换(本例中, 使用(const char **)), 不过, 通常需要使用这样的转换意味着还有转换所不能修复的深层次问题。
#include <stdio.h>
void func(const char **ppAr);
int main(void)
{
char Ar[] = "lly7";
char *pAr = NULL, **ppAr = NULL;
pAr = Ar;
ppAr = &pAr;
func((const char **)ppAr);
Ar[0] = 'L';
printf("%s\n", Ar);
return 0;
}
void func(const char **ppAr)
{
printf("%s\n", *ppAr);
}
编译并执行得到结果:
lly7@debian:~/C_Proble$ gccdata_transform.c -o data_transform lly7@debian:~/C_Proble$./data_transform lly7 Lly7 |
如果在给func传递实参时没有用强制类型转换,则Debian GNU/Linux编译器会给出警告。一切正常。并没有像想象中那样将二级指针所指向的内容转回为只读内容。但不知《C常见问题集》提到的不能修复的问题指的是什么。
5. 编译器如何处理类型转换
《Linux_C编程一站式学习 P.243》
把M位的类型(值为X)转换为一个N位的类型(Implementation-defined表示编译器根据情况自行设定规则):
待转换的类型 | M > N | M == N | M < N |
signed integer to signed integer | 如果X在目标类型的取值范围内则取值不变,否则Implementation-defined | 值不变 | 值不变 |
unsigned integer to signed integer | 如果X在目标类型的取值范围内则取值不变,否则Implementation-defined | 如果X在目标类型的取值范围内则取值不变,否则Implementation-defined | 值不变 |
signed integer to unsigned integer | X % 2N | X % 2N | X % 2N |
unsigned integer to unsigned integer | X % 2N | 值不变 | 值不变 |
floating-point to signed or unsigned integer
| Truncate toward Zero,如果X的整数部分超出目标类型的取值范围则 Undefined | Truncate toward Zero,如果X的整数部分超出目标类型的取值范围则Undefined | Truncate toward Zero,如果X的整数部分超出目标类型的取值范围则Undefined |
signed or unsigned integer to floating-point
| 如果X在目标类型的取值范围内则值不变,但有可能损失精度,如果X超出目标类型的取值范围则Undefined | 如果X在目标类型的取值范围内则值不变,但有可能损失精度,如果X超出目标类型的取值范围则Undefined | 如果X在目标类型的取值范围内则值不变,但有可能损失精度,如果X超出目标类型的取值范围则Undefined |
floating-point to floating-point
| 如果X在目标类型的取值范围内则值不变,但有可能损失精度,如果X超出目标类型的取值范围则Undefined | 值不变 | 值不变 |
注意上表中的“X % 2N”,我想表达的意思是“把X加上或者减去2N的整数倍,使结果落入[0, 2N-1]的范围内”,当X是负数时运算结果也得是正数,即运算结果和除数同号而不是和被除数同号,这不同于C语言%运算的定义。写程序时不要故意用上表中的规则,尤其不要触碰Implementation-defined和Undefined的情况,但程序出错时可以借助上表分析错误原因。
C Note Over.