C-Primer-Plus-第三章-数据和C

第三章 数据和C

参考书籍:C Primer Plus(第6版)中文版

一、前言

打算跟这本书系统地学习一遍C语言,谨以此系列文章记录自己的学习之路。本文所有图片均来源于此书。

二、主要内容

1. 示例程序

/* platinum.c --your weight in platinum */
#include <stdio.h>
int main(void) // C Primer Plus program3.1
{
    float weight;
    float value;  // the value of platinum in same weight

    printf("Are you worth your weight in platinum?\n");
    printf("Let's check it out.\n");
    printf("Please enter your weight in pounds: ");

    scanf("%f", &weight); // get user input

    value = 1700.0 * weight * 14.5833;
    printf("Your weight in platinum is worth $%.2f.\n", value);
    printf("You are easily worth that! If platinum prices drop,\n");
    printf("eat more to maintain your value.\n");
    
    getchar();
    getchar();
    return 0;
}

错误与警告:

如果输入程序时打错(如,漏了一个分号),编译器会报告语法错误消息。然而,即使输入正确无误,编译器也可能给出一些警告,如“警告:从double类型转换成float类型可能会丢失数据”。错误消息表明程序中有错,不能进行编译。而警告则表明,尽管编写的代码有效,但可能不是程序员想要的。警告并不终止编译。特殊的警告与C如何处理1700.0这样的值有关,C语言将浮点数常量1700.0视为double类型。

另外要注意的是,getchar()函数读取下一个输入字符,因此程序会等待用户输入。在这种情况下,键入156并按下Enter(或Return)键(发送一个换行符),然后scanf()读取键入的数字,第1个getchar()读取换行符,第2个getchar() 让程序暂停,等待输入。

1.1 程序中的新元素

  • 新的变量声明。前面的例子中只使用了整数类型的变量(int),但是本例使用了浮点数类型(float)的变量,以便处理更大范围的数据。float类型可以储存带小数的数字。
  • 演示了常量的几种新写法。现在可以使用带小数点的数了。
  • printf()中使用%f来处理浮点值。%.2f中的.2用于精确控制输出,指定输出的浮点数只显示小数点后面两位。
  • scanf()函数用于读取键盘的输入。%f说明scanf()要读取用户从键盘输入的浮点数,&weight告诉scanf()把输入的值赋给名为weight的变量。scanf()函数使用&符号表明找到weight变量的地点。
  • scanf()printf()函数用于实现程序的交互性。scanf()函数读取用户从键盘输入的数据,并把数据传递给程序;printf()函数读取程序中的数据,并把数据显示在屏幕上。把两个函数结合起来,就可以建立人机双向通信,这让使用计算机更加饶有趣味。
程序中的scanf()和printf()函数

2. 变量与常量数据

