第 3 章 数据和 C

目录

3.1 示例程序

3.1.1 程序中的新元素

3.2 变量与常量数据

3.3 数据: 数据类型关键字

3.3.1 整数和浮点数

3.3.2 整数

 3.3.3 浮点数

3.4 C 语言基本数据类型

3.4.1 int 类型

1. 声明 int 变量

2. 初始化变量

3. int 类型常量

4. 打印 int 值

5. 八进制和十六进制

6. 显示八进制和十六进制

3.4.2 其他整数类型

1. 声明其他整数类型

2. 使用多种整数类型的原因

3. long 常量和 long long 常量

4. 打印 short、long、long long 和 unsigned 类型

3.4.3 使用字符:char 类型

1. 声明 char 类型变量

2. 字符常量和初始化

3. 非打印字符

4. 打印字符

5. 有符号还是无符号

3.4.4 _Bool 类型

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

3.4.6 float、double 和 long double

1. 声明浮点型变量

2. 浮点型常量

3. 打印浮点值

4. 浮点值的上溢和下溢

3.4.7 复数和虚数类型

3.4.8 其他类型

3.4.9 类型大小

3.5 使用数据类型

3.6 参数和陷阱

3.7 转义序列示例

3.7.1 程序运行情况

3.7.2 刷新输出

3.8 关键概念

3.9 本章小结

3.10 复习题

3.11 编程练习


本章介绍以下内容:

  • 关键字:int、short、long、unsigned、char、float、double、 _Bool、 _Complex、 _Imaginary
  • 运算符:sizeof()
  • 函数:scanf()
  • 整数类型和浮点数类型的区别
  • 如何书写整型和浮点型常数,如何声明这些类型的变量
  • 如何使用 printf() 和 scanf() 函数读写不同类型的值

        程序离不开数据。把数字、字母和文字输入计算机,就是希望它利用这些数据完成某些任务。例如,需要计算一份利息或显示一份葡萄酒商的排序列表。本章除了介绍如何读取数据外,还将教会读者如何操控数据。

        C 语言提供两大系列的多种数据类型。本章详细介绍两大数据类型:整数类型浮点数类型,讲解这些数据类型是什么、如何声明它们、如何以及何时使用它们。除此之外,还将介绍常量和变量的区别。读者很快就能看到第 1 个交互式程序。

3.1 示例程序

        本章仍从一个简单的程序开始。如果发现有不熟悉的内容,别担心,我们稍后会详细解释。该程序的意图比较明了,请试着编译并运行程序清单 3.1 中的源代码。为了节省时间,在输入源代码时可省略注释。

        程序清单 3.1          platinum.c 程序

/* platinum.c   --   your weight in platinum */
#include <stdio.h>

int main(void)
{
    float weight;   /* 你的体重 */
    float value;    /* 相等重量的白金价值 */
    
    printf("你是否值自己体重等价的白金 ?\n");
    printf("让我们来检查一下。\n");
    printf("Please enter your weight in pounds: ");  // 请输入你的体重(以磅为单位):
    
    fflush(stdout);  // clion 编译器中确保前面的输出立即显示
    
    /* 获取用户的输入 */
    scanf("%f", &weight);
    
    /* 假设白金的价格是每盎司 $1700 */
    /* 14.5833 用于把英镑常衡盎司转换为金衡盎司 */
    value = 1700.0 * weight * 14.5833;
    
    printf("你的体重等价的白金价值为 $%.2f。\n", value);
    printf("你肯定值这个价!如果白金价格下跌,\n");
    printf("多吃点以保持你的价值。\n");
    
    return 0;
}

        欧美日常使用的度量衡单位是常衡盎司(avoirdupois ounce),而欧美黄金市场上使用的黄金交易计量单位是金衡盎司(troy ounce)。国际黄金市场上的报价,其单位“盎司”都指的是黄金盎司。常衡盎司属英制计量单位,做重量单位时也称为英两。相关换算参考如下:
        1 常衡盎司 = 28.350 克,
        1 金衡盎司 = 31.104 克,
        16 常衡盎司 = 1 磅。
该程序的单位转换思路是:把磅换算成金衡盎司,即 28.350 ÷ 31.104 × 16 = 14.5833。 

        博主已经将原书程序中的 printf() 语句中的英文语句翻译成了对应的中文语句,以便于大家的学习。需要注意的是,有些编译器可能需要额外设置才能正确打印中文,你可以自行在网上查找你所使用的编译器对应的设置方法。

        博主使用的是 CLion 编译器,在代码中加入了一行原本书中没有的 fflush(stdout); 语句。

        fflush(stdout); 是一个 C 语言标准库函数调用,用于刷新标准输出流 stdout在 C 语言中,标准输出通常是以缓冲的方式工作的,这意味着程序写入 stdout 的数据可能会暂时存储在一个缓冲区中,直到缓冲区满或者被显式地清空才会实际输出到屏幕上

        当你调用 fflush(stdout); 时,你告诉程序立即将缓冲区中的所有数据发送到标准输出设备(通常是屏幕)。这确保了之前通过 printf() 或其他输出函数写入的数据立即可见

        在 CLion 或者其他 IDE 中,如果你希望确保某个 printf() 语句输出的内容能够立即显示,而不需要等待更多的输出或者程序结束,你可以使用 fflush(stdout);。如果不是使用 CLion 编辑器,则这行代码可以根据具体情况选择是否使用。

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

        输入该程序时,可以把 1700.0 改成贵金属白金当前的市价,但是不要改动 14.5833,该数是 1 英镑的金衡盎司数(金衡盎司用于衡量贵金属,而英镑常衡盎司用于衡量人的体重)。

        注意,“enter your weight” 的意思是输入你的体重,然后按下 Enter Return 键(不要键入体重后就一直等着)。按下 Enter 键是告知计算机,你已完成输入数据。该程序需要你输入一个数字(如,155),而不是单词(如,too much)。如果输入字母而不是数字,会导致程序出问题。这个问题要用 if 语句来解决(详见第 7 章),因此请先输入数字。下面是程序的输出示例:

程序调整
        即使用第 2 章介绍的方法,在程序中添加下面一行代码:

 getchar();

        程序的输出是否依旧在屏幕上一闪而过?本例,需要调用两次 getchar() 函数:

getchar();
getchar();

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

        如果使用 CLion 编辑器运行程序就不会发生屏幕一闪而过这种情况。

3.1.1 程序中的新元素

        程序清单 3.1 中包含 C 语言的一些新元素。

        1. 注意,代码中使用了一种新的变量声明。前面的例子中只使用了整数类型的变量 ( int ) ,但是本例使用了浮点数类型 ( float ) 的变量,以便处理更大范围的数据。float 类型可以储存带小数的数字。

        2. 程序中演示了常量的几种新写法。现在可以使用带小数点的数了。

        3. 为了打印新类型的变量,在 printf() 中使用 %f 来处理浮点值。%.2f 中的 .2 用于精确控制输出,指定输出的浮点数只显示小数点后面两位

        4. scanf() 函数用于读取键盘的输入。%f 说明 scanf() 要读取用户从键盘输入的浮点数&weight 告诉 scanf() 把输入的值赋给名为 weight 的变量。scanf() 函数使用 & 符号表明找到 weight 变量的位置。下一章将详细讨论 &。就目前而言,请按照这样写。

        5. 也许本程序最突出的新特点是它的交互性。计算机向用户询问信息,然后用户输入数字。与非交互式程序相比,交互式程序用起来更有趣。更重要的是,交互式使得程序更加灵活。例如,示例程序可以使用任何合理的体重,而不只是 156 磅。不必重写程序,就可以根据不同体重进行计算。scanf() 和 printf() 函数用于实现这种交互。scanf() 函数读取用户从键盘输入的数据,并把数据传递给程序printf() 函数读取程序中的数据,并把数据显示在屏幕上。把两个函数结合起来,就可以建立人机双向通信(见图 3.1),这让使用计算机更加饶有趣味。

        本章着重解释上述新特性中的前两项:各种数据类型的变量和常量。第 4 章将介绍后 3 项。

