第7章 基本类型
请别搞错:计算机处理的是数而不是符号。我们用对行为的算术化程度来衡量我们的理解力(和控制力)。
到目前为止,本书只使用了C语言的两种基本(内置的)类型:int和float。(我们还见到过_Bool
,那是C99中的一种基本类型。)本章讲述其余的基本类型,并从总体上讨论了与类型有关的重要问题。7.1节展示整数类型的取值范围,包括长整型、短整型和无符号整型。7.2节介绍double类型和long double类型,这些类型提供了更大的取值范围和比float类型更高的精度。7.3节讨论char(字符)类型,这种类型将用于字符数据的处理。7.4节解决重要的类型转换问题,即把一种类型的值转换成另外一种类型的等价值。7.5节展示利用typedef定义新类型名的方法。最后,7.6节描述sizeof运算符,这种运算符用来计算一种类型需要的存储空间大小。
7.1 整数类型
C语言支持两种根本不同的数值类型:整数类型(也称整型)和浮点类型(也称浮点型)。整数类型的值是整数,而浮点类型的值则可能还有小数部分。整数类型又分为两大类:有符号型和无符号型。
有符号整数和无符号整数
有符号整数如果为正数或零,那么最左边的位(符号位)为0;如果是负数,则符号位为1。因此,最大的16位整数的二进制表示形式是0111111111111111,对应的值是32 767(即(2^{15}-1))。而最大的32位整数是01111111111111111111111111111111,对应的数值是2 147 483 647(即(2^{31}-1))。不带符号位的整数(最左边的位是数值的一部分)的整数称为无符号整数。最大的16位无符号整数是65 535(即(2^{16}-1)),而最大的32位无符号整数是4 294 967 295(即(2^{32}-1))。
默认情况下,C语言中的整数变量都是有符号的,也就是说最左位保留为符号位。若要告诉编译器变量没有符号位,需要把它声明成unsigned类型。无符号整数主要用于系统编程和底层与机器相关的应用。第20章将讨论无符号整数的常见应用,在此之前,我们通常回避无符号整数。
C语言的整数类型有不同的尺寸。int类型通常为32位,但在老得CPU上可能是16位。有些程序所需的数很大,无法以int类型存储,所以C语言还提供了长整型。某些时候,为了节省空间,我们会指示编译器以比正常存储小的空间来存储一些数,称这样的数为短整型。
为了使构造的整数类型正好满足要求,可以指明变量是long类型或short类型,singed类型或unsigned类型,甚至可以把说明符组合起来(如long unsigned int)。然而,实际上只有下列6种组合可以产生不同的类型:
short int
unsigned short int
int
unsigned int
long int
unsigned long int
其他组合都是上述某种类型的同义词。(例如,除非额外说明,否则所有整数都是有符号的。因此,long signed int和long int是一样的类型。)另外,说明符的顺序没什么影响,所以unsigned short int和short unsigned int是一样的。
C语言允许通过省略单词int来缩写整数类型的名字。例如,unsigned short int可以缩写为unsigned short,而long int则可以缩写为long。C程序员经常会省略int;一些新出现的基于C的语言(包括Java)甚至不允许程序员使用short int或long int这样的名字,而必须写成short或long。基于这些原因,本书在单词int可有可无的情况下通常将其省略。
6种整数类型的每一种所表示的取值范围都会根据机器的不同而不同,但是有两条所有编译器都必须遵守的原则。首先,C标准要求short int、int和long int中的每一种类型都要覆盖一个确定的最小取值范围(详见23.2节)。其次,标准要求int类型不能比short int类型短,long int类型不能比int类型短。但是,short int类型的取值范围有可能和int类型的范围是一样的,int类型的取值范围也可以和long int的一样。
表7-1说明了在16位机上整数类型通常的取值范围,注意short int和int有相同的取值范围。
表7-1 16位机的整数类型
类型 | 最小值 | 最大值 |
---|---|---|
short int | -32 768 | 32 767 |
unsigned short int | 0 | 65 535 |
int | -32 768 | 32 767 |
unsigned int | 0 | 65 535 |
long int | -2 147 483 648 | 2 147 483 647 |
unsigned long int | 0 | 4 294 967 295 |
表7-2说明了32位机上整数类型通常的取值范围,这里的int和long int有着相同的取值范围。
表7-2 32位机的整数类型
类型 | 最小值 | 最大值 |
---|---|---|
short int | -32 768 | 32 767 |
unsigned short int | 0 | 65 535 |
int | -2 147 483 648 | 2 147 483 648 |
unsigned int | 0 | 4 294 967 295 |
long int | -2 147 483 648 | 2 147 483 647 |
unsigned long int | 0 | 4 294 967 295 |
最近64位的CPU逐渐流行起来了。表7-3给出了64位机上(尤其是在UNIX系统下)整数类型常见的取值范围。
类型 | 最小值 | 最大值 |
---|---|---|
short int | -32 768 | 32 767 |
unsigned short int | 0 | 65 535 |
int | -2 147 483 648 | 2 147 483 648 |
unsigned int | 0 | 4 294 967 295 |
long int | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 808 |
unsigned long int | 0 | 18 446 744 073 709 551 615 |
再强调一下,表7-1、表7-2和表7-3中给出的取值范围不是C标准强制的,会随着编译器的不同而不同。对于特定的实现,确定整数类型范围的一种方法是检查<limits.h>头(23.2节)。该头是标准库的一部分,其中定义了表示每种整数类型的最大值和最小值的宏。
7.1.1 C99中的整数类型
C99提供了两个额外的标准整数类型:long long int和unsigned long long int。增加这两种整数类型有两个原因,一是为了满足日益增长的对超大型整数的需求,而是为了适应支持64位运算的新处理器的能力。这两个long long类型要求至少64位宽,所以long long int类型值得范围通常为(-2^{63})(-9 223 372 036 854 775 808)到(2^{63}-1)(9 223 372 036 854 775 807),而unsigned long long int类型值得范围通常为0到(2^{64}-1)(18 446 744 073 709 551 615)。
C99中把short int、int、long int和long long int类型(以及signed char类型(7.3节))称为标准有符号整型,而把unsigned short int、unsigned int、unsigned long int和unsigned long long int类型(以及unsigned char类型(7.3节)和_Bool
类型(5.2节))称为标准无符号整型。
除了标准的整数类型以外,C99标准还允许在具体实现时定义扩展的数类整型(包括有符号和无符号的)。例如,编译器可以提供有符号和无符号的128位整数类型。
7.1.2 整数常量
现在把注意力转向常量——在程序中以文本形式出现的数,而不是读、写或计算出来的数。C语言允许用十进制(基数为10)、八进制(基数为8)和十六进制(基数为16)形式书写整数常量。
八进制数和十六进制数
八进制数是用数字0~7书写的。八进制数的每一位表示一个8的幂(这就如同十进制数的每一位表示10的幂一样)。因此,八进制的数237表示成十进制数就是(2*8^2+3*8^1+7*8^0=128+24+7=159)。
十六进制数是用数字0~9加上字母A ~ F表示10~15的数。十六进制数的每一位表示一个16的幂,十六进制数1AF的十进制数值是(1*16^2+10*16^1+15*16^0=256+160+15=431)。
- 十进制常量包含0~9中的数字,但是一定不能以零开头:15 255 32767
- 八进制常量只包含0~7中的数字,而且必须要以零开头:017 0377 077777
- 十六进制常量包含0~9中的数字和a ~f中的字母,而且总是以0x开头:0xf 0xff 0x7fff
请记住八进制和十六进制只是书写数的方式,它们不会对数的实际存储方式产生影响。(整数都是以二进制形式存储的,跟表示方式无关。)任何时候都可以从一种书写方式切换到另一种书写方式,甚至可以混合使用:10+015+0x20的值为55(十进制)。八进制和十六进制更适用于底层程序的编写,本书直到第20章才会较多地用到它们。
十进制整数常量的类型通常为int,但如果常量的值大得无法存储在int型中,就用long int 类型。如果出现long int 不够用的罕见情况,编译器会用unsigned long int 作最后的尝试。确定八进制和十六进制常量的规则略有不同:编译器会依次尝试int、unsigned int、long int和unsigned long int类型,直至找到能表示该常量的类型。
为了强制编译器把常量作为长整数来处理,只需在后边加上一个字母L(或l):
15L 0377L 0x7fffL
为了指明是无符号常量,可以在常量后边加上字母U(或u):
15U 0377U 0x7fffU
L和U可以结合使用,以表明常量既是长整型又是无符号的:0xffffffffUL
。(字母L、U的顺序和大小写无所谓。)
7.1.3 C99中的常数常量
在C99中,以LL或ll(两个字母大小写要一致)结尾的整数常量是long long int型的。如果在LL或ll的前面或后面增加字母U(或u),则该整数常量为unsigned long long int型。
C99确定整数常量类型的规则与C89有些不同。对于没有后缀(U、u、L、l、LL、ll)的十进制常量,其类型是int、long int或long long int中能表示该值的“最小”类型。对于八进制或者十六进制常量,可能的类型顺序为int、unsigned int、long int、unsigned long int、long long int和unsigned long long int。常量后面的任何后缀都会改变可能类型的列表。例如,以U(或u)结尾的常量类型一定是unsigned int、unsigned long int 和unsigned long long int中的一种,以L(或l)结尾的十进制常量类型一定是long int或long long int中的一种。如果常量的数值过大以至于不能用标准的整数类型表示,则可以使用扩展的整数类型。
7.1.4 整数溢出
对整数执行算术运算时,其结果有可能因为太大而无法表示。例如,对两个int值进行算术运算时,结果必须仍然能用int类型来表示;否则(表示结果所需的数位太多)就会发生溢出。
整数溢出时的行为要根据操作数是有符号型还是无符号型来确定。有符号整数运算中发生溢出时,程序的行为是未定义的。回顾4.4节的介绍可知,未定义行为的结果是不确定的。最可能的情况是,仅仅是运算的结果出错了,但程序也有可能崩溃,或出现其他意想不到的状况。
无符号整数运算过程中发生溢出时,结果是有定义的:正确答案对(2^n)取模,其中n是用于存储结果的位数。例如,如果对无符号的16位数65 535加1,其结果可以保证为0。
7.1.5 读/写整数
假设有一个程序因为其中一个int变量发生了“溢出”而无法工作。我们的第一反应是把变量的类型从int变为long int。但仅仅这样做事不够的,我们还必须检查数据类型的改变对程序其他部分的影响,尤其是需要检查该变量是否用在printf函数或scanf函数的调用中。如果用了,需要改变调用中的格式串,因为%d只适用于int类型。
读写无符号整数、短整数和长整数需要一些新的转换说明符。
- 读写无符号整数时,使用字母u、o或x代替转换说明中的d。如果使用u说明符,该数将以十进制形式读写,o表示八进制形式,而x表示十六进制形式。
unsigned int u;
scanf("%u", &u);/* reads u in base 10 */
printf("%u", u);/* writes u in base 10 */
scanf("%o", &u);/* reads u in base 8 */
printf("%o", u);/* writes u in base 8 */
scanf("%x", &u);/* reads u in base 16 */
printf("%x", u);/* writes u in base 16 */
- 读写短整数时,在d、o、u前或x前面加上字母l;
- 读写长长整数时(仅限C99),在d、o、u或x前面加上字母ll;
程序:数列求和(改进版)
6.1节编写了一个程序对用户输入的整数数列求和。该程序的一个问题就是所求出的和(或其中某个输入数)可能会超出int型变量允许的最大值。如果程序运行在整数长度为16位的机器上,可能会发生下面的情况:
This program sums a series of integers.
Enter integers (0 to terminate):10000 20000 30000 0
The sum is: -5536
求和的结果应该为60 000,但这个值不在int型变量表示的范围内,所以出现了溢出。当有符号整数发生溢出时,结果是未定义是未定义的,在本例中我们得到了一个毫无意义的结果。为了改进这个程序,可以把变量改成long型。
/**
* Sums a series of numbers (using long int variables)
*/
#include <stdio.h>
int main() {
long n, sum = 0;
printf("This program sums a series of integers.
");
printf("Enter integers (0 to terminate): ");
scanf("%ld", &n);
while (n != 0) {
sum += n;
printf("The sum is: %ld
", sum);
scanf("%ld", &n);
if (n < 0) {
printf("Length Overflow!!!");
break;
}
}
return 0;
}
这种改变非常简单:将n和sum声明为long型变量而不是int型变量,然后把scanf和printf函数中的转换说明由%d改为%ld。
7.2 浮点类型
整数类型并不适用于所有应用。有些时候需要变量能存储带小数点的数,或者能存储极大数或极小数。这类数可以用浮点(因小数点是“浮动的”而得名)格式进行存储。C语言提供了3种浮点类型,对应三种不同的浮点格式。
- float:单精度浮点数。
- double:双精度浮点数。
- long double:扩展精度浮点数。
当精度要求不严格时,(例如,计算带一位小数的温度),float类型是很适合的类型。double提供更高的进度,对绝大多数程序来说都够用了。long double支持极高精度的要求,很少会用到。
C标准没有说明float、double和long double类型提供的精度到底是多少,因为不同的计算机可以用不同方法存储浮点数。大多数现代计算机都遵循IEEE 754标准(即IEC 60559)的规范,所以这里也用它作为一个示例。
IEEE浮点标准
由IEEE开发的IEEE标准提供了两种主要的浮点数格式:单精度(32位)和双精度(64位)。数值以科学计数法的形式存储,每一个数都由三部分组成:符号、指数和小数。指数部分的位数说明了数值的可能大小程度,而小数部分的位数说明了精度。单精度格式中,指数长度为8位,而小数部分占了23位。因此,单精度数可以表示的最大值大约是(3.40*10^{38}),其中精度是6个十进制数字。
IEEE标准还描述了另外两种格式:单扩展精度和双扩展精度。标准没有指明这些格式中的位数,但要求单扩展精度类型至少为43位,而双精度类型至少为79位。为了获得更多有关IEEE标准和浮点算术的信息,可以参阅David Goldberg在1991年3月发布的“What every computer scientist should know about floating-point arithmetic”(ACM Computing Surveys, vol, 23, no.1: 5-48 )。
表7-4给出了根据IEEE标准实现时浮点类型的特征。(表中给出了规范化的最小正值,非规范化的数(23.4节)可以更小。)long double 类型没有显示在此表中,因为它的长度随着机器的不同而变化,而最常见的大小是80位和128位。
类型 | 最小正值 | 最大值 | 精度 |
---|---|---|---|
float | (1.17549*10^{-38}) | (3.40282*10^{38}) | 6个数字 |
double | (2.225 07*10^{-308}) | (1.79769*10^{308}) | 15个数字 |
在不遵循IEEE标准的计算机上,表7-4是无效的。事实上,在一些机器上,float可以有和double相同的数值集合,或者double可以有和long double相同的数值集合。可以在头<float.h>
(23.1节)中找到定义浮点类型特征的宏。
在C99中,浮点类型分为两种:一种是实浮点类型,包括float、double和long double类型;另一种是C99新增的复数类型(27.3节,包括float _Complex、double _Complex和long double _Complex)。