在程序的指导下,计算机可以做许多事情,如数值计算、名字排序、执行语言或视频命令、计算彗星轨道、准备邮件列表、拨电话号码、画画、做决策或其他你能想到的事情。要完成这些任务,程序需要使用数据 ,即承载信息的数字和字符。有些数据类型在程序使用之前已经预先设定好了,在整个程序的运行过程中没有变化,这些称为常量(constant。其他数据类型在程序运行期间可能会改变或被赋值,这些称为变量(variable

3. 数据类型关键字

不仅变量和常量不同,不同的数据类型之间也有差异。一些数据类型表示数字,一些数据类型表示字母(更普遍地说是字符)。如果数据是常量,编译器一般通过用户书写的形式来识别类型(如,42是整数,42.100是浮点数)。但是,对变量而言,要在声明时指定其类型。K&C给出了7个与类型相关的关键字。C90标准添加了2个关键字,C99标准又添加了3个关键字,见下表。

C语言数据类型关键字

在C语言中,用int关键字来表示基本的整数类型。后3个关键字(longshortunsigned)和C90新增的signed用于提供基本整数类型的变式,例如unsigned short intlong long intchar关键字用于指定字母和其他字符(如,#$%和*)。另外,char类型也可以表示较小的整数。floatdoublelong double表示带小数点的数。_Bool类型表示布尔值(truefalse),_Complex_Imaginary分别表示复数和虚数。

通过这些关键字创建的类型,按计算机的储存方式可分为两大基本类型:整数类型和浮点数类型 。

位、字节和字

位、字节和字是描述计算机数据单元或存储单元的术语。这里主要指存储单元。

最小的存储单元是位(bit),可以储存0或1(或者说,位用于设置“开”或“关”)。位是计算机内存的基本构建块。

字节(byte)是常用的计算机存储单位。对于几乎所有的机器,1字节均为8位。这是字节的标准定义,至少在衡量存储单位时是这样。

字(word)是设计计算机时给定的自然存储单位。对于8位的微型计算机(如,最初的苹果机),1个字长只有8位。从那以后,个人计算机字长增至16位、32位,直到目前的64位。计算机的字长越大,其数据转移越快,允许的内存访问也更多。

对我们而言,整数和浮点数的区别是它们的书写方式不同。对计算机而言,它们的区别是储存方式不同。

3.1 整数

和数学的概念一样,在C语言中,整数是没有小数部分的数。例如,2、−23和2456都是整数。而3.14、0.22和2.000都不是整数。计算机以二进制数字储存整数,例如,整数7以二进制写是111。因此,要在8位字节中储存该数字,需要把前5位都设置成0,后3位设置成1。

使用二进制编码储存整数7

3.2 浮点数

浮点数与数学中实数的概念差不多。2.75、3.16E7、7.00和2e-8都是浮点数。注意,在一个值后面加上一个小数点,该值就成为一个浮点值。所以,7是整数,7.00是浮点数。e记数法:3.16E7表示3.16×10^7 (3.16乘以10的7次方),其中,\(10^7=10000000\),7被称为10的指数 。

这里关键要理解浮点数和整数的储存方案不同。计算机把浮点数分成小数部分和指数部分来表示,而且分开储存这两部分。当然,计算机在内部使用二进制和2的幂进行储存,而不是10的幂。

以浮点格式(十进制)储存pai的值
  • 整数没有小数部分,浮点数有小数部分。
  • 浮点数可以表示的范围比整数大。
  • 对于一些算术运算(如,两个很大的数相减),浮点数损失的精度更多。
  • 因为在任何区间内(如,1.0到2.0之间)都存在无穷多个实数,所以计算机的浮点数不能表示区间内所有的值。浮点数通常只是实际值的近似值。例如,7.0可能被储存为浮点值6.99999。
  • 过去,浮点运算比整数运算慢。不过,现在许多CPU都包含浮点处理器,缩小了速度上的差距。

4. C语言基本数据类型

4.1 int类型

C语言提供了许多整数类型,为什么一种类型不够用?因为C语言让程序员针对不同情况选择不同的类型。C语言中的整数类型可表示不同的取值范围和正负值。一般情况使用int类型即可,但是为满足特定任务和机器的要求,还可以选择其他类型。

int类型是有符号整型,即int类型的值必须是整数,可以是正整数、负整数或零。其取值范围依计算机系统而异。一般而言,储存一个int要占用一个机器字长。因此,早期的16位IBM PC兼容机使用16位来储存一个int值,其取值范围(即int值的取值范围)是-3276832767 。目前的个人计算机一般是32位,因此用32位储存一个int值。现在,个人计算机产业正逐步向着64位处理器发展,自然能储存更大的整数。ISO C规定int的取值范围最小为-3276832767

4.1.1 声明int变量
int erns;
int hogs, cows, goats;
4.1.2 初始化变量

初始化 (initialize)变量就是为变量赋一个初始值。在C语言中,初始化可以直接在声明中完成。

int hogs = 21;
int cows = 32, goats = 14;
int dogs, cats = 94; // 有效,但是这种格式很糟糕

简而言之,声明为变量创建和标记存储空间,并为其指定初始值。

定义并初始化变量
4.1.3 int类型常量

上面示例中出现的整数(21、32、14和94)都是整型常量或整型字面量 。C语言把大多数整型常量视为int类型,但是非常大的整数除外

4.1.4 打印int值

可以使用printf()函数打印int类型的值。%d指明了在一行中打印整数的位置。

%d称为转换说明 ,它指定了printf()应使用什么格式来显示一个值。作为程序员,要确保转换说明的数量与待打印值的数量相同,编译器不会捕获这类型的错误。未指明待打印值的转化说明打印出的值是内存中的任意值。

4.1.5 八进制和十六进制

通常,C语言都假定整型常量是十进制数。然而,许多程序员很喜欢使用八进制和十六进制数。因为8和16都是2的幂,而10却不是。显然,八进制和十六进制记数系统在表达与计算机相关的值时很方便。

在C语言中,用特定的前缀表示使用哪种进制。0x或0X前缀表示十六进制值,所以十进制数16表示成十六进制是0x10或0X10。与此类似,0前缀表示八进制。例如,十进制数16表示成八进制是020。

4.1.6 显示八进制和十六进制

在C程序中,既可以使用也可以显示不同进制的数。不同的进制要使用不同的转换说明。以十进制显示数字,使用%d;以八进制显示数字,使用%o;以十六进制显示数字,使用%x。另外,要显示各进制数的前缀00x0X,必须分别使用%#o%#x%#X。注意,如果要在八进制和十六进制值前显示00x前缀,要分别在转换说明中加入#。

4.2 其他整数类型

C语言提供3个附属关键字修饰基本整数类型:shortlongunsigned

  • short int类型(或者简写为short)占用的存储空间可能比int类型少,常用于较小数值的场合以节省空间。与int类似,short是有符号类型。
  • long intlong占用的存储空间可能比int多,适用于较大数值的场合。与int类似,long是有符号类型。
  • long long intlong long(C99标准加入)占用的储存空间可能比long多,适用于更大数值的场合。该类型至少占64位。与int类似,long long是有符号类型。
  • unsigned intunsigned只用于非负值的场合。这种类型与有符号类型表示的范围不同。例如,16位unsigned int允许的取值范围是065535,而不是-3276832767。用于表示正负号的位现在用于表示另一个二进制位,所以无符号整型可以表示更大的数。
  • 在C90标准中,添加了unsigned long intunsigned longunsigned short intunsigned short类型。C99标准又添加了unsigned long long intunsigned long long
  • 在任何有符号类型前面添加关键字signed,可强调使用有符号类型的意图。例如,shortshort intsigned shortsigned short int都表示同一种类型。
4.2.1 声明其他整数类型
long int estine;
long johns;
short int erns;
short ribs;
unsigned int s_count;
unsigned players;
unsigned long headcount;
unsigned short yesvotes;
long long ago;
4.2.2 使用多种整数类型的原因

为什么说short类型“可能”比int类型占用的空间少,long类型“可能”比int类型占用的空间多?因为C语言只规定了short占用的存储空间不能多于intlong占用的存储空间不能少于int

这样规定是为了适应不同的机器。例如,过去的一台运行Windows 3.x的机器上,int类型和short类型都占16位,long类型占32位。后来,Windows和苹果系统都使用16位储存short类型,32位储存int类型和long类型(使用32位可以表示的整数数值超过20亿)。现在,计算机普遍使用64位处理器,为了储存64位的整数,才引入了long long类型。

现在,个人计算机上最常见的设置是,long long占64位,long占32位,short 占16位,int占16位或32位(依计算机的自然字长而定)。

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位。

4.2.3 long常量和long long常量

通常,程序代码中使用的数字(如,2345)都被储存为int类型。如果使用1000000这样的大数字,超出了int类型能表示的范围,编译器会将其视为long int类型(假设这种类型可以表示该数字)。如果数字超出long可表示的最大值,编译器则将其视为unsigned long类型。如果还不够大,编译器则将其视为long longunsigned long long类型(前提是编译器能识别这些类型)。

八进制和十六进制常量被视为int类型。

要把一个较小的常量作为long类型对待,可以在值的末尾加上l(小写的L )或L后缀。使用L后缀更好,因为l看上去和数字1很像。因此,在int为16位、long为32位的系统中,会把7作为16位储存,把7L作为32位储存。lL后缀也可用于八进制和十六进制整数,如020L0x10L

类似地,在支持long long类型的系统中,也可以使用llLL后缀来表示long long类型的值,如3LL。另外,uU后缀表示unsigned long long,如5ull10LLU6LLU9Ull

整数溢出

如果整数超出了相应类型的取值范围会怎样?在超过最大值时,unsigned int类型的变量j从0开始;而int类型的变量i则从−2147483648开始。

溢出行为是未定义的行为,C标准并未定义有符号类型的溢出规则。以上描述的溢出行为比较有代表性,但是也可能会出现其他情况。

4.2.4 打印short、long、long long和unsigned类型

打印unsigned int类型的值,使用%u转换说明;打印long类型的值,使用%ld转换说明。如果系统中intlong的大小相同,使用%d就行。但是,这样的程序被移植到其他系统(intlong类型的大小不同)中会无法正常工作。在xo前面可以使用l前缀,%lx表示以十六进制格式打印long类型整数,%lo表示以八进制格式打印long类型整数。注意,虽然C允许使用大写或小写的常量后缀,但是在转换说明中只能用小写。

对于short类型,可以使用h前缀。%hd表示以十进制显示short类型的整数,%ho表示以八进制显示short类型的整数。hl前缀都可以和u一起使用,用于表示无符号类型。例如,%lu表示打印unsigned long类型的值。对于支持long long类型的系统,%lld%llu分别表示有符号和无符号类型。

注意:

  • 使用错误的转换说明会得到意想不到的结果。

  • printf()中无论指定以short类型(%hd)还是int类型(%d)打印,打印出来的值都相同。这是因为在给函数传递参数时,C编译器把short类型的值自动转换成int类型的值。你可能会提出疑问:为什么要进行转换?h修饰符有什么用?

  • 第1个问题的答案是,int类型被认为是计算机处理整数类型时最高效的类型。因此,在shortint类型的大小不同的计算机中,用int类型的参数传递速度更快。

  • 第2个问题的答案是,使用h修饰符可以显示较大整数被截断成short类型值的情况。

4.3 使用字符:char类型

char类型用于储存字符(如,字母或标点符号),但是从技术层面看,char是整数类型。因为char类型实际上储存的是整数而不是字符。

计算机使用数字编码来处理字符,即用特定的整数表示特定的字符。美国最常用的编码是ASCII(American Standard Code for Information Interchange)编码,本书也使用此编码。例如,在ASCII码中,整数65代表大写字母A

标准ASCII码的范围是0~127,只需7位二进制数即可表示。通常,char类型被定义为8位的存储单元,因此容纳标准ASCII码绰绰有余。许多其他系统(如IMB PC和苹果Macs)还提供扩展ASCII码,也在8位的表示范围之内。一般而言,C语言会保证char类型足够大,以储存系统(实现C语言的系统)的基本字符集。

C语言把1字节定义为char类型占用的位(bit)数,因此无论是16位还是32位系统,都可以使用char类型。

4.3.1 声明char类型变量
char response;
char itable, latan;
4.3.2 字符常量和初始化

如果要把一个字符常量初始化为字母A,不必背下ASCII码,用计算机语言很容易做到。

char grade = 'A';

在C语言中,用单引号括起来的单个字符被称为字符常量 (character constant。编译器一发现'A',就会将其转换成相应的代码值。单引号必不可少。下面还有一些其他的例子:

char broiled;        /* 声明一个char类型的变量 */
broiled = 'T';       /* 为其赋值,正确 */
broiled = T;         /* 错误!此时T是一个变量 */
broiled = "T";       /* 错误!此时"T"是一个字符串 */

奇怪的是,C语言将字符常量视为int类型而非char类型。例如,在int为32位、char为8位的ASCII系统中,有下面的代码:

char grade = 'B';

本来'B'对应的数值66储存在32位的存储单元中,现在却可以储存在8位的存储单元中(grade)。利用字符常量的这种特性,可以定义一个字符常量'FATE',即把4个独立的8位ASCII码储存在一个32位存储单元中。如果把这样的字符常量赋给char类型变量grade,只有最后8位有效。因此,grade的值是'E'。

4.3.3 非打印字符

单引号只适用于字符、数字和标点符号,浏览ASCII表会发现,有些ASCII字符打印不出来。例如,一些代表行为的字符(如,退格、换行、终端响铃或蜂鸣)。C语言提供了3种方法表示这些字符。

第1种方法前面介绍过——使用ASCII码。例如,蜂鸣字符的ASCII值是7,因此可以这样写:

char beep = 7;

第2种方法是,使用特殊的符号序列表示一些特殊的字符。这些符号序列叫作转义序列 (escape sequence)。把转义序列赋给字符变量时,必须用单引号把转义序列括起来。

转义序列

使用C90新增的警报字符(\a)是否能产生听到或看到的警报,取决于计算机的硬件,蜂鸣是最常见的警报(在一些系统中,警报字符不起作用)。C标准规定警报字符不得改变活跃位置。标准中的活跃位置 (active position)指的是显示设备(屏幕、电传打字机、打印机等)中下一个字符将出现的位置。简而言之,平时常说的屏幕光标位置就是活跃位置。在程序中把警报字符输出在屏幕上的效果是,发出一声蜂鸣,但不会移动屏幕光标。

表中的最后两个转义序列(\0oo\xhh)是ASCII码的特殊表示。如果要用八进制ASCII码表示一个字符,可以在编码值前面加一个反斜杠(\)并用单引号括起来。例如,如果编译器不识别警报字符(\a),可以使用ASCII码来代替:

beep = '\007';
4.3.4 打印字符

printf()函数用%c指明待打印的字符。前面介绍过,一个字符变量实际上被储存为1字节的整数值。因此,如果用%d转换说明打印char类型变量的值,打印的是一个整数。而%c转换说明告诉printf()打印该整数值对应的字符。

注意,printf()函数中的转换说明决定了数据的显示方式,而不是数据的储存方式。

数据显示和数据存储
4.3.5 有符号还是无符号

有些C编译器把char实现为有符号类型,这意味着char可表示的范围是-128~127。而有些C编译器把char实现为无符号类型,那么char可表示的范围是0~255。请查阅相应的编译器手册,确定正在使用的编译器如何实现char类型。或者,可以查阅limits.h头文件。

根据C90标准,C语言允许在关键字char前面使用signedunsigned。这样,无论编译器默认char是什么类型,signed char表示有符号类型,而unsigned char表示无符号类型。这在用char类型处理小整数时很有用。如果只用char处理字符,那么char前面无需使用任何修饰符。

4.4 _Bool类型

C99标准添加了_Bool类型,用于表示布尔值,即逻辑值truefalse。因为C语言用值1表示true,值0表示false,所以_Bool类型实际上也是一种整数类型。但原则上它仅占用1位存储空间,因为对01而言,1位的存储空间足够了。

4.5 可移植类型:stdint.h和inttypes.h

C语言提供了许多有用的整数类型。但是,某些类型名在不同系统中的功能不一样。C99新增了两个头文件stdint.hinttypes.h,以确保C语言的类型在各系统中的功能相同。

C语言为现有类型创建了更多类型名。这些新的类型名定义在stdint.h头文件中。例如,int32_t表示32位的有符号整数类型。在使用32位int的系统中,头文件会把int32_t作为int的别名。不同的系统也可以定义相同的类型名。例如,int为16位、long为32位的系统会把int32_t作为long的别名。然后,使用int32_t类型编写程序,并包含stdint.h头文件时,编译器会把intlong替换成与当前系统匹配的类型。

上面讨论的类型别名是精确宽度整数类型 (exact-width integer type的示例。int32_t表示整数类型的宽度正好是32位。但是,计算机的底层系统可能不支持。因此,精确宽度整数类型是可选项。

C99和C11提供了第2类别名集合。一些类型名保证所表示的类型一定是至少有指定宽度的最小整数类型。这组类型集合被称为最小宽度类型(minimum width type。例如,int_least8_t是可容纳8位有符号整数值的类型中宽度最小的类型的一个别名。如果某系统的最小整数类型是16位,可能不会定义int8_t类型。尽管如此,该系统仍可使用int_least8_t类型,但可能把该类型实现为16位的整数类型。

当然,一些程序员更关心速度而非空间。为此,C99和C11定义了一组可使计算达到最快的类型集合。这组类型集合被称为最快最小宽度类型(fastst minimum width type。例如,int_fast8_t被定义为系统中对8位有符号值而言运算最快的整数类型的别名。

另外,有些程序员需要系统的最大整数类型。为此,C99定义了最大的有符号整数类型intmax_t,可储存任何有效的有符号整数值。类似地,uintmax_t表示最大的无符号整数类型。顺带一提,这些类型有可能比long longunsigned long类型更大,因为C编译器除了实现标准规定的类型以外,还可利用C语言实现其他类型。例如,一些编译器在标准引入long long类型之前,已提前实现了该类型。

C99和C11不仅提供可移植的类型名,还提供相应的输入和输出。例如,printf()打印特定类型时要求与相应的转换说明匹配。如果要打印int32_t类型的值,有些定义使用%d,而有些定义使用%ld,怎么办?C标准针对这一情况,提供了一些字符串宏(第4章中详细介绍)来显示可移植类型。例如,inttypes.h头文件中定义了PRId32字符串宏,代表打印32位有符号值的合适转换说明(如dl)。

注意:对C99/C11的支持

C语言发展至今,虽然ISO已发布了C11标准,但是编译器供应商对C99的实现程度却各不相同。在本书第6版的编写过程中,一些编译器仍未实现inttypes.h头文件及其相关功能。

4.6 float、double和long double

各种整数类型对大多数软件开发项目而言够用了。然而,面向金融和数学的程序经常使用浮点数 。C语言中的浮点类型有floatdoublelong double类型。

前面提到过,浮点类型能表示包括小数在内更大范围的数。浮点数的表示类似于科学记数法( 即用小数乘以10的幂来表示数字) 。该记数系统常用于表示非常大或非常小的数。

记数法示例

第1列是一般记数法;第2列是科学记数法;第3列是指数记数法(或称为e记数法),这是科学记数法在计算机中的写法,e后面的数字代表10的指数。

C标准规定,float类型必须至少能表示6位有效数字,且取值范围至少是10-37 ~10+37 。前一项规定指float类型必须能够表示33.333333的前6位数字,而不是精确到小数点后6位数字。后一项规定用于方便地表示诸如太阳质量(2.0e30千克)、一个质子的电荷量(1.6e-19库仑)或国家债务之类的数字。通常,系统储存一个浮点数要占用32位。其中8位用于表示指数的值和符号,剩下24位用于表示非指数部分(也叫作尾数或有效数)及其符号。

C语言提供的另一种浮点类型是double(意为双精度)。double类型和float类型的最小取值范围相同,但至少必须能表示10位有效数字。一般情况下,double占用64位而不是32位。一些系统将多出的32位全部用来表示非指数部分,这不仅增加了有效数字的位数(即提高了精度),而且还减少了舍入误差。另一些系统把其中的一些位分配给指数部分,以容纳更大的指数,从而增加了可表示数的范围。无论哪种方法,double类型的值至少有13位有效数字,超过了标准的最低位数规定。

C语言的第3种浮点类型是long double,以满足比double类型更高的精度要求。不过,C只保证long double类型至少与double类型的精度相同。

4.6.1 声明浮点型变量
float noah, jonah;
double trouble;
float planck = 6.63e-34;
long double gnp;
4.6.2 浮点型常量

在代码中,可以用多种形式书写浮点型常量。浮点型常量的基本形式是:有符号的数字(包括小数点),后面紧跟e或E,最后是一个有符号数表示10的指数。

-1.56E+12
2.87e-3

正号可以省略。可以没有小数点(如,2E5)或指数部分(如,19.28),但是不能同时省略两者。可以省略小数部分(如,3.E16)或整数部分(如,.45E-6),但是不能同时省略两者。下面是更多的有效浮点型常量示例:

3.14159
.2
4e16
.8E-5
100.

不要在浮点型常量中间加空格:1.56 E+12(错误!)

默认情况下,编译器假定浮点型常量是double类型的精度。例如,假设somefloat类型的变量,编写下面的语句:

some = 4.0 * 2.0;

通常,4.02.0被储存为64位的double类型,使用双精度进行乘法运算,然后将乘积截断成float类型的宽度。这样做虽然计算精度更高,但是会减慢程序的运行速度。

在浮点数后面加上fF后缀可覆盖默认设置,编译器会将浮点型常量看作float类型,如2.3f9.11E9F。使用lL后缀使得数字成为long double类型,如54.3l4.32L。注意,建议使用L后缀,因为字母l和数字1很容易混淆。没有后缀的浮点型常量是double类型。

C99标准添加了一种新的浮点型常量格式——用十六进制表示浮点型常量,即在十六进制数前加上十六进制前缀(0x0X),用pP分别代替eE,用2的幂代替10的幂(即,p计数法)。如下所示:

0xa.1fp10

十六进制a等于十进制10.1f1/16加上15/256(十六进制f 等于十进制15 ),p1021010240xa.1fp10表示的值是(10 + 1/16 + 15/256)×1024(即,十进制10364.0)。注意,并非所有的编译器都支持C99的这一特性。

4.6.3 打印浮点值

printf()函数使用%f转换说明打印十进制记数法的floatdouble类型浮点数,用%e打印指数记数法的浮点数。如果系统支持十六进制格式的浮点数,可用aA分别代替eE。打印long double类型要使用%Lf%Le%La转换说明。给那些未在函数原型中显式说明参数类型的函数(如,printf())传递参数时,C编译器会把float类型的值自动转换成double类型。

4.6.4 浮点值的上溢和下溢

假设系统的最大float类型值是3.4E38,编写如下代码:

float toobig = 3.4E38 * 100.0f;
printf("%e\n", toobig);

会发生什么?这是一个上溢(overflow)的示例。当计算导致数字过大,超过当前类型能表达的范围时,就会发生上溢。这种行为在过去是未定义的,不过现在C语言规定,在这种情况下会给toobig赋一个表示无穷大 的特定值,而且printf()显示该值为infinfinity(或者具有无穷含义的其他内容)。

当对一个很小的数做除法时,情况更为复杂。回忆一下,float类型的数以指数和尾数部分来储存。存在这样一个数,它的指数部分是最小值,即由全部可用位表示的最小尾数值。该数字是float类型能用全部精度表示的最小数字。现在把它除以2。通常,这个操作会减小指数部分,但是假设的情况中,指数已经是最小值了。所以计算机只好把尾数部分的位向右移,空出第1个二进制位,并丢弃最后一个二进制数。以十进制为例,把一个有4位有效数字的数(如,0.1234E-10)除以10,得到的结果是0.0123E-10。虽然得到了结果,但是在计算过程中却损失了原末尾有效位上的数字。这种情况叫作下溢 (underflow)。

C语言把损失了类型全精度的浮点值称为低于正常的(subnormal)浮点值。因此,把最小的正浮点数除以2将得到一个低于正常的值。如果除以一个非常大的值,会导致所有的位都为0。现在,C库已提供了用于检查计算是否会产生低于正常值的函数。

还有另一个特殊的浮点值NaN(not a number的缩写)。例如,给asin()函数传递一个值,该函数将返回一个角度,该角度的正弦就是传入函数的值。但是正弦值不能大于1,因此,如果传入的参数大于1,该函数的行为是未定义的。在这种情况下,该函数将返回NaN值,printf()函数可将其显示为nanNaN或其他类似的内容。

浮点数舍入错误

给定一个数,加上1,再减去原来给定的数,结果是多少?你一定认为是1。但是,下面的浮点运算给出了不同的答案:

/* floaterr.c--演示舍入错误 */
#include <stdio.h>
int main(void)
{
    float a,b;

    b = 2.0e20 + 1.0;
    a = b - 2.0e20;
    printf("%f \n", a);

    return 0;
}

该程序的输出如下:

0.000000        ←Linux系统下的老式gcc
-13584010575872.000000   ←Turbo C 1.5
4008175468544.000000     ←XCode 4.5、Visual Studio 2012、当前版本的gcc

得出这些奇怪答案的原因是,计算机缺少足够的小数位来完成正确的运算。2.0e20是2后面有20个0。如果把该数加1,那么发生变化的是第21位。要正确运算,程序至少要储存21位数字。而float类型的数字通常只能储存按指数比例缩小或放大的6或7位有效数字。在这种情况下,计算结果一定是错误的。另一方面,如果把2.0e20改成2.0e4,计算结果就没问题。因为2.0e4加1只需改变第5位上的数字,float类型的精度足够进行这样的计算。

4.7 复数和虚数类型

许多科学和工程计算都要用到复数和虚数。C99标准支持复数类型和虚数类型,但是有所保留。一些独立实现,如嵌入式处理器的实现,就不需要使用复数和虚数(VCR芯片就不需要复数)。一般而言,虚数类型都是可选项。C11标准把整个复数软件包都作为可选项。

简而言之,C语言有3种复数类型:float _Complexdouble _Complexlong double _Complex。例如,float _Complex类型的变量应包含两个float类型的值,分别表示复数的实部和虚部。类似地,C语言的3种虚数类型是float _Imaginarydouble _Imaginarylong double _Imaginary

如果包含complex.h头文件,便可用complex代替_Complex,用imaginary代替_Imaginary,还可以用I代替-1的平方根。

为何C标准不直接用complex作为关键字来代替_Complex,而要添加一个头文件(该头文件中把complex定义为_Complex)?因为标准委员会考虑到,如果使用新的关键字,会导致以该关键字作为标识符的现有代码全部失效。例如,之前的C99,许多程序员已经使用struct complex定义一个结构来表示复数或者心理学程序中的心理状况。让complex成为关键字会导致之前的这些代码出现语法错误。但是,使用struct _Complex的人很少,特别是标准使用首字母是下划线的标识符作为预留字以后。因此,标准委员会选定_Complex作为关键字,在不用考虑名称冲突的情况下可选择使用complex

4.8 其他类型

C语言还有一些从基本类型衍生的其他类型,包括数组、指针、结构和联合。尽管后面章节中会详细介绍这些类型,但是本章的程序示例中已经用到了指针〔指针 (pointer)指向变量或其他数据对象位置〕。例如,在scanf()函数中用到的前缀&,便创建了一个指针,告诉scanf()把数据放在何处。

小结:基本数据类型

关键字:

基本数据类型由11个关键字组成:intlongshortunsignedcharfloatdoublesigned_Bool_Complex_Imaginary

有符号整型:

有符号整型可用于表示正整数和负整数。

  • int——系统给定的基本整数类型。C语言规定int类型不小于16位。
  • short或short int——最大的short类型整数小于或等于最大的int类型整数。C语言规定short类型至少占16位。
  • long或long int——该类型可表示的整数大于或等于最大的int类型整数。C语言规定long类型至少占32位。
  • long long或long long int——该类型可表示的整数大于或等于最大的long类型整数。long long类型至少占64位。

一般而言,long类型占用的内存比short 类型大,int类型的宽度要么和long类型相同,要么和short类型相同。例如,旧DOS系统的PC提供16位的shortint,以及32位的long;Windows 95系统提供16位的short以及32位的intlong

无符号整型:

无符号整型只能用于表示零和正整数,因此无符号整型可表示的正整数比有符号整型的大。在整型类型前加上关键字unsigned表明该类型是无符号整型:unsigned intunsigned longunsigned short。单独的unsigned相当于unsigned int

字符类型:

可打印出来的符号(如A&+)都是字符。根据定义,char类型表示一个字符要占用1字节内存。出于历史原因,1字节通常是8位,但是如果要表示基本字符集,也可以是16位或更大。

  • char——字符类型的关键字。有些编译器使用有符号的char,而有些则使用无符号的char。在需要时,可在char前面加上关键字signedunsigned来指明具体使用哪一种类型。

布尔类型:

布尔值表示truefalse。C语言用1表示true,0表示false

  • _Bool——布尔类型的关键字。布尔类型是无符号int类型,所占用的空间只要能储存0或1即可。

实浮点类型:

实浮点类型可表示正浮点数和负浮点数。

  • float——系统的基本浮点类型,可精确表示至少6位有效数字。
  • double——储存浮点数的范围(可能)更大,能表示比float类型更多的有效数字(至少10位,通常会更多)和更大的指数。
  • long double——储存浮点数的范围(可能)比double更大,能表示比double更多的有效数字和更大的指数。

复数和虚数浮点数:

虚数类型是可选的类型。复数的实部和虚部类型都基于实浮点类型来构成:

  • float _Complex
  • double _Complex
  • long double _Complex
  • float _Imaginary
  • double _Imaginary
  • long double _Imaginary

4.9 类型大小

/* typesize.c -- 打印类型大小 */
#include <stdio.h>
int main(void)
{
    /* C99为类型大小提供%zd转换说明 */
    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的返回类型。一些不支持C99和C11的编译器可用%u%lu代替%zd

该程序列出了6种类型的大小,你也可以把程序中的类型更换成感兴趣的其他类型。注意,因为C语言定义了char类型是1字节,所以char类型的大小一定是1字节。而在char类型为16位、double类型为64位的系统中,sizeof给出的double是4字节。在limits.hfloat.h头文件中有类型限制的相关信息。

5. 使用数据类型

编写程序时,应注意合理选择所需的变量及其类型。通常,用intfloat类型表示数字,char类型表示字符。在使用变量之前必须先声明,并选择有意义的变量名。初始化变量应使用与变量类型匹配的常数类型。例如:

int apples = 3;     //正确
int oranges = 3.0;  //不好的形式

把一个类型的数值初始化给不同类型的变量时,编译器会把值转换成与变量匹配的类型,这将导致部分数据丢失。例如,下面的初始化:

int cost = 12.99;         /* 用double类型的值初始化int类型的变量 */
float pi = 3.1415926536;     /* 用double类型的值初始化float类型的变量 */

第1个声明,cost的值是12C编译器把浮点数转换成整数时,会直接丢弃(截断)小数部分,而不进行四舍五入。第2个声明会损失一些精度,因为C只保证了float类型前6位的精度。编译器对这样的初始化可能给出警告。

6. 参数和陷阱

有必要再次提醒读者注意printf()函数的用法。传递给函数的信息被称为参数。例如,printf("Hello,pal.")函数调用有一个参数:"Hello,pal."

与此类似,scanf("%d", &weight)函数调用有两个参数:"%d"&weight。C语言用逗号分隔函数中的参数。printf()scanf()函数与一般函数不同,它们的参数个数是可变的。

程序员要负责确保转换说明的数量、类型与后面参数的数量、类型相匹配。现在,C语言通过函数原型机制检查函数调用时参数的个数和类型是否正确。但是,该机制对printf()scanf()不起作用,因为这两个函数的参数个数可变。

7. 转义序列示例

/* escape.c -- 使用转义序列 */
#include <stdio.h>
int main(void)
{
    float salary;

    printf("\aEnter your desired monthly salary:");    /* 1 */
    printf(" $_______\b\b\b\b\b\b\b");                 /* 2 */
    scanf("%f", &salary);
    printf("\n\t$%.2f a month is $%.2f a year.", salary,
            salary * 12.0);                           /* 3 */
    printf("\rGee!\n");                               /* 4 */

    return 0;
}

printf()何时把输出发送到屏幕上?最初,printf()语句把输出发送到一个叫作缓冲区 (buffer)的中间存储区域,然后缓冲区中的内容再不断被发送到屏幕上。C标准明确规定了何时把缓冲区中的内容发送到屏幕:当缓冲区满、遇到换行字符或需要输入的时候(从缓冲区把数据发送到屏幕或文件被称为刷新缓冲区)。例如,前两个printf()语句既没有填满缓冲区,也没有换行符,但是下一条scanf()语句要求用户输入,这迫使printf()的输出被发送到屏幕上。

旧式编译器遇到scanf()也不会强行刷新缓冲区,程序会停在那里不显示任何提示内容,等待用户输入数据。在这种情况下,可以使用换行字符刷新缓冲区。

无论接下来的输入是否能刷新缓冲区,代码都会正常运行。这将导致光标移至下一行起始处,用户无法在提示内容同一行输入数据。还有一种刷新缓冲区的方法是使用fflush()函数。

8. 关键概念

C语言提供了大量的数值类型,目的是为程序员提供方便。那以整数类型为例,C认为一种整型不够,提供了有符号、无符号,以及大小不同的整型,以满足不同程序的需求。

计算机中的浮点数和整数在本质上不同,其存储方式和运算过程有很大区别。即使两个32位存储单元储存的位组合完全相同,但是一个解释为float类型,另一个解释为long类型,这两个相同的位组合表示的值也完全不同。例如,在PC中,假设一个位组合表示float类型的数256.0,如果将其解释为long类型,得到的值是113246208。C语言允许编写混合数据类型的表达式,但是会进行自动类型转换,以便在实际运算时统一使用一种类型。

计算机在内存中用数值编码来表示字符。美国最常用的是ASCII码,除此之外C也支持其他编码。字符常量是计算机系统使用的数值编码的符号表示,它表示为单引号括起来的字符,如'A'

9. 本章小结

C有多种的数据类型。基本数据类型分为两大类:整数类型和浮点数类型。通过为类型分配的储存量以及是有符号还是无符号,区分不同的整数类型。最小的整数类型是char,因实现不同,可以是有符号的char或无符号的char,即unsigned charsigned char。但是,通常用char类型表示小整数时才这样显示说明。其他整数类型有shortintlonglong long类型。C规定,后面的类型不能小于前面的类型。上述都是有符号类型,但也可以使用unsigned关键字创建相应的无符号类型:unsigned shortunsigned intunsigned longunsigned long long。或者,在类型名前加上signed修饰符显式表明该类型是有符号类型。最后,_Bool类型是一种无符号类型,可储存01,分别代表falsetrue

浮点类型有3种:floatdouble和C90新增的long double。后面的类型应大于或等于前面的类型。有些实现可选择支持复数类型和虚数类型,通过关键字_Complex_Imaginary与浮点类型的关键字组合(如,double _Complex类型和float _Imaginary类型)来表示这些类型。

整数可以表示为十进制、八进制或十六进制。0前缀表示八进制数,0x或0X前缀表示十六进制数。例如,32、040、0x20分别以十进制、八进制、十六进制表示同一个值。lL后缀表明该值是long类型,llLL后缀表明该值是long long类型。

在C语言中,直接表示一个字符常量的方法是:把该字符用单引号括起来,如'Q''8''$'。C语言的转义序列(如,'\n')表示某些非打印字符。另外,还可以在八进制或十六进制数前加上一个反斜杠(如,'\007'),表示ASCII码中的一个字符。

浮点数可写成固定小数点的形式(如,9393.912)或指数形式(如,7.38E10)。C99和C11提供了第3种指数表示法,即用十六进制数和2的幂来表示(如,0xa.1fp10)。

printf()函数根据转换说明打印各种类型的值。转换说明最简单的形式由一个百分号(%)和一个转换字符组成,如%d%f

posted @ 2022-08-22 10:17  十豆加日月  阅读(103)  评论(1编辑  收藏  举报