3.2 变量与常量数据

        在程序的指导下,计算机可以做许多事情,如数值计算、名字排序、执行语言或视频命令、计算彗星轨道、准备邮件列表、拨电话号码、画画、做决策或其他你能想到的事情。要完成这些任务,程序需要使用数据,即承载信息的数字和字符。

        有些数据类型在程序使用之前已经预先设定好了,在整个程序的运行过程中没有变化,这些称为常量(constant)。其他数据类型在程序运行期间可能会改变或被赋值,这些称为变量(variable)。在示例程序中,weight 是一个变量,而 14.5833 是一个常量。那么,1700.0 是常量还是变量?在现实生活中,白金的价格不会是常量,但是在程序中,像 1700.0 这样的价格被视为常量。

3.3 数据: 数据类型关键字

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

最初 K&R 给出的关键字C90 标准添加的关键字C99 标准添加的关键字
intsigned_Bool
longvoid_complex
short_Imaginary
unsigned
char
float
double

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

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

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

        最小的存储单元是(bit),可以储存 0 或 1(或者说,位用于设置“开”或“关”)。虽然 1 位储存的信息有限,但是计算机中位的数量十分庞大。位是计算机内存的基本构建块。

        字节(byte)是常用的计算机存储单位。对于几乎所有的机器,1 字节均为 8 位。这是字节的标准定义,至少在衡量存储单位时是这样(但是,C 语言对此有不同的定义,请参阅本章 3.4.3 节)。既然 1 位可以表示 0 或 1,那么 8 位字节就有 256(2 的 8 次方)种可能的 0、1 的组合。通过二进制编码(仅用 0 和 1 便可表示数字,便可表示(0~255)的整数或一组字符(第 15 章将详细讨论二进制编码,如果感兴趣可以现在浏览一下该章的内容)。
        (word)是设计计算机时给定的自然存储单位。对于 8 位的微型计算机(如,最初的苹果机),1 个字长只有 8 位。从那以后,个人计算机字长增至 16 位、32 位,直到目前的 64 位计算机的字长越大,其数据转移越快,允许的内存访问也更多

3.3.1 整数和浮点数

        整数类型?浮点数类型?如果觉得这些术语非常陌生,别担心,下面先简述它们的含义。如果不熟悉位、字节和字的概念,请阅读上面的内容。刚开始学习时,不必了解所有的细节,就像学习开车之前不必详细了解汽车内部引擎的原理一样。但是,了解一些计算机或汽车引擎内部的原理会对你有所帮助。

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

3.3.2 整数

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

 3.3.3 浮点数

        浮点数与数学中实数的概念差不多。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 的指数。

        这里关键要理解浮点数和整数的储存方案不同。计算机把浮点数分成小数部分指数部分来表示,而且分开储存这两部分。因此,虽然 7.00 和 7 在数值上相同,但是它们的储存方式不同。在十进制下,可以把 7.0 写成 0.7E1。这里,0.7 是小数部分,1 是指数部分。图 3.3 演示了一个储存浮点数的例子。

        当然,计算机在内部使用二进制 2 的幂进行储存,而不是 10 的幂。第 15 章将详述相关内容。现在,我们着重讲解这两种类型的实际区别。

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

3.4 C 语言基本数据类型

        本节将详细介绍 C 语言的基本数据类型,包括如何声明变量、如何表示字面值常量(如,5 或 2.78),以及典型的用法。一些老式的 C 语言编译器无法支持这里提到的所有类型,请查阅你使用的编译器文档,了解可以使用哪些类型。

3.4.1 int 类型

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

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

1. 声明 int 变量

        第 2 章中已经用 int 声明过基本整型变量。先写上 int,然后写变量名,最后加上一个分号。要声明多个变量,可以单独声明每个变量,也可在 int 后面列出多个变量名,变量名之间用逗号分隔。下面都是有效的声明:

int earns;
int hogs, cows, goats;

        可以分别在 4 条声明中声明各变量,也可以在一条声明中声明 4 个变量。两种方法的效果相同,都为 4 个 int 大小的变量赋予名称并分配内存空间

        以上声明创建了变量,但是并没有给它们提供值。变量如何获得值?前面介绍过在程序中获取值的两种途径。第 1 种途径是赋值:

cows = 112;

        第 2 种途径是,通过函数(如,scanf() )获得值。接下来,我们着重介绍第 3 种途径。

2. 初始化变量

        初始化(initialize)变量就是为变量赋一个初始值。在 C 语言中,初始化可以直接在声明中完成。只需在变量名后面加上赋值运算符(= )和待赋给变量的值即可。如下所示:

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

        以上示例的最后一行,只初始化了 cats,并未初始化 dogs。这种写法很容易让人误认为 dogs 也被初始化为 94,所以最好不要把初始化的变量和未初始化的变量放在同一条声明中

        简而言之,声明为变量创建和标记存储空间,并为其指定初始值(如图 3.4 所示)。

3. int 类型常量

        上面示例中出现的整数(21、32、14 和 94)都是整型常量整型字面量C 语言把不含小数点和指数的数作为整数。因此,22 和 -44 都是整型常量,但是 22.0 和 2.2E1 则不是。C 语言把大多数整型常量视为 int 类型,但是非常大的整数除外。详见后面“long 常量和 long long 常量”小节对 long int 类型的讨论。

4. 打印 int 值

        可以使用 printf() 函数打印 int 类型的值。第 2 章中介绍过,%d 指明了在一行中打印整数的位置。%d 称为转换说明,它指定了 printf() 应使用什么格式来显示一个值。格式化字符串中的每个 %d 都与待打印变量列表中相应的 int 值匹配。这个值可以是 int 类型的变量、int 类型的常量或其他任何值为 int 类型的表达式。作为程序员,要确保转换说明的数量与待打印值的数量相同,编译器不会捕获这类型的错误。程序清单 3.2 演示了一个简单的程序,程序中初始化了一个变量,并打印该变量的值、一个常量值和一个简单表达式的值。另外,程序还演示了如果粗心犯错会导致什么结果。

        程序清单 3.2         print1.c 程序

/* print1.c  -  演示 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); // 遗漏 2 个参数,打印会有垃圾值
    
    return 0;
}

        编译并运行该程序,输出如下:

        在第一行输出中,第 1 个 %d 对应 int 类型变量 ten;第 2 个 %d 对应 int 类型常量 2;第 3 个 %d 对应 int 类型表达式 ten - two 的值。在第二行输出中,第 1 个 %d 对应 ten 的值,但是由于没有给后两个 %d 提供任何值,所以打印出的值是内存中的任意值(读者在运行该程序时显示的这两个数值会与输出示例中的数值不同,因为内存中储存的数据不同,而且编译器管理内存的位置也不同)。

        你可能会抱怨编译器为何不能捕获这种明显的错误,但实际上问题出在 printf() 不寻常的设计。大部分函数都需要指定数目的参数,编译器会检查参数的数目是否正确。但是,printf() 函数的参数数目不定,可以有 1 个、2 个、3 个或更多,编译器也爱莫能助。记住,使用 printf() 函数时,要确保转换说明的数量与待打印值的数量相等

5. 八进制和十六进制

        通常,C 语言都假定整型常量是十进制数。然而,许多程序员很喜欢使用八进制和十六进制数。因为 8 和 16 都是 2 的幂,而 10 却不是。显然,八进制和十六进制记数系统在表达与计算机相关的值时很方便。例如,十进制数 65536 经常出现在 16 位机中,用十六进制表示正好是 10000。另外,十六进制数的每一位的数恰好由 4 位二进制数表示(八进制数的每一位的数恰好由 3 位二进制数表示)。例如,十六进制数 3 是 0011,十六进制数 5 是 0101。因此,十六进制数 35 的位组合(bit pattern)是 00110101,十六进制数 53 的位组合是 01010011。这种对应关系使得十六进制和二进制的转换非常方便。但是,计算机如何知道 10000 是十进制、十六进制还是二进制?在 C 语言中,用特定的前缀表示使用哪种进制。0x 或 0X 前缀表示十六进制值,所以十进制数 16 表示成十六进制是 0x10 或 0X10。与此类似,0 前缀表示八进制。例如,十进制数 16 表示成八进制是 020。第 15 章将更全面地介绍进制相关的内容。

        要清楚,使用不同的进制数是为了方便,不会影响数被储存的方式。也就是说,无论把数字写成 16、020 或 0x10,储存该数的方式都相同,因为计算机内部都以二进制进行编码

6. 显示八进制和十六进制

        在 C 程序中,既可以使用和显示不同进制的数。不同的进制要使用不同的转换说明。以十进制显示数字,使用 %d;以八进制显示数字,使用 %o;以十六进制显示数字,使用 %x 或 %X。另外,要显示各进制数的前缀 0、0x 和 0X,必须分别使用 %#o、%#x、%#X。程序清单 3.3 演示了一个小程序。回忆一下,在某些集成开发环境(IDE)下编写的代码中插入 getchar(); 语句,程序在执行完毕后不会立即关闭执行窗口。

        程序清单 3.3         bases.c 程序

/* bases.c  --  以十进制、八进制、十六进制打印十进制数100 */
#include <stdio.h>

int main(void)
{
    int x = 100;
    
    printf("dec = %d; octal = %o; hex = %x; HEX = %X\n",x,x,x,x);
    printf("dec = %d; octal = %#o; hex = %#x; HEX = %#X\n",x,x,x,x);
    
    return 0;
}

        编译并运行该程序,输出如下:

        该程序以 4 种不同记数系统显示同一个值。printf() 函数做了相应的转换。注意,如果要在八进制和十六进制值前显示 0 和 0x(或 0X) 前缀,要分别在转换说明中加入 #。

3.4.2 其他整数类型

        初学 C 语言时,int 类型应该能满足大多数程序的整数类型需求。尽管如此,还应了解一下整型的其他形式。当然,也可以略过本节跳至 3.4.3 节阅读 char 类型的相关内容,以后有需要时再阅读本节。

        C 语言提供 3 个附属关键字修饰基本整数类型:shortlong 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 只用于非负值的场合。这种类型与有符号类型表示的范围不同。例如,16 位 unsigned int 允许的取值范围是 0~65535,而不是 -32768~32767。用于表示正负号的位现在用于表示另一个二进制位,所以无符号整型可以表示更大的数
  • 在 C90 标准中,添加了 unsigned long int 或 unsigned long 和 unsigned short 类型。C99 标准又添加了 unsigned long long int 或 unsigned long long在任何有符号类型前面添加关键字 signed,可强调使用有符号类型的意图。例如,short、short int、signed short、signed short int 都表示同一种类型。

1. 声明其他整数类型

        其他整数类型的声明方式与 int 类型相同,下面列出了一些例子。不是所有的 C 编译器都能识别最后 3 条声明,最后一个例子所有的类型是 C99 标准新增的。

// 声明一个长整型变量 estine,用于存储可能较大的整数值  
long int estine;  
  
// 声明一个长整型变量 johns,同样用于存储可能较大的整数值  
long johns;  
  
// 声明一个短整型变量 erns,适用于存储较小的整数值以节省内存  
short int erns;  
  
// 声明一个短整型变量 ribs,适用于存储较小的整数值以节省内存    
short ribs;  
  
// 声明一个无符号整型变量 s_count,用于存储非负整数值  
unsigned int s_count;  
  
// 声明一个无符号整型变量 players(默认为int),用于存储非负的玩家数量  
unsigned players;  
  
// 声明一个无符号长整型变量 headcount,用于存储可能非常大的非负整数值  
unsigned long headcount;  
  
// 声明一个无符号短整型变量 yesvotes,用于存储非负且较小的整数值,如赞成票的数量  
unsigned short yesvotes;  
  
// 声明一个长长整型变量 ago,用于存储极大范围内的整数值,可能表示很久以前的时间或数量  
long long ago;

2. 使用多种整数类型的原因

        为什么说 short 类型“可能”比 int 类型占用的空间少,long 类型“可能”比 int 类型占用的空间多?因为 C 语言只规定了 short 占用的存储空间不能多于 int,long 占用的存储空间不能少于 int。这样规定是为了适应不同的机器。例如,过去的一台运行 Windows 3 的机器上,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 位(依计算机的自然字长而定)。原则上,这 4 种类型代表 4 种不同的大小,但是在实际使用中,有些类型之间通常有重叠。

        C 标准对基本数据类型只规定了允许的最小大小。对于 16 位机,short 和 int 的最小取值范围是 [-32767, 32767];对于 32 位机,long 的最小取值范围是 [-2147483647, 2147483647]。对于 unsigned short 和 unsigned int,最小取值范围是 [0, 65535];对于 unsigned long,最小取值范围是 [0, 4294967295]。long long 类型是为了支持 64 位的需求,最小取值范围是 [-9223372036854775807, 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 常量

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

        八进制和十六进制常量被视为 int 类型。如果值太大,编译器会尝试使用 unsigned int。如果还不够大,编译器会依次使用 long、unsigned long、long long 和 unsigned long long 类型。

        有些情况下,需要编译器以 long 类型储存一个小数字。例如,编程时要显式使用 IBM PC 上的内存地址时。另外,一些 C 标准函数也要求使用 long 类型的值。要把一个较小的常量作为 long 类型对待,可以在值的末尾加上 l(小写的 L)或 L 后缀。使用 L 后缀更好,因为 l 看上去和数字 1 很像。因此,在 int 为 16 位、long 为 32 位的系统中,会把 7 作为 16 位储存,把 7L 作为 32 位储存。L 或 l 后缀也可用于八进制和十六进制整数,如 020L 和 0x10L。

        类似地,在支持 long long 类型的系统中,也可以使用 LL 或 L 后缀来表示 long long 类型的值,如 3LL。另外,uU 后缀表示 unsigned long long,如 5ull、10LLU、6LLU 或 9Ull。

整数溢出

        如果整数超出了相应类型的取值范围会怎样?下面分别将有符号类型和无符号类型的整数设置为比最大值略大,看看会发生什么(printf() 函数使用 %u 说明显示 unsigned int 类型的值)。

/* toobig.c -- 超出系统允许的最大 int 值 */
#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 类型的值,使用 %ld 转换说明。如果系统中 int 和 long 的大小相同,使用 %d 就行。但是,这样的程序被移植到其他系统(int 和 long 类型的大小不同)中会无法正常工作。在 x 和 o 前面可以使用 l 前缀,%lx 表示以十六进制格式打印 long 类型整数,%lo 表示以八进制格式打印 long 类型整数。注意,虽然 C 语言允许使用大写或小写的常量后缀,但是在转换说明中只能用小写 L(即 l

        C 语言有多种 printf() 格式。对于 short 类型,可以使用 h 前缀。%hd 表示以十进制显示 short 类型的整数,%ho 表示以八进制显示 short 类型的整数。h 和 l 前缀都可以和 u 一起使用,用于表示无符号类型。例如,%lu 表示打印 unsigned long 类型的值。程序清单 3.4 演示了一些例子。对于支持 long long 类型的系统,%lld 和 %llu 分别表示有符号和无符号类型。第 4 章将详细介绍转换说明。

        程序清单 3.4         print2.c 程序

/* print2.c -- 更多 printf() 的特性 */
#include <stdio.h>

int main(void) {
    unsigned int un = 3000000000; /* int 为 32 位和 short 为 16 位的系统 */
    short end = 200;
    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;
}

        在特定的系统中输出如下(输出的结果可能不同):

        该例表明,使用错误的转换说明会得到意想不到的结果。第 1 行输出,对于无符号变量 un,使用 %d 会生成负值!其原因是,无符号值 3000000000 和有符号值 -129496296 在系统内存中的内部表示完全相同(详见第 15 章)。因此,如果告诉 printf() 该数是无符号数,它打印一个值;如果告诉它该数是有符号数,它将打印另一个值。在待打印的值大于有符号值的最大值时,会发生这种情况。对于较小的正数(如 96),有符号和无符号类型的存储、显示都相同。

        第 2 行输出,对于 short 类型的变量 end,在 printf() 中无论指定以 short 类型 (%hd) 还是 int 类型 (%d) 打印,打印出来的值都相同。这是因为在给函数传递参数时,C 编译器把 short 类型的值自动转换成 int 类型的值。你可能会提出疑问:为什么要进行转换?h 修饰符有什么用?第 1 个问题的答案是,int 类型被认为是计算机处理整数类型时最高效的类型。因此,在 short 和 int 类型的大小不同的计算机中,用 int 类型的参数传递速度更快。第 2 个问题的答案是,使用 h 修饰符可以显示较大整数被截断成 short 类型值的情况。第 3 行输出就演示了这种情况。把 65537 以二进制格式写成一个 32 位数是 0000 0000 0000 0001 0000 0000 0000 0001。使用 %hd,printf() 只会查看后 16 位,所以显示的值是 1。与此类似,输出的最后一行先显示了 verybig 的完整值,然后由于使用了 %ld,printf() 只显示了储存于后 32 位的值。

        本章前面介绍过,程序员必须确保转换说明的数量和待打印值的数量相同。以上内容也提醒读者,程序员还必须根据待打印值的类型使用正确的转换说明。

提示        匹配 printf() 说明符的类型

        在使用 printf() 函数时,切记检查每个待打印值都有对应的转换说明,还要检查转换说明的类型是否与待打印值的类型相匹配

        应检查说明符的:【数量、类型

3.4.3 使用字符:char 类型

        char 类型用于储存字符(如,字母或标点符号),但是从技术层面看,char 是整数类型。因为 char 类型实际上储存的是整数而不是字符。计算机使用数字编码来处理字符,即用特定的整数表示特定的字符。美国最常用的编码是 ASCII 编码,本书也使用此编码。例如,在 ASCII 码中,整数 65 代表大写字母 A。因此,储存字母 A 实际上储存的是整数 65(许多 IBM 的大型主机使用另一种编码——EBCDIC,其原理相同。另外,其他国家的计算机系统可能使用完全不同的编码)。

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

        许多字符集都超过了 127,甚至多于 255。例如,日本汉字(kanji)字符集。商用的统一码(Unicode)创建了一个能表示世界范围内多种字符集的系统,目前包含的字符已超过 110000 个。国际标准化组织(ISO)和国际电工技术委员会(IEC)为字符集开发了 ISO/IEC 10646 标准。统一码标准也与 ISO/IEC 10646 标准兼容。

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

常用的ASCII码:

        字符            十进制

          0                  48

          A                  65

          a                  97

1. 声明 char 类型变量

        char 类型变量的声明方式与其他类型变量的声明方式相同。下面是一些例子:

char response;
char itable, latan;

        以上声明创建了 3 个 char 类型的变量:response、itable 和 latan。

2. 字符常量和初始化

        如果要把一个字符常量初始化为字母 A,不必背下 ASCII 码,用计算机语言很容易做到。通过以下初始化把字母 A 赋给 grade 即可:

char grade = 'A';

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

char broiled;    /* 声明一个 char 类型的变量 */
broiled = 'T';   /* 为其赋值,正确 */

broiled = T;     /* 错误! 此时 T 是一个变量 */
broiled = "T";   /* 错误! 此时 "T" 是一个字符串 */

        如上所示,如果省略单引号,编译器认为 T 是一个变量名;如果把 T 用双引号括起来,编译器则认为 "T" 是一个字符串。字符串的内容将在第 4 章中介绍。

        实际上,字符是以数值形式储存的所以也可使用数字代码值来赋值

char grade = 65; /* 对于 ASCII,这样做没问题,但这是一种不好的编程风格 */

        在本例中,虽然 65 是 int 类型,但是它在 char 类型能表示的范围内,所以将其赋值给 grade 没问题。由于 65 是字母 A 对应的 ASCII 码,因此本例是把 A 赋给 grade。注意,能这样做的前提是系统使用 ASCII 码。其实,用 'A' 代替 65 才是较为妥当的做法,这样在任何系统中都不会出问题。因此,最好使用字符常量,而不是数字代码值。

        奇怪的是,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',如下代码所示。

#include <stdio.h>

int main(void) {
    char grade = 'FATE';
    
    printf("grade is %c",grade);  // grade is E
    
    return 0;
}

3. 非打印字符

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

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

char beep = 7;

        第 2 种方法是,使用特殊的符号序列表示一些特殊的字符。这些符号序列叫作转义序列(escape sequence)。下表列出了转义序列及其含义。

转义序列含义
\a警报(ANSI C)
\b退格
\f换页
\n换行
\r回车
\t水平制表符
\v垂直制表符
\\反斜杠
\'单引号
\"双引号
\?问号
\0oo八进制值(oo 必须是有效的八进制数,即每个 o 可表示 0~7 中的一个数)
\xhh十六进制值(hh 必须是有效的十六进制数,即每个 h 可表示 0~f 中的一个数)

        把转义序列赋给字符变量时,必须用单引号把转义序列括起来。例如,假设有下面一行代码:

char nerf = '\n';

        稍后打印变量 nerf 的效果是,在打印机或屏幕上另起一行。

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

        接下来的转义字符 \b、\f、\n、\r、\t 和 \v 是常用的输出设备控制字符。了解它们最好的方式是查看它们对活跃位置的影响。

  • 换页符 (\f) 把活跃位置移至下一页的开始处;
  • 换行符 (\n) 把活跃位置移至下一行的开始处;
  • 回车符 (\r) 把活跃位置移动到当前行的开始处;
  • 水平制表符 (\t) 将活跃位置移至下一个水平制表点(通常是第 1 个、第 9 个、第 17 个、第 25 个等字符位置);
  • 垂直制表符 (\v) 把活跃位置移至下一个垂直制表点。

        这些转义序列字符不一定在所有的显示设备上都起作用。例如,换页符和垂直制表符在 PC 屏幕上可能会生成奇怪的符号,光标并不会移动。只有将其输出到打印机上时才会产生前面描述的效果。

        接下来的 3 个转义序列 ( \\、 \'、\" ) 用于打印 \、'、" 字符(由于这些字符用于定义字符常量,是 printf() 函数的一部分,若直接使用它们会造成混淆)。如果打印下面一行内容:

        Gramps sez, "a \ is a backslash."

        应这样编写代码:

printf("Gramps sez, \"a \\ is a backslash.\"\n");

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

char beep = '\007';

        可以省略前面的 0,'\07' 甚至 '\7' 都可以。即使没有前缀 0,编译器在处理这种写法时,仍会解释为八进制。

        从 C90 开始,不仅可以用十进制、八进制形式表示字符常量,C 语言还提供了第 3 种选择——用十六进制形式表示字符常量,即反斜杠后面跟一个 x 或 X,再加上 1~3 位十六进制数字。例如,Ctrl+P 字符的 ASCII 十六进制码是 10(相当于十进制的 16),可表示为 '\x10' 或 '\x010'。
图 3.5 列出了一些整数类型的不同进制形式。

        使用 ASCII 码时,注意数字和数字字符的区别。例如,字符 4 对应的 ASCII 码是 52。'4' 表示字符 4,而不是数值 4

        下面来看一个综合案例,它演示了如何使用这些转义序列,并且用八进制和十六进制来表示字符。

#include <stdio.h>

int main() {
    // 预定义的转义序列
    printf("\a - 警报(Alert)\n");
    printf("\b - 退格(Backspace)\n");
    printf("\f - 换页(Form feed)\n");
    printf("\n - 换行(Newline)\n");
    
    printf("我不换行,我会被回车转义序列所覆盖");
    
    printf("\r - 回车(Carriage return)\n");
    printf("\t - 水平制表符(Tab)\n");
    printf("\v - 垂直制表符(Vertical tab)\n");
    printf("\\ - 反斜杠(Backslash)\n");
    printf("\' - 单引号(Single quote)\n");
    printf("\" - 双引号(Double quote)\n");
    printf("\? - 问号(Question mark)\n");
    
    // 八进制表示的字符
    printf("\\0141 - 八进制表示的 'a' (ASCII码 101)\n");
    printf("\\141 - 八进制表示的 'a' (ASCII码 101)\n");  // 0 可以省略
    
    // 十六进制表示的字符
    printf("\\x41 - 十六进制表示的 'A' (ASCII码 65)\n");
    
    return 0;
}

        在我们的系统下输出的结果是:

        关于转义序列,读者可能有下面三个问题。

        1. 之前的例子 ( printf("Gramps sez, \"a \\ is a backslash.\"\n"); ),为何没有用单引号把转义序列括起来?无论是普通字符还是转义序列,只要是双引号括起来的字符集合,就无需用单引号括起来。双引号中的字符集合叫作字符串(详见第 4 章)。注意,该例中的其他字符(G、r、a、m、p、s 等)都没有用单引号括起来。与此类似,printf("Hello! \007\n"); 将打印 Hello! 并发出一声蜂鸣,而 printf("Hello!7\n"); 则打印 Hello!7。不是转义序列中的数字将作为普通字符被打印出来

        2. 何时使用 ASCII 码?何时使用转义序列?如果要在转义序列(假设使用 '\f' )和 ASCII 码( '\014')之间选择,请选择前者(即 '\f' )。这样的写法不仅更好记,而且可移植性更高。 '\f' 在不使用 ASCII 码的系统中仍然有效。

        3. 如果要使用 ASCII 码,为何要写成 '\032' 而不是 032?首先,'\032' 能更清晰地表达程序员使用字符编码的意图。其次,类似 \032 这样的转义序列可以嵌入 C 的字符串中,如 printf("Hello\007\n"); 中就嵌入了 \007。

4. 打印字符

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

        程序清单 3.5         charcode.c 程序

/* charcode.c - 显示字符的代码编号 */
#include <stdio.h>

int main(void) {
    char ch;
    
    printf("Please enter a character.\n");

    fflush(stdout); // clion 编译器中确保前面的输出立即显示

    scanf("%c", &ch); /* 用户输入字符 */
    
    printf("The code for %c is %d.\n", ch, ch);
    
    return 0;
}

        运行该程序后,输出示例如下:

        运行该程序时,在输入字母后不要忘记按下 Enter Return 键。随后,scanf() 函数会读取用户输入的字符,%c 符号表示把输入的字符赋给变量 ch。接着,printf() 函数打印 ch 的值两次,第一次打印一个字符(对应代码中的 %c),第二次打印一个十进制整数值(对应代码中的 %d)。注意,printf() 函数中的转换说明决定了数据的显示方式,而不是数据的储存方式(见图 3.6)。

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 位的存储空间足够了。

        程序通过布尔值可以选择执行哪部分代码。我们将在第 6 章和第 7 章中详述相关内容。

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

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

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

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

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

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

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

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

        程序清单 3.6         altnames.c 程序

/* altnames.c   --  可移植整数类型名 */
// 这个文件展示了如何使用<inttypes.h>中定义的可移植整数类型名
#include <stdio.h>
#include <inttypes.h> // 支持可移植类型定义,例如int32_t和格式化宏如PRId32

int main(void)
{
    int32_t me32; // me32 是一个 32 位有符号整型变量
    me32 = 45933945; // 给me32赋一个值
    
    printf("首先,假设int32_t是int类型: ");
    printf("me32 = %d\n", me32); // 使用%d格式化me32
    
    printf("接下来,我们不做任何假设。\n");
    printf("相反,我们使用来自inttypes.h的\"字符串宏\"来格式化: ");

    // 使用PRId32宏来确保me32以正确的格式(32位有符号整数)被打印
    // PRId32是一个字符串宏,展开后用于printf中格式化32位有符号整数
    printf("me32 = %"PRId32"\n", me32);
    
    return 0;
}

        该程序最后一个 printf() 中,参数 PRId32 被定义在 inttypes.h 中的 "d" 替换,因而这条语句等价于:

printf("me16 = %" "d" "\n", me16);

        在 C 语言中,可以把多个连续的字符串组合成一个字符串,所以这条语句又等价于:

printf("me16 = %d\n", me16);

        下面是该程序的输出,注意,程序中使用了 \" 转义序列来显示双引号:

        篇幅有限,无法介绍扩展的所有整数类型。本节主要是为了让读者知道,在需要时可进行这种级别的类型控制。附录 B 中的参考资料 VI“扩展的整数类型”介绍了完整的 inttypes.h 和 stdint.h 头文件。

注意        对C99/C11 的支持

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

3.4.6 float、double 和 long double

        各种整数类型对大多数软件开发项目而言够用了。然而,面向金融和数学的程序经常使用浮点数。C 语言中的浮点类型有 float、double 和 long double 类型。它们与 FORTRAN 和 Pascal 中的 real 类型一致。前面提到过,浮点类型能表示包括小数在内更大范围的数。浮点数的表示类似于科学记数法(即用小数乘以 10 的幂来表示数字)。该记数系统常用于表示非常大或非常小的数。表 3.3 列出了一些示例。

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

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

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

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

1. 声明浮点型变量

        浮点型变量的声明和初始化方式与整型变量相同,下面是一些例子:

float noah, jonah;
double trouble;
float planck = 6.63e-34;
long double gnp;

2. 浮点型常量

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

-1.56E+12
2.87e-3

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

3.14159
.2  // 等价于 0.2
4e16 // 表示4 * 10^16
.8E-5  // 表示 0.8 * 10^-5,也就是0.000008
100.   // 表示 100.0

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

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

some = 4.0 * 2.0;

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

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

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

0xa.1fp10

        十六进制 a 等于十进制 10,.1f 是 1/16 加上 15/256(十六进制 f 等于十进制 15),p10 是 2^10 或 1024。0xa.1fp10 表示的值是 (10 + 1/16 + 15/256) × 1024(即,十进制 10364.0)。

        注意,并非所有的编译器都支持 C99 的这一特性。

3. 打印浮点值

        printf() 函数使 %f 转换说明打印十进制记数法的 float double 类型浮点数%e 打印指数记数法的浮点数。如果系统支持十六进制格式的浮点数,可用 a 和 A 分别代替 e 和 E。打印 long double 类型要使用 %Lf、%Le 或 %La 转换说明。给那些未在函数原型中显式说明参数类型的函数(如,printf())传递参数时,C 编译器会把 float 类型的值自动转换成 double 类型。程序清单 3.7 演示了这些特性。

        程序清单 3.7         showf_pt.c 程序

/* showf_pt.c     --     以两种方式显示 float 类型的值 */
#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);
   
    // 下一行要求编译器支持 C99 或其中的相关特性
    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;
}

        该程序的输出如下,前提是编译器支持 C99/C11:

32000.000000 can be written 3.200000e+004
And it's 0x1.f40000p+14 in hexadecimal, powers of 2 notation
2140000000.000000 can be written 2.140000e+009
0.000053 can be written 5.320000e-05

4. 浮点值的上溢和下溢

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

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

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

        当除以一个很小的数时,情况更为复杂。回忆一下,float 类型的数以指数和尾数部分来存储。存在这样一个数,它的指数部分是最小值,即由全部可用位表示的最小尾数值。该数字是 float 类型能用全部精度表示的最小数字。现在把它除以 2。通常,这个操作会减小指数部分,但是假设的情况中,指数已经是最小值了。所以计算机只好把尾数部分的位向右移,空出第 1 个二进制位,并丢弃最后一个二进制数。以十进制为例,把一个有 4 位有效数字的数(如,0.1234E-10)除以 10,得到的结果是 0.0123E-10。虽然得到了结果,但是在计算过程中却损失了原末尾有效位上的数字。这种情况叫作下溢(underflow)。C 语言把损失了类型全精度的浮点值称为低于正常的(subnormal)浮点值。因此,把最小的正浮点数除以 2 将得到一个低于正常的值。如果除以一个非常大的值,会导致所有的位都为 0。现在,C 库已提供了用于检查计算是否会产生低于正常值的函数。

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

浮点数舍入错误
        给定一个数,加上 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 类型的精度足够进行这样的计算。

浮点数表示法
        上一个方框中列出了由于计算机使用的系统不同,一个程序有不同的输出。原因是,根据前面介绍的知识,实现浮点数表示法的方法有多种。为了尽可能地统一实现,电子和电气工程师协会 (IEEE) 为浮点数计算和表示法开发了一套标准。现在,许多硬件浮点单元都采用该标准。2011 年,该标准被 ISO/IEC/IEEE 60559:2011 标准收录。该标准作为 C99 和 C11 的可选项,符合硬件要求的平台可开启。floaterr.c 程序的第三个输出示例即是支持该浮点标准的系统显示的结果。支持 C 标准的编译器还包含捕获异常问题的工具。详见附录 B.5,参考资料 V。

3.4.7 复数和虚数类型

        许多科学和工程计算都要用到复数和虚数。C99 标准支持复数类型和虚数类型,但是有所保留。一些独立实现,如嵌入式处理器的实现,就不需要使用复数和虚数(VCR 芯片就不需要复数)。一般而言,虚数类型都是可选项。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 的平方根。

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

        下面的代码了解即可:

#include <stdio.h>
#include <complex.h>

int main() {
    // 定义复数变量
    double complex z1 = 1.0 + 2.0 * I; // 复数 1 + 2i
    float complex z2 = 3.0f + 4.0f * I; // 复数 3 + 4i(float 类型)
    long double complex z3 = 5.0L + 6.0L * I; // 复数 5 + 6i(long double 类型)
    
    // 输出复数
    printf("z1 = %.1f + %.1fi\n", creal(z1), cimag(z1));
    printf("z2 = %.1f + %.1fi\n", crealf(z2), cimagf(z2)); // 注意使用 crealf 和 cimagf 来处理 float complex
    printf("z3 = %.1Lf + %.1Lfi\n", creall(z3), cimagl(z3)); // 注意使用 creall 和 cimagl 来处理 long double complex
    
    // 复数运算示例
    double complex sum = z1 + z2;
    double complex product = z1 * z2;
    
    // 输出结果
    printf("Sum: %.1f + %.1fi\n", creal(sum), cimag(sum));
    printf("Product: %.1f + %.1fi\n", creal(product), cimag(product));
    
    return 0;
}

        代码运行结果如下:

3.4.8 其他类型

        现在已经介绍完 C 语言的所有基本数据类型。有些人认为这些类型实在太多了,但有些人觉得还不够用。注意,虽然 C 语言没有字符串类型,但也能很好地处理字符串。第 4 章将详细介绍相关内容。

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

小结: 基本数据类型
        关键字:
        基本数据类型由 11 个关键字组成: int、long、short、unsigned、char、float、double、signed、_Bool、_Complex 和 _Imaginary。

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

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

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

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

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

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


        布尔类型:
        布尔值表示 true 和 false。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

小结: 如何声明简单变量
        1. 选择需要的类型。
        2. 使用有效的字符给变量起一个变量名。
        3. 按以下格式进行声明:

类型说明符 变量名;

        类型说明符由一个或多个关键字组成。下面是一些示例:

int interest;
unsigned short cash;

        4. 可以同时声明相同类型的多个变量,用逗号分隔各变量名,如下所示:

char ch, init, ans;

        5. 在声明的同时还可以初始化变量:

float mass = 6.0E24;

3.4.9 类型大小

        如何知道当前系统的指定类型的大小是多少?运行程序清单 3.8,会列出当前系统的各类型的大小。

        程序清单 3.8         typesize.c 程序

/* 本程序中数据类型的大小是在 64 位系统运行该程序得到的结果 */
#include <stdio.h>

int main(void) {
    // 打印有符号整数类型的大小
    printf("Type int has a size of %zd bytes.\n", sizeof(int)); // 4 byte
    printf("Type short has a size of %zd bytes.\n", sizeof(short)); // 2 byte
    printf("Type long has a size of %zd bytes.\n", sizeof(long)); // 4 byte
    printf("Type long long has a size of %zd bytes.\n", sizeof(long long)); // 8 byte
    
    // 打印无符号整数类型的大小
    printf("Type unsigned int has a size of %zd bytes.\n", sizeof(unsigned int)); // 4 byte
    printf("Type unsigned short has a size of %zd bytes.\n", sizeof(unsigned short)); // 2 byte
    printf("Type unsigned long has a size of %zd bytes.\n", sizeof(unsigned long)); // 4 byte
    printf("Type unsigned long long has a size of %zd bytes.\n", sizeof(unsigned long long)); // 8 byte
    
    // 打印浮点数类型的大小
    printf("Type float has a size of %zd bytes.\n", 
           sizeof(float)); // 4 byte
    printf("Type double has a size of %zd bytes.\n", 
           sizeof(double)); // 8 byte
    printf("Type long double has a size of %zd bytes.\n", 
           sizeof(long double)); // 16 byte
    
    // 打印字符类型的大小(char 在大多数环境中通常是8位,即1字节)
    printf("Type char has a size of %zd bytes.\n",
           sizeof(char)); // 1 byte
    
    return 0;
}

        sizeof 是 C 语言的内置运算符,以字节为单位给出指定类型的大小。C99 和 C11 提供 %zd 转换说明匹配 sizeof 的返回类型。一些不支持 C99 和 C11 的编译器可用 %u 或 %lu 代替 %zd。

注意

        sizeof 操作符的结果类型是size_t,在 C99 及之后的版本中,推荐使用 %zu 格式说明符来打印 size_t 类型的值。
        然而,%zd 通常用于 ssize_t 类型,它是 signed 版本的 size_t,但在实际编程中,使用 %zu 来打印 sizeof 的结果更为常见和准确。

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

        顺带一提,注意该程序最后几行 printf() 语句都被分为两行,只要不在引号内部或一个单词中间断行,就可以这样写

3.5 使用数据类型

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

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

        与 Pascal 相比,C 在检查类型匹配方面不太严格。C 编译器甚至允许二次初始化,但在激活了较高级别警告时,会给出警告。最好不要养成这样的习惯。

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

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

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

        许多程序员和公司内部都有系统化的命名约定,在变量名中体现其类型。例如,用 i_ 前缀表示 int 类型,us_ 前缀表示 unsigned short 类型。这样,一眼就能看出来 i_smart 是 int 类型的变量,us_versmart 是 unsigned short 类型的变量。

3.6 参数和陷阱

        有必要再次提醒读者注意 printf() 函数的用法。读者应该还记得,传递给函数的信息被称为参数。例如,printf("Hello,pal.") 函数调用有一个参数:“Hello, pal.”。双引号中的字符序列(如,“Hello, pal.”)被称为字符串(string),第 4 章将详细讲解相关内容。现在,关键是要理解无论双引号中包含多少个字符和标点符号,一个字符串就是一个参数。

        与此类似,scanf ("%d", &weight) 函数调用有两个参数:“%d” 和 &weight。C 语言用逗号分隔函数中的参数printf() 和 scanf() 函数与一般函数不同,它们的参数个数是可变的。例如,前面的程序示例中调用过带一个、两个,甚至三个参数的 printf() 函数。程序要知道函数的参数个数才能正常工作。printf() 和 scanf() 函数用第 1 个参数表明后续有多少个参数,即第 1 个字符串中的转换说明与后面的参数一一对应。例如,下面的语句有两个 %d 转换说明,说明后面还有两个参数:

printf("%d cats ate %d cans of tuna\n", cats, cans);

        后面的确还有两个参数:cats 和 cans。

        程序员要负责确保转换说明的数量、类型与后面参数的数量、类型相匹配。现在,C 语言通过函数原型机制检查函数调用时参数的个数和类型是否正确。但是,该机制对 printf() 和 scanf() 不起作用,因为这两个函数的参数个数可变。如果参数在匹配上有问题,会出现什么情况?假设你编写了程序清单 3.9 中的程序。

        程序清单         3.9 badcount.c 程序

/* badcount.c   --  参数错误的情况 */
#include <stdio.h>

int main(void)
{
    int n = 4;
    int m = 5;
    float f = 7.0f;
    float g = 8.0f;
    
    printf("%d\n", n, m); /* 参数太多 */
    printf("%d %d %d\n", n); /* 参数太少 */
    printf("%d %d \n", f, g); /* 值的类型不匹配 */
    
    return 0;
}

        CLion 的输出如下:

        注意,用 %d 显示 float 类型的值,其值不会被转换成 int 类型。在不同的平台下,缺少参数或参数类型不匹配导致的结果不同。

        所有编译器都能顺利编译并运行该程序,但其中大部分会给出警告。的确,有些编译器会捕获到这类问题,然而 C 标准对此未作要求。因此,计算机在运行时可能不会捕获这类错误。如果程序正常运行,很难觉察出来。如果程序没有打印出期望值或打印出意想不到的值,你才会检查 printf() 函数中的参数个数和类型是否得当。

3.7 转义序列示例

        再来看一个程序示例,该程序使用了一些特殊的转义序列。程序清单 3.10 演示了退格 ( \b )、水平制表符 ( \t ) 和回车 ( \r ) 的工作方式。这些概念在计算机使用电传打字机作为输出设备时就有了,但是它们不一定能与现代的图形接口兼容。例如,程序清单 3.10 在某些 Macintosh 的实现中就无法正常运行。

        程序清单         3.10 escape.c 程序

/* 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;
}

3.7.1 程序运行情况

        假设在系统中运行的转义序列行为与本章描述的行为一致(实际行为可能不同。例如,XCode 4.6 把 \a、\b 和 \r 显示为颠倒的问号),下面我们来分析这个程序。

        第 1 条 printf() 语句(注释中标为 1)发出一声警报(因为使用了 \a ),然后打印下面的内容:

Enter your desired monthly salary:

        因为 printf() 中的字符串末尾没有 \n,所以光标停留在冒号后面。

        第 2 条 printf() 语句在光标处接着打印,屏幕上显示的内容是:

Enter your desired monthly salary: $_______

        冒号和美元符号之间有一个空格,这是因为第 2 条 printf() 语句中的字符串以一个空格开始。7 个退格字符使得光标左移 7 个位置,即把光标移至 7 个下划线字符的前面,紧跟在美元符号后面。通常,退格不会擦除退回所经过的字符,但有些实现是擦除的,这和本例不同。

        假设键入的数据是 4000.00(并按下 Enter 键),屏幕显示的内容应该是:

Enter your desired monthly salary: $4000.00

        键入的字符替换了下划线字符。按下 Enter 键后,光标移至下一行的起始处。

        第 3 条 printf() 语句中的字符串以 \n\t 开始。换行字符使光标移至下一行起始处。水平制表符使光标移至该行的下一个制表点,一般是第 9 列(但不一定,CLion是 4 列,可以设置)。然后打印字符串中的其他内容。执行完该语句后,此时屏幕显示的内容应该是:

Enter your desired monthly salary: $4000.00
    $4000.00 a month is $48000.00 a year.

        因为这条 printf() 语句中没有使用换行字符,所以光标停留在最后的点号后面。

        第 4 条 printf() 语句以 \r 开始。这使得光标回到当前行的起始处。然后打印 Gee!,接着 \n 使光标移至下一行的起始处。屏幕最后显示的内容应该是:

Enter your desired monthly salary: $4000.00
GEE! $4000.00 a month is $48000.00 a year.

        在 CLion 中过程却不是这样,结果是:

3.7.2 刷新输出

        printf() 何时把输出发送到屏幕上?最初,printf() 语句把输出发送到一个叫作缓冲区(buffer)的中间存储区域,然后缓冲区中的内容再不断被发送到屏幕上。C 标准明确规定了何时把缓冲区中的内容发送到屏幕:当缓冲区满、遇到换行字符或需要输入的时候(从缓冲区把数据发送到屏幕或文件被称为刷新缓冲区)。例如,前两个 printf() 语句既没有填满缓冲区,也没有换行符,但是下一条 scanf() 语句要求用户输入,这迫使 printf() 的输出被发送到屏幕上。旧式编译器遇到 scanf() 也不会强行刷新缓冲区,程序会停在那里不显示任何提示内容,等待用户输入数据。在这种情况下,可以使用换行字符刷新缓冲区。代码应改为:

printf("Enter your desired monthly salary:\n");
sacnf("%d",&salary);

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

CLion 只能使用 fflush(stdout);  确保前面的输出立即显示

3.8 关键概念

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

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

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

3.9 本章小结

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

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

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

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

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

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

3.10 复习题

1. 指出下面各种数据使用的合适数据类型(有些可使用多种数据类型);

        a. East Simpleton 的人口

        b. DVD 影碟的价格

        c. 本章出现次数最多的字母

        d. 本章出现次数最多的字母次数

答:

        a. int 类型,也可以是 short 类型或 unsigned short 类型。人口数是一个整数。

        b. float 类型,价格通常不是一个整数(也可以使用 double 类型,但实际上不需要那么高的精度)。

        c. char类型。

        d. int 类型,也可以是 unsigned 类型。


2. 在什么情况下要用 long 类型的变量代替 int 类型的变量?

答:原因之一:在系统中要表示的数超过了 int 可表示的范围,这时要使用 long 类型。原因之二:如果要处理更大的值,那么使用一种在所有系统上都保证至少是 32 位的类型,可提高程序的可移植性。


3. 使用哪些可移植的数据类型可以获得 32 位有符号整数?选择的理由是什么?

答:如果要正好获得 32 位的整数,可以使用 int32_t 类型。要获得可储存至少 32 位整数的最小类型,可以使用 int_least32_t 类型。如果要为 32 位整数提供最快的计算速度,可以选择 int_fast32_t 类型(假设你的系统已定义了上述类型)。


4. 指出下列常量的类型和含义(如果有的话):
        a. '\b'

        b. 066

        c. 99.44

        d. 0xAA

        e. 2.0e30

答:

        a. char类型常量(但是储存为 int 类型)

        b. int 类型常量

        c. double 类型常量

        d. unsigned int 类型常量,十六进制格式

        e. double 类型常量


5. Dottie Cawm 编写了一个程序,请找出程序中的错误。

include <stdio.h>
main
(
    float g; h;
    float tax, rate;

    g = e21;
    tax = rate*g ;
)

答:

        第 1 行:应该是 #include <stdio.h>

        第 2 行:应该是 int main (void)

        第 3 行:把 (  改为 {

        第 4 行:g 和 h 之间的 ; 改成 ,

        第 5 行:没问题

        第 6 行:没问题

        第 7 行:虽然这数字比较大,但在 e 前面应至少有一个数字,如 1e21 或 1.0e21 都可以。

        第 8 行:没问题,至少没有语法问题。

        第 9 行:把 ) 改成 }

        除此之外,还缺少一些内容。首先,没有给 rate 变量赋值;其次未使用 h 变量;而且程序不会报告计算结果。虽然这些错误不会影响程序的运行(编译器可能给出变量未被使用的警告),但是它们确实与程序设计的初衷不符合。另外,在该程序的末尾应该有一个 return 语句。

下面是一个正确的版本,仅供参考:

#include <stdio.h>

int main (void) 
{
    float g, h;
    float tax, rate;
    
    rate = 0.08;
    g = 1.0e5;
    tax = rate * g;
    h = g + tax;
    
    printf("You owe $%f plus $%f in taxes for a total of $%f.\n", g, tax, h);
    
    return 0;
}

6. 写出下列常量在声明中使用的数据类型和在 printf() 中对应的转换说明:


7. 写出下列常量在声明中使用的数据类型和在 printf() 中对应的转换说明(假设 int 为 16 位):


8. 假设程序的开头有下列声明:

int imate = 2;
long shot = 53456;
char grade = 'A';
float log = 2.71828;

把下面 printf() 语句中的转换字符补充完整:

printf("The odds against the %__ were %__ to 1.\n", imate, shot);
printf("A score of %__ is not an %__ grade.\n", log, grade);

答:

printf("The odds against the %d were %ld to 1.\n", imate, shot);
printf("A score of %f is not an %c grade.\n", log, grade);

9. 假设 ch 是 char 类型的变量。分别使用转义序列、十进制值、八进制字符常量和十六进制字符常量把回车字符赋给 ch(假设使用 ASCII 编码值)。

答:

ch = '\r';
ch = 13;
ch = '\015';
ch = '\xd';

10. 修正下面的程序(在 C 中,/ 表示除以)。

void main (int) / this program is perfect /
 {  
    cows, legs integer;
    printf("How many cow legs did you count?\n);
    scanf("%d", legs);
    cows = legs / 4;
    printf("That implies there are %f cows.\n", cows);
}

答:

        最前面缺少一行(第 0 行):#include <stdio.h>

        第 1 行:void main (int) 中 void 和 int 顺序错误;使用 /* 和 */ 把注释括起来,或者在注释前面使用 //。

        第 3 行:声明变量错误:应该是  int cows, legs;

        第 4 行:差双引号结束符:应该是   count?\n");

        第 5 行:把 %c 改为 %d,把 legs 改为 &legs。

        第 7 行:把 %f 改为 %d。

        另外,在程序末尾还要加上 return 语句。下面是修改后的版本:

#include <stdio.h>

int main (void) {    /* this program is perfect */
    int cows, legs;
    printf("How many cow legs did you count?\n");
    scanf("%d", &legs);
    cows = legs / 4;
    printf("That implies there are %d cows.\n", cows);
    return 0;
}

11. 指出下列转义序列的含义:

        a. \n

        b. \\

        c. \"

        d. \t

答:

        a. 换行字符

        b. 反斜杠字符

        c. 双引号字符

        d. 制表字符


3.11 编程练习

1. 通过试验(即编写带有此类问题的程序)观察系统如何处理整数上溢、浮点数上溢和浮点数下溢的情况。

#include <stdio.h>
#include <limits.h>
#include <math.h>

int main() {
    // 整数上溢示例(使用有符号整数以更明显地看到溢出)
    int int_overflow = INT_MAX; // 假设是 2147483647 (2^31 - 1)
    printf("Before int overflow: %d\n", int_overflow);
    int_overflow++; // 尝试上溢
    printf("After int overflow: %d\n", int_overflow); // 注意:这里的行为是未定义的,但通常会是负值
    
    // 浮点数上溢示例
    float float_overflow = 3.40282347e+38F; // 接近但小于FLT_MAX的值
    printf("Before float overflow: %e\n", float_overflow);
    float_overflow *= 10.0; // 上溢
    if (float_overflow == INFINITY) {
        printf("After float overflow: INFINITY\n");
    } else {
        printf("After float overflow: %e\n", float_overflow);
    }
    
    // 浮点数下溢示例
    float float_underflow = 1.17549435e-38F; // 接近但大于FLT_MIN的值
    printf("Before float underflow: %e\n", float_underflow);
    float_underflow *= 0.0000000000000000000001; // 下溢
    if (float_underflow == 0.0) {
        printf("After float underflow: 0.0\n");
    } else {
        printf("After float underflow: %e\n", float_underflow);
    }
    
    return 0;
}

输出结果:


2. 编写一个程序,要求提示输入一个 ASCII 码值(如,66),然后打印输入的字符。

#include <stdio.h>

int main(void)
{
    int ascii;
    
    printf("Enter an ASCII code: ");
    scanf("%d", &ascii);
    printf("%d is the ASCII code for %c.\n", ascii, ascii);
    return 0;
}

3. 编写一个程序,发出一声警报,然后打印下面的文本:

        startled by the sudden sound, sally shouted,

        "By the Great Pumpkin, what was that!"

#include <stdio.h>

int main(void)
{
    printf("\astartled by the sudden sound, sally shouted,\nBy the Great Pumpkin, what was that!");
    return 0;
}

4. 编写一个程序,读取一个浮点数,先打印成小数点形式,再打印成指数形式。然后,如果系统支持,再打印成 p 记数法(即十六进制记数法)。按以下格式输出(实际显示的指数位数因系统而异):
        Enter a floating-point value: 64.25
        fixed-point notation: 64.250000
        exponential notation: 6.425000e+01
        p notation: 0x1.01p+6

#include <stdio.h>

int main(void)
{
    float num;
    
    printf("Enter a floating-point value: ");
    scanf("%f", &num);
    
    printf("fixed-point notation: %f\n", num);
    printf("exponential notation: %e\n", num);
    printf("p notation: %a\n", num);
    return 0;
}

5. 一年大约有 3.156 × 10^7 秒。编写一个程序,提示用户输入年龄,然后显示该年龄对应的秒数。

#include <stdio.h>

int main() {
    double secondsInAYear = 3.156e7; // 使用科学计数法表示一年中的秒数
    double userAge, totalSeconds;
    
    // 提示用户输入年龄
    printf("请输入您的年龄: ");
    scanf("%lf", &userAge); // 使用%lf来读取double类型的输入
    
    // 计算年龄对应的秒数
    totalSeconds = userAge * secondsInAYear;
    
    // 显示结果
    printf("%.2f 岁对应的秒数是: %.2e\n", userAge, totalSeconds);
    
    return 0;
}

6. 1 个水分子的质量约为 3.0 × 10^-23 克。1 夸脱水大约是 950 克。编写一个程序,提示用户输入水的夸脱数,并显示水分子的数量。

#include <stdio.h>  
int main(void)  
{  
    float mass_mol = 3.0e-23; /* 水分子的质量,单位:克 */  
    float mass_qt = 950; /* 1夸脱水的质量,单位:克 */  
    float quarts; /* 用户输入的夸脱数 */  
    float molecules; /* 计算得到的水分子数量 */  
      
    printf("请输入水的夸脱数: ");  
    scanf("%f", &quarts);  
    // 计算水分子数量:总质量(夸脱水数 * 每夸脱水质量)除以每个水分子的质量  
    molecules = quarts * mass_qt / mass_mol;  
    // 输出结果  
    printf("%.2f 夸脱水包含 %e 个水分子。\n", quarts, molecules);  
    return 0;  
}

7. 1 英寸相当于 2.54 厘米。编写一个程序,提示用户输入身高(英寸),然后以厘米为单位显示身高。

#include <stdio.h>

int main(void) {
    float heightInInches, heightInCentimeters;
    
    // 提示用户输入身高(英寸)
    printf("请输入您的身高(英寸): ");
    scanf("%f", &heightInInches);
    
    // 将英寸转换为厘米
    heightInCentimeters = heightInInches * 2.54;
    
    // 显示结果
    printf("%.2f 英寸对应的身高是 %.2f 厘米。\n", heightInInches, heightInCentimeters);
    
    return 0;
}

8. 在美国的体积测量系统中,1 品脱等于 2 杯,1 杯等于 8 盎司,1 盎司等于 2 大汤勺,1 大汤勺等于 3 茶勺。编写一个程序,提示用户输入杯数,并以品脱、盎司、汤勺、茶勺为单位显示等价容量。思考对于该程序,为何使用浮点类型比整数类型更合适?

#include <stdio.h>

int main()
{
    float cups, pints, ounces, tablespoons, teaspoons;
    
    // 提示用户输入杯数
    printf("请输入杯数: ");
    scanf("%f", &cups);
    
    // 执行转换
    pints = cups / 2.0;
    ounces = cups * 8.0;
    tablespoons = ounces * 2.0;
    teaspoons = tablespoons * 3.0;
    
    // 显示结果
    printf("%.2f 杯等于 %.2f 品脱, %.0f 盎司, %.0f 汤勺, %.0f 茶勺。\n",
           cups, pints, ounces, tablespoons, teaspoons);
    
    return 0;
}

对于这个程序,使用浮点类型(如 float)比整数类型更合适的原因主要是因为体积单位的转换涉及到小数部分。

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thanks_ks

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值