3.4 C语言基本数据类型
3.4.1 int类型
C语言提供了许多整数类型,因为C语言让程序员针对不同情况选择不同的类型。特别是,C语言中的整数类型可表示不同的取值范围和正负值。
int类型是有符号类型,即int类型的值必须是整数,可以是正整数、负整数或零。其取值范围依计算机系统而异。一般而言,存储一个int要占一个机器字长。ISO C规定int的取值范围最小为-32768~32767。一般而言,系统用一个特殊位的值表示有符号整数的正负号。
1.声明int变量
声明单个int变量:先写上int,然后写变量名,最后加上一个分号;
声明多个int变量:先写上int,然后列出多个变量名,变量名之间用逗号分隔。
如何给变量提供值呢?
第一种途径是赋值;
第二种途径是通过函数(如,scanf())获得值。
2. 初始化变量
初始化(initialize)变量就是为变量赋一个初始值。在C语言中,初始化可以直接在声明中完成。只需要在变量名后面加上赋值运算符(=)和待赋给变量的值即可。
int dogs, cats = 94; /*有效,但是这种格式很糟糕*/
最好不要把初始化的变量和未初始化的变量放在同一条声明中。
简而言之,声明为变量创建和标记存储空间,并未其指定初始值。
int sows; /*创建内存空间*/
int boars; /*创建内存空间并为其赋值*/
3.int类型变量
21、32都是整型常量或整型字面量。C语言把不含小数点和指数的数作为整数。C语言把大多数正常常量视为int类型,但是非常大的整数除外(例如long、long long)。
4.打印int值
%d都与待打印变量列表中相应的int值匹配。这个值可以是int类型的变量、int类型的常量或其他值为int类型的表达式。作为程序员,要确保转换说明的数量与待打印的数量相同,编译器不会捕获这类型的错误。
/* print1.c-displays some properties of printf() */
#include <stdio.h>
int main(void)
{
int ten = 10;
int two = 2;
printf("Doing it right: ");
printf("%d minus %d is %d\n", ten, 2, ten - two );
printf("Doing it wrong: ");
printf("%d minus %d is %d\n", ten ); // forgot 2 arguments
return 0;
}
/* 输出:
*/
第二行输出中,第1个%d对应ten的值,但是由于没有给后两个%d提供任何值,所以打印出的值是内存中的任意值。任意值跟内存中存储的数据和编译器管理内存的位置有关。
编译器不能捕获这种明显的错误,实际上问题处在printf()不寻常的设计。大部分函数都需要指定数目的参数,编译器都会检查参数的数目是否正确。但是,printf()函数的参数数目不定,编译器也爱莫能助。记住,使用printf()函数时,要确保转换说明的数量与待打印的数量相等。
5.八进制和十六进制
通常,C语言都假定整型常量是十进制。由于八进制和十六进制是2的幂,因此八进制和十六进制记数系统在表达与计算机相关的值时很方便。在C语言中,用特定的前缀表示使用哪种进制。0x或0X前缀表示十六进制值;0前缀表示八进制。
使用不同的进制数是为了方便,不会影响被存储的方式。
6.显示八进制和十六进制。
在C程序中,既可以使用也可以显示不同进制的数。不同的进制要使用不同的转换说明。
十进制显示数字:%d;
八进制显示数字:%o;
十六进制显示数字:%x;
另外,要显示各进制数的前缀0、0x和0X,必须使用%#o、%#x、%#X。
/* bases.c--prints 100 in decimal, octal, and hex */
#include <stdio.h>
int main(void)
{
int x = 100;
printf("dec = %d; octal = %o; hex = %x\n", x, x, x);
printf("dec = %d; octal = %#o; hex = %#x\n", x, x, x);
return 0;
}
/* 输出:
*/
3.4.2 其他整数类型
C语言提供了3个附属关键字修饰基本整数类型:short、long和unsigned。应记住以下几点:
*short int类型( 或者简写为short)占用的存储空间可能比int类型少,常用于较小数值的场合以节省空间。与int类似,short是有符号类型。
*long int或long占用的存储空间可能比int多,适用于较大数值的场合。与int类似,long是有符号类型。
*long long int或long long(C99标准加入)占用的存储空间可能比long多,适用于更大数值的场合。该类型至少占64位。与int类似,long long是有符号类型。
*unsigned int或unsigned只用于非负值的场合。这种类型与有符号类型表示的范围不同。用于表示正负号的位现在用于表示另一个二进制位,所以无符号整型可以表示更大的数。
*在C90标准中,添加了unsigned long int或unsigned long和unsigned short int或unsigned short类型。C99标准又添加了unsigned long long int或unsigned long long。
*在任何有符号类型前面添加关键字signed,可强调使用有符号类型的意图。例如,short等价于signed short。
1.声明其他整数类型。
其他整数类型的声明方式与int类型相同。
2.使用多种整数类型的原因
为什么说short类型“可能”比int类型占用的空间少,long类型“可能”比int类型占用的空间多?因为C语言只规定了short占用的存储空间不能多于int,long占用的存储空间不能少于int。这样规定是为了适应不同的机器。
现在,个人计算机最常见的设置是,long long占64位,long占32位,short占16位,int占16位或32位(依计算机的自然字长而定)。原则上,这4中类型代表4中不同的大小,但是在实际使用中,有些类型之间通常有重叠。
C标准对基本类型只规定了允许的最小大小。对于16位机,short和int的最小取值范围是[-32768, 32767];对于32位机,long的最小取值范围是[-2147483648, 2147483647]。对于unsigned short和unsigned int,最小取值范围是[0, 65535];对于unsigned long,最小取值范围是[0, 4294967295]。long long类型是为了支持64位的需求,最小取值范围是[-9223372036854775808, 9223372036854775807];unsigned long long的最小取值范围是[0, 18446744073709551615]。
应该如何选择int类型呢?
首先,考虑unsigned类型。这种类型的数常用于计数,因为计数不用负数。而且,unsigned类型可以表示更大的正数。
如果一个数超出了int类型的取值范围,且在long类型的取值范围时,使用long类型。然而,对于那些long占用的空间比int大的系统,使用long类型会减慢运算速度。因此,如非必要,请不要使用long类型。另外在long类型和int类型占用空间相同的机器上编写代码,当确实需要32位的整数时,应使用long类型而不是int类型,以便把程序移植到16位机后仍然可以正常工作。类似,如果确实需要64位的整数,应使用long long类型。
如果在int设置为32位的系统中要使用16位的值,应使用short类型以节省存储空间。通常,只有当程序使用相对于系统可用内存较大的整型数组时,才需要重点考虑节省空间的问题。使用short类型的另一个原因是,计算机中某些组件使用的硬件寄存器是16位。
3.long常量和long long常量
如果使用的数字超过了int类型能表示的范围,编译器会将其视为long int类型(假设这种类型可以表示该数字)。如果数字超出了long可表示的最大值,编译器则将其视为unsigned long类型。如果还不够大,编译器则将其视为long long或unsigned long long类型。(前提是编译器能识别这些类型)。
八进制和十六进制常量被视为int类型。如果值太大,编译器会尝试使用unsigned int。如果还不够大,编译器会依次使用long、unsigned long、long long和unsigned long long类型。
有些情况下,需要编译器以long类型存储一个小数字。另外,一些C标准也要求使用long类型的值。要把一个较小的常量作为long类型对待,可以在值的末尾加上l(小写的L)或L后缀。使用L后缀更好,因为l看上去和数字1很像。l和L后缀也可以用于八进制和十六进制整数。
类似地,在支持long long类型的系统中,也可以使用ll或LL后缀来表示long long类型的值。另外,u或U后缀表示unsigned long long,例如5ull,10LLU,10LLU或9Ull。
整数溢出
将有符号类型和无符号类型设置为比最大值大,看看会发生什么。
%u说明显示unsigned int类型的值。
/* toobig.c-exceeds maximum int size on our system */
#include <stdio.h>
int main(void)
{
int i = 2147483647;
unsigned int j = 4294967295;
printf("%d %d %d\n", i, i+1, i+2);
printf("%u %u %u\n", j, j+1, j+2);
return 0;
}
/* 输出:
*/
可以把无符号整数j看作是汽车的里程表。当达到它能表示的最大值时,会重新从起始点开始。整数i也是类似的情况。它们的主要区别是,在超过最大值时,unsigned int类型的变量j从0开始;而int类型的变量i则从-2147483648开始。注意,当i超出(溢出)其相应类型所能表示的最大值时,系统并未通知用户。
溢出行为是未定义的行为,C标准并未定义有符号类型的溢出规则。
4.打印short、long、long long和unsigned类型
打印unsigned int类型的值:%u;
打印long int类型的值:%ld;
如果系统中int和long的大小相同,使用%d就行。但是,这样的程序被移植到其他系统(int和long类型的大小不同)中无法正常运行。
在x或o前面可以使用l前缀,%lx表示以十六进制格式打印long类型整数,%lo表示以八进制格式打印long类型整数。注意,虽然C允许使用大写或小写的常量后缀,但是在转换说明中只能用小写。
对于short类型,可以使用h前缀。%hd表示以十进制显示short类型的整数;%ho表示以八进制显示short类型的整数。h和l前缀可以和u一起使用,用于表示无符号类型。对于支持long long类型的系统,%lld和%llu分别表示有符号和无符号类型。
/* print2.c-more printf() properties */
#include <stdio.h>
int main(void)
{
unsigned int un = 3000000000; /* system with 32-bit int */
short end = 200; /* and 16-bit short */
long big = 65537;
long long verybig = 12345678908642;
printf("un = %u and not %d\n", un, un);
printf("end = %hd and %d\n", end, end);
printf("big = %ld and not %hd\n", big, big);
printf("verybig= %lld and not %ld\n", verybig, verybig);
return 0;
}
/* 输出:
*/
该例子表明,使用错误的转换说明会得到意想不到的结果。在待打印的值大于有符号值的最大值时,使用%u和%d会产生不同的值。对于较小的整数,有符号和无符号类型的存储、显示都相同。
第2行输出,对于short类型的变量end,在printf()无论指定以short类型(%hd)还是int类型(%d)打印,打印出来的值都相同。
这是因为在给函数传递参数时,C编译器把short类型的值自动转换为int类型的值。为什么要进行转换?h修饰符有什么用?第1个问题的答案是,int类型被认为是计算机处理整数类型的最高效的类型。因此,在short和int类型的大小不同的计算机中,用int类型的参数传递速度更快。第2个问题的答案是,使用h修饰符可以显示较大整数被截断成short类型值的情况。第3行输出就演示了这种情况。使用%hd,printf()只会查看后16位。使用%ld,printf()只显示了存储在后32位的值。
程序员必须确保转换说明的数量和待打印值的数量相同,还必须根据待打印值的类型使用正确的转换说明。
3.4.3
使用字符:char类型
char类型用于存储字符,但从技术层面看,char是整数类型。因为char类型实际上存储的是整数而不是字符。计算机使用数字编码来处理字符,即用特定的整数表示特定的字符。美国最常用的编码是ASCII编码。许多IBM的大型主机使用另一种编码---EBCDIC。
标准ASCII码的范围时(0~127),只需7位二进制数即可表示。通常,char类型被定义为8位的存储单元。一般而言,C语言会保证char类型足够大,以存储系统(实现C语言的系统)的基本字符集。
商用的同一码(Unicode)创建了一个能表示世界范围内多种字符集的系统,目前包含的字符已超过110000个。国际标准化组织(ISO)和国际电工技术委员会(IEC)为字符集开发了ISO/IEC 10646标准。统一码标准也与ISO/IEC标准兼容。
C语言把1字节定义为char类型占用的位(bit)数,因此无论是16位还是32位系统,都可以使用char类型。
1.声明char类型变量
char类型变量的声明方式与其它类型变量的声明方式相同。
2.字符常量和初始化
在C语言中,用单引号括起来的单个字符被称为字符常量(character constant)。编译器一发现'A',就会将其转换成相应的代码值。单引号必不可少。
char broiled = 'T'; //正确,把'T'赋值给broiled
broiled = T; //错误!T是一个变量
broiled = "T"; //错误!"T"是一个字符串
如果省略单引号,编译器会认为T是一个变量名;如果把T用双引号括起来,编译器则认为"T"是一个字符串。
实际上,字符是以数值形式存储的,所以也可使用数字代码来赋值。
char grade = 65; /*对于ASCII,这样做没问题,但这是一种不好的编程风格*/
注意,能这样做的前提是系统使用ASCII码。其实,用'A'代替65才是较为妥当的做法,这样在任何系统中都不会出问题。因此,最好使用字符常量,而不是数字代码值。
奇怪的是,C语言将字符常量视为int类型而非char类型。
int为32位,char为8位的ASCII系统中,有下面的代码:
char grade = 'B';
本来'B'对应的数值66存储在32位的存储单位中,现在却可以存储在8位的存储单元。利用字符常量这种特性,可以定义字符常量'FATE',即把4个独立的8位ASCII码存储在一个32位存储单元中。如果把这样的字符常量赋给grade,只有最后8位有效。因此,grade的值是'E'。
3.非打印字符
单引号只适用于字符、数字和标点符号。浏览ASCII表会发现,有些ASCII字符打印不出来。例如,一些代表行为的字符(如退格)
C语言提供了3中方法表示这些字符。
第1种方法---使用ASCII码。
第2种方法---使用特殊的符号序列表示一些特殊的字符。这些符号序列叫作转义序列(escape sequence)。如表3.2所示。
把转义序列赋给字符变量时,必须用单引号把转移序列括起来。例如:
char nerf = '\n';
表3.2 转移序列
转移序列 含义
\a 警报(ANSI C)
\b 退格
\f 换页
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\\ 反斜杠(\)
\' 单引号
\" 双引号
\? 问号
\Ooo 八进制值(oo必须是有效的八进制数,即每个o可表示0~7中的一个数)
\xhh 十六进制值(hh必须是有效的十六进制数,即每个h可表示0~f中的一个数)
使用C90新增的警告字符(\a)是否能产生听到或看到的警告取决于计算机的硬件,蜂鸣是最常见的警告(在一些系统中,警报字符不起作用)。C标准规定警报字符不得改变活跃位置。标准中的活跃位置(active position)指的是显示设备(屏幕、电传打字机、打印机等)中下一个字符将出现的位置。简而言之,平时常说的屏幕光标位置就是活跃位置。在程序中把警报字符输出在屏幕上的效果是,发出一声蜂鸣,但不会移动屏幕光标。
换页符(\f)把活跃位置移至下一页的开始处;换行符(\n)把活跃位置移至下一行的开始处;回车符(\r)把活跃位置移动到当前行的开始处;水平制表符(\t)将活跃位置移至下一个水平制表点;垂直制表符(\v)把活跃位置移至下一个垂直制表点。
这些转移序列字符不一定在所有的显示设备上都其作用。
字符\、'、"用于定义字符常量,是printf()函数的一部分,若直接使用它们会造成混乱。
转义序列(\Ooo)和(\xhh)是ASCII码的特殊表示。如果要用八进制ASCII码表示一个字符,可以在编码值前面加一个反斜杠(\)并用单引号括起来。例如,如果编译器不识别警报字符(\a),可以使用ASCII码来代替:
beep = '\007'
可以省略前面的0,'\07'甚至'\7'都可以。即使没有前缀0,编译器在处理这种写法时,仍会解释为八进制。
从C90开始,C语言还提供了第3中选择---用十六进制表示字符常量,即反斜杠后面跟一个x或X,再加上1~3位十六进制数字。图3.5列出了一些整数类型的不同进制形式。
整型常量的例子
类型 十六进制 八进制 十进制
char \x41 \101 N.A
int 0x41 0101 65
unsigned int 0x41u 0101u 65u
long 0x41L 0101L 65L
unsigned long 0x41UL 0101UL 65UL
long long 0x41LL 0101LL 65LL
unsigned long long 0x41ULL 0101ULL 65ULL
图3.5 int系列类型的常量写法示例
使用ASCII码时,注意数字和数字字符的区别。
无论是普通字符还是转义字符,只要是双引号括起来的字符集合,就无需用单引号括起来。双引号中的字符集合叫作字符串。printf()函数中不是转移序列中的数字将作为普通字符被打印出来。
如何使用ASCII码?何时使用转义序列?如果存在转义序列和ASCII码,请选择转义序列,因为其写法更好记,可移植性也更高。
如果要使用ASCII码,为何要写成'\032'而不是032?首先,'\032'能更清晰地表达程序员使用字符编码的意图。其次,类似\032这样的转义序列可以嵌入C的字符串。
4.打印字符
printf()函数用%c指明待打印的字符。一个字符变量实际上被存储为1字节的整数值。
/* charcode.c-displays code number for a character */
#include <stdio.h>
int main(void)
{
char ch;
printf("Please enter a character.\n");
scanf("%c", &ch); /* user inputs character */
printf("The code for %c is %d.\n", ch, ch);
return 0;
}
/*输出:
*/
运行该程序,在输入字母后不要忘记按下Enter或Return键。注意,printf()函数中的转换说明决定了数据的显示方式,而不是数据的存储方式。
5.有符号还是无符号
有些C编译器把char类型实现为有符号类型,这意味着char可表示的范围是-128~127。而有些C编译器把char实现为无符号类型,那么char可表示的范围是0~255。你可以通过相应的编译器手册,确定正在使用的编译器如何实现char类型。或者,可以查阅limits.h头文件。
根据C90标准,C语言允许在关键字char前面使用signed或unsigned。这样,无论编译器默认char是什么类型,signed char表示有符号类型,而unsigned char表示无符号类型。这在用char类型处理小整数时很有用。如果只用char处理字符,那么char前面无需使用任何
修饰符。
3.4.4 _Bool类型
C99标准添加了_Bool类型,用于表示布尔值,即逻辑值true和false,因为C语言用值1表示true,值0表示false,所以_Bool类型实际上也是一种整数类型。但原则上它仅占用1位存储空间,因为对0和1而言,1位的存储空间足够了。
3.4.5 可移植类型:stdint.h和inttypes.h
C99新增了两个头文件stdint.h和inttypes.h,以确保C语言的类型在各系统中的功能相同。
C语言为现有类型创建了更多类型名。这些新的类型名定义在stdint.h头文件中。例如,int32表示32位的有符号整数类型。然后,使用int32_t类型编写程序,并包含stdint.h头文件时,编译器会把int或long替换成当前系统匹配的类型。
上面讨论的类型别名是精确宽度整数类型(exact-width integer type)的示例。但是,计算机的底层系统可能不支持。因此,精确宽度整数类型是可选项。
如果系统不支持精确宽度整数类型怎么办?C99和C11提供了第2类别名集合。一些类型名保证所表示的类型一定是至少有指定宽度的最小整数类型。这组类型集合被称为最小宽度类型(minumum width type)。例如,int_least8_t是可容纳8位有符号整数值的类型中宽度最小的类型的一个别名。
当然,一些程序员更关心速度而非空间。为此C99和C11定义了一组可使计算机达到最快的类型集合。这组类型集合被称为最快最小宽度类型(fastest minimum width type)。例如,int_fast8_t被定义为系统中对8位有符号值而言运算速度最快的整数类型的别名。
另外,有些程序员需要系统的最大整数类型。为此,C99定义了最大的有符号整数类型intmax_t,可存储任何有效的有符号整数值。
类似地,uintmax_t表示最大的无符号整数类型。顺带一提,这些类型有可能比long long和unsigned long long类型更大,因为C编译器除了实现标准规定的类型以外,还可利用C语言实现其他类型。
C99和C11不仅提供可移植的类型名,还提供相应的输入和输出。例如,printf()打印特定类型时要求与相应的转换说明匹配。如果要打印int32_t类型的值,有些定义使用%d,而有些定义使用%ld,怎么办?C标准针对这一情况,提供了字符串宏来显示可移植类型。例如,inttypes.h头文件中定义了PRID32字符串宏。
/* altnames.c -- portable names for integer types */
#include <stdio.h>
#include <inttypes.h> // supports portable types
int main(void)
{
int32_t me32; // me32 a 32-bit signed variable
me32 = 45933945;
printf("First, assume int32_t is int: ");
printf("me32 = %d\n", me32);
printf("Next, let's not make any assumptions.\n");
printf("Instead, use a \"macro\" from inttypes.h: ");
printf("me32 = %" PRId32 "\n", me32);
return 0;
}
/* 输出:
*/
printf( "me32 = %""d""\n", me32 );
在C语言中,可以把多个连续的字符串组合成一个字符串,所以这条语句又等价于:
printf( "me32 = %d\n", me32 );
注意,程序中使用了\"转移序列来显示双引号。
3.4.6 float、double和long double
面向金融和数学的程序经常使用浮点数。C语言中的浮点类型有float、double和long double类型。浮点数类型能表示包括小数在内更大范围的数。浮点数的表示类似于科学计数法(即用小数乘以10的幂来表示数字)。该计数系统常用于表示非常大或非常小的数。
表3.3列出了一些示例。
表3.3 记数法示例
数字 科学计数法 指数记数法
1000000000 1.0 * 10^9 1.0e9
123000 1.23 * 10^5 1.23e5
322.56 3.2256 * 10^2 3.2256e2
0.000056 5.6 * 10^-5 5.6e-5
e后面的数字代表10的指数。
C标准规定,float类型必须至少能表示6位有效数字,且取值范围至少是10^-37~10^+37。通常,系统存储一个浮点数要占用32位。其中8位用于表示指数的值和符号,剩下24位用于表示非指数部分(也叫作尾数或有效数)及其符号。
C语言提供的另一种浮点类型是double(意为双精度)。double类型和float类型的最小取值范围相同,但至少必须能表示10位有效数字。
一般情况下,double占用64位而不是32位。一些系统将多出的32位全部用来表示非指数部分,这不仅增加了有效数字的位数(即提高了精度),而且还减少了舍入误差。另一些系统把其中的一部分分配给指数部分,以容纳更大的指数,从而增加了可表示数的范围。无论那种方法,double类型的值至少有13位有效数字,超过了标准的最低位数规定。
C语言的第3中浮点类型是long double,以满足double类型更高的精度要求。不过,C只保证long double类型至少与double类型的精度相同。
1.声明浮点型变量
浮点型变量的声明和初始化方式与整型变量相同。
2.浮点型常量
浮点型常量的基本形式是:有符号的数字(包括小数点),后面紧跟e或E,最后是一个有符号数表示10的指数。
e后面的正号可以省略,可以没有小数点或指数部分,但是不能同时省略两者。可以省略小数部分或整数部分,但是不能同时省略两者。
不要在浮点型常量中间加空格。
默认情况下,编译器假定浮点型常量是double类型的精度。
float some;
some = 4.0 * 2.0;
使用双精度进行乘法运算,然后将乘积阶段成float类型的宽度。这样做虽然计算精度更高,但是会减慢程序的运行速度。
在浮点数后面加上f或F后缀可覆盖默认设置,编译器会将浮点型常量看作float类型。使用l或L后缀使得数字成为long double类型。
注意,建议使用L后缀,因为字母l和数字L很容易混淆。没有后缀的浮点型常量是double类型。
C99标准添加了一种新的浮点型常量格式---用十六进制表示浮点型常量,即在十六进制数前加上十六进制前缀(Ox或0X),用p和P分别代替e和E,用2的幂代替10的幂(即,p计数法)。如下所示:
0xa.1fp10
注意,并非所有的编译器都支持C99的之一特性。
3.打印浮点值
printf()函数使用%f转换说明打印十进制计数法的float和double类型浮点数,用%e打印指数记数法的浮点数。如果系统支持十六进制格式的浮点数,可用a和A分别代替e和E。打印long double类型要使用%Lf、%Le或%La转换说明。给那些未在函数原型中显式说明参数类型的函数(如,printf())传递参数时,C编译器会把float类型的值自动转换为double类型。
/* showf_pt.c -- displays float value in two ways */
#include <stdio.h>
int main(void)
{
float aboat = 32000.0;
double abet = 2.14e9;
long double dip = 5.32e-5;
printf("%f can be written %e\n", aboat, aboat);
// next line requires C99 or later compliance
printf("And it's %a in hexadecimal, powers of 2 notation\n", aboat);
printf("%f can be written %e\n", abet, abet);
printf("%Lf can be written %Le\n", dip, dip);
return 0;
}
/*输出:
*/
4.浮点值的上溢和下溢
当计算导致数字过大,超过当前类型能表达的范围时,就会发生上溢(overflow)。这种行为是未定义的,不过现在C语言规定,在这种情况下会给toobig赋一个表示无穷大的特定值,而且printf()显示该值为inf或infinity(或者具有无穷含义的其他内容)。
当对一个很小的数做除法时,情况更为复杂。float类型的数以指数和尾数部分来存储。存在这样一个数,它的指数部分是最小值,即全部可用位表示的最小尾数值。该数字是float类型能用全部精度表示的最小数字。现在把它除以2。通常,这个操作会减小指数部分,但是假设的情况中,指数已经是最小值了。所以计算机只好把尾数部分的位向右移,空出第1个二进制位,并丢弃最后一个二进制数。以10进制为例,把一个有4位有效数字的数(如,0.1234E-10)除以10,得到的结果是0.0123E-10。虽然得到了结果,但是在计算过程中却损失了原末尾有效位上的数字。这种情况叫作下溢(underflow)。C语言把损失了类型全精度的浮点数称为低于正常的(subnormal)浮点值。因此,把最小的正浮点数除以2将得到一个低于正常的值。如果除以一个非常大的值,会导致所有的位都为0。现在,C库已经提供了用于检查计算是否会产生低于正常值的函数。
还有另一个特殊的浮点值NaN(not a number的缩写)。例如,给asin()函数传递一个大于1的参数,该函数的行为将是未定义的。在这种情况下,该函数将返回NaN值,printf()函数可将其显示为nan、NaN或其他类似的内容。
浮点数舍入错误
给定一个数,加上1,再减去原来给定的数,结果是多少?你一定认为是1。但是,下面的浮点运算给出了不同的答案:
/* floaterr.c--demonstrates round-off error */
#include <stdio.h>
int main(void)
{
float a,b;
b = 2.0e20 + 1.0;
a = b - 2.0e20;
printf("%f \n", a);
return 0;
}
/* 输出:
*/
得出这些奇怪答案的原因是,计算机缺少足够的小数位来完成正确的运算。2.0e20是2后面有20个0。如果把该数加1,那么发生变化的是第21位。要正确计算,程序至少要存储21位数字。而float类型的数字通常只能存储按指数比例缩小或放大的6或7位数字。在这种情况下,计算结果一定是错误的。另一方面,如果把2.0e20改为2.0e4,计算结果就没问题。因为2.0e4加1只需改变第5位上的数字,float类型的精度足够进行这样的计算。
浮点数表示法
由于计算机使用的系统不同,一个程序有不同的输出。原因是,根据前面介绍的知识,实现浮点数表示法的方法有多种。为了尽可能统一实现,电子和电气工程师协会(IEEE)为浮点数计算和表示法开发了一套标准。现在,许多硬件浮点单元都采用该标准。2011年,该标准被ISO/IEC/IEEE 60559:2011标准收录。该标准作为C99和C11的可选项,符合硬件要求的平台可开启。支持C标准的编译器还包含捕获异常问题的工具。
3.4.7 复数和虚数类型
C99标准支持复数类型和虚数类型,但是有所保留。一般而言,虚数类型都是可选项。C11标准把整个复数软件包都作为可选项。
简而言之,C原因有3中复数类型:float _Complex、double _Complex和long double _Complex。例如,float _Complex类型的变量应包含两个float类型的值,分别表示复数的实部和虚部。类似地,C语言的3种虚数类型是float _Imaginary、double _Imaginary和long double _Imaginary。
如果包含complex.h头文件,便可用complex代替_Complex,用imaginary代替_Imaginary,还可用I代替-1的平方根。
标准使用首字母是下划线的标识符作为预留字可以减少名称冲突的几率。
3.4.8 其他类型
基本类型衍生物的其他类型,包括数组、指针、结构和联合。
在scanf()函数中用到的前缀&,便创建了一个指针,告诉scanf()把数据放在何处。
小结:基本数据类型
无符号整型:
单独的unsigned相当于unsigned int。
字符类型:
如果要表示基本字符集,也可以是16位或更大。
小结:如何声明简单变量
类型说明符由一个或多个关键字组成。
3.4.9 类型大小
打印当前系统的各类型的大小。
//* typesize.c -- prints out type sizes */
#include <stdio.h>
int main(void)
{
/* c99 provides a %zd specifier for sizes */
printf("Type int has a size of %zd bytes.\n", sizeof(int));
printf("Type char has a size of %zd bytes.\n", sizeof(char));
printf("Type long has a size of %zd bytes.\n", sizeof(long));
printf("Type long long has a size of %zd bytes.\n",
sizeof(long long));
printf("Type double has a size of %zd bytes.\n",
sizeof(double));
printf("Type long double has a size of %zd bytes.\n",
sizeof(long double));
return 0;
}
/* 输出:
*/
sizeof是C语言的内置运算符,以字节为单位给出指定类型的大小。C99和C11提供%zd转换说明匹配sizeof的返回类型(size_t类型)。一些不支持C99和C11的编译器可用%u或%lu代替%zd。
在limits.h和float.h头文件中有类型限定的相关信息。
注意在程序几行printf()语句都被分为两行,只要不在引号内部或一个单词中间断行,就可以这样写。