7.3.4 转义序列 🚀
然而,一些特殊符号(比如换行符)是无法采用上述方式书写的,因为它们不可见(非打印字符),或者无法从键盘输入。因此,为了使程序可以处理字符集中的每一个字符,C语言提供了一种特殊的 表示法——转义序列 ( escape sequence ).。
转义序列共有两种: 字符转义序列 ( character escape )和 数字转义序列 (numeric escape )。
字符转义序列使用起来很容易,但是它们有一个问题:转义序列列表没有包含所有无法打印的ASCII 字符,只包含了最常用的字符。字符转义序列也无法用于表示基本的 128 个 ASCII 字符以外的字符。数字转义序列可以表示任何 字符,所以它可以解决上述问题。
为了把特殊字符书写成数字转义序列,首先需要在类似附录E那样的表中查找字符的八进制或十六进制值。例如,某个ASCII码转义字符(十进制值为27)对应的八进制值为33,对应的十六进制值为1B。上述八进制或十六进制码可以用来书写转义序列。
7.3.6 用 scanf 和 printf 读/写字符 🚀
转换说明%c允许scanf函数和printf函数对单个字符进行读/写操作:
7.3.7 用 getchar 和 putchar 读/写字符 🚀
C语言还提供了另外一些读/写单个字符的方法。特别是,可以使用getchar函数和
putchar函数来取代scanf函数和printf函数。putchar函数用于写单个字符:
7.4 类型转换 🚀
C 语言则允许在表达式中混合使用基本类型。在单个表达式中可以组合整数、浮点数,甚至是字符。当然,在这种情况下 C 编译器可能需要生成一些指令将某些操作数转换成不同类型,使得硬件可以对表达式进行计算。例如,如果对16 位 short 型数和 32 位 int 型数进行加法操作,那么编译器将安排把16 位 short 型值转换成 32 位值。如果是 int 型数据和 float 型数据进行加法操作,那么编译器将安排把int 型值转换成为 float 格式。这个转换过程稍微复杂一些,因为 int型值和float 型值的存储方式不同。
因为编译器可以自动处理这些转换而无需程序员介入,所以这类转换称为 隐式转换(implicit conversion )。 C 语言还允许程序员使用强制运算符执行 显式转换 ( explicitconversion)。
当发生下列情况时会进行隐式转换。
7.4.1 常用算术转换 🚀
7.4.2 赋值过程中的转换 🚀
常用算术转换不适用于赋值运算。C 语言会遵循另一条简单的转换规则,那就是把赋值运算右边的表达式转换成左边变量的类型。如果变量的类型至少和表达式类型一样“宽”,那么这种转换将没有任何障碍。
7.4.4 强制类型转换 🚀
7.5.2 类型定义和可移植性 🚀
C语言库自身使用typedef为那些可能依据C语言实现的不同而不同的类型创建类型名;这
些类型的名字经常以_t结尾,比如ptrdiff_t、size_t和wchar_t。这些类型的精确定义不尽相同,下面是一些常见的例子:
在C99中,<stdint.h>头使用typedef定义占用特定位数的整数类型名。例如,int32_t是恰好占用32位的有符号整型。这是一种有效的定义方式,能使程序更易于移植。
7.6 sizeof 运算符 🚀
sizeof运算符允许程序存储指定类型值所需空间的大小。表达式
问与答 🚀
问:7.1节说到%o和%x分别用于以八进制和十六进制书写无符号整数。那么如何以八进制和十六进制书写普通的(有符号)整数呢?(p.92)答:只要有符号整数的值不是负值,就可以用 %o和%x 显示。这些转换导致 printf 函数把有符号整数看成是无符号的;换句话说,printf 函数将假设符号位是数的绝对值部分。只要符号位为 0 ,就没有问题。如果符号位为1 ,那么 printf 函数将显示出一个超出预期的大数。
问:但是,如果数是负数该怎么办呢?如何以八进制或十六进制形式书写它?
答:没有直接的方法可以书写负数的八进制或十六进制形式。幸运的是,需要这样做的情况非常少。当然,我们可以判定这个数是否是负数,然后自己显示一个负号:
问:浮点常量为什么存储成double格式而不是float格式?(p.94)
答:由于历史的原因, C语言更倾向于使用double类型,float 类型则被看成是“二等公民”。思考 Kernighan和Ritchie 的 The C Programming Language 一书中关于 float 的论述:“使用 float 类型的主要原因是节省大型数组的存储空间,或者有时是为了节省时间,因为在一些机器上双精度计算的开销格外大。” 经典C 要求所有浮点计算都采用双精度的格式。(C89和C99没有这样的要求。)
*问:十六进制的浮点常量是什么样子?使用这种浮点常量有什么好处?(p.94)
答:十六进制浮点常量以 0x或0X开头,且必须包含指数(指数跟在字母P或p后面)。指数可以有符号,常量可以以f、F、l或L结尾。指数以十进制数表示,但代表的是2的幂而不是10 的幂。例如, 0x1.Bp3表示1.6875×2 3 = 13.5 。十六进制位 B 对应的位模式为 1011 ;由于 B 出现在小数点的右边,所以其每一位代表一个2 的负整数幂,把它们(2^- 1 + ^- 3 + 2^- 4 )相加得到 0.6875 。
十六进制浮点常量主要用于指定精度要求较高的浮点常量(包括e 和 π 等数学常量)。十进制数具有精确的二进制表示,而十进制常量在转换为二进制时则可能会受到舍入误差的些许影响。十六进制数对于定义极值(例如<float.h> 头中宏的值)常量也是很有用的,这些常量很容易用十六进制表示但难以用十进制表示。
*问:为什么使用%lf读取double类型的值,而用%f进行显示呢?(p.94)
答:这是一个十分难回答的问题。首先,注意, scanf函数和printf 函数都是不同寻常的函数,因为它们都没有将函数的参数限制为固定数量。scanf 函数和 printf 函数有可变长度的参数列表( 26.1节)。当调用带有可变长度参数列表的函数时,编译器会安排float 参数自动转换成为 double 类型,其结果是printf 函数无法区分 float 类型和 double 类型的参数。这解释了在 printf 函数调用中为何可以用%f 既表示 float 类型又表示 double 类型的参数。
另一方面, scanf 函数是通过 指针 指向变量的。 %f 告诉 scanf 函数在所传地址位置上存储一个
float 类型值,而 %lf 告诉 scanf 函数在该地址上存储一个 double 类型值。这里 float和double
的区别是非常重要的。如果给出了错误的转换说明,那么 scanf 函数将可能存储错误的字节数量(没有提到的是,float 类型的位模式可能不同于 double 类型的位模式)。
问:什么时候需要考虑字符变量是有符号的还是无符号的?(p.96)
答:如果在变量中只存储7位的字符,那么不需要考虑,因为符号位将为零。但是,如果计划存储8位字符,那么将希望变量是unsigned char类型。思考下面的例子:
如果已经把变量 ch 声明成 char 类型,那么编译器可能选择把它看作是有符号的字符来处理(许多编译器这么做)。只要变量ch 只是作为字符来使用,就不会有什么问题。
但是如果 ch 用在一些需要编译器将其值转换为整数的上下文中,那么可能就有问题了:转换为整数的结果将是负数,因为变量ch的符号位为 1 。
还有另外一种情况:在一些程序中,习惯使用 char 类型变量存储单字节的整数。如果编写了这类程序,就需要决定每个变量应该是signed char 类型的还是 unsigned char 类型的,这就像需要决定普通整型变量应该是int 类型还是 unsigned int 类型一样。
问:我无法理解换行 ( new-line ) 符怎么会是 ASCII 码的回行 ( line-feed ) 符。当用户录入输入内容并且按回车键时,程序不会把它作为回车符或者回车加回行符读取吗?( p.97 )
答:不会的。作为 C语言的UNIX 继承部分,行的结束位置标记一直被作为单独的回行符来看待。(在 UNIX文本文件中,单独一个回行符(但不是回车符)会出现在每行的结束处。)C 语言函数库会把用户的按键翻译成回行符。当程序读文件时,输入/输出函数库将文件的行结束标记(不管它是什么)翻译成单个的回行符。与之相对应的反向转换发生在将输出往屏幕或文件中写的时候。(详见22.1节。)
虽然这些翻译可能看上去很混乱,但是它们都为了一个重要的目的:使程序不受不同操作系统的影响。
*问:使用转义序列\?的目的是什么?(p.97)
答:转义序列 \? 与三字符序列( 25.3 节)有关,因为三字符序列以 ?? 开头。如果需要在字符串中加入 ?? ,那么编译器很可能会把它误当作三字符序列的开始。用\? 代替第二个?可以解决这个问题。
问:既然getchar函数的读取速度更快,为什么仍然需要使用scanf函数读取单个的字符呢?(p.98)
答:虽然 scanf 函数没有 getchar 函数读取的速度快,但是它更灵活。正如前面已经看到的,格式串 "%c"可以使scanf函数读入下一个输入字符;" %c"则可以使scanf 函数读入下一个非空白字符。而且,scanf函数也很擅长读取混合了其他数据类型的字符。假设输入数据中包含有一个整数、一个单独的非数值型字符和另一个整数。通过使用格式串"%d%c%d" ,就可以利用 scanf 函数读取全部三项内容。
*问:在什么情况下,整值提升会把字符或短整数转换成unsigned int类型?(p.101)
答:如果 int类型整数没有大到足以包含所有可能的原始类型值,整值提升会产生unsigned int 类型。因为字符通常是8 位的长度,几乎总会转化为 int 类型(可以保证 int 类型至少为 16 位长度)。有符号短整数也总可以转换为int 类型,但无符号短整数却是有疑问的。如果短整数和普通整数的长度相同(例如在16 位机上),那么无符号短整数必须被转化为 unsigned int 类型,因为最大的无符号短整数(在16 位机上为 65 535 )要大于最大的 int 类型数(即 32 767 )。
问:如果把超出变量取值范围的值赋值给变量,究竟会发生什么?(p.102)
答:粗略地讲,如果值是整值类型并且变量是无符号类型,那么会丢掉超出的位数;如果变量是有符号类型,那么结果是由实现定义的。把浮点数赋值给整型或浮点型变量的话,如果变量太小而无法承受,会产生未定义的 行为:任何事情都可能发生,包括程序终止。
* 问:为什么 C 语言要提供类型定义呢?定义一个 BOOL 宏不是和用 typedef 定义一个 Bool 类型一样好用吗?( p.105 )
答:类型定义和宏定义存在两个重要的不同点。首先,类型定义比宏定义功能更强大。特别是,数组和指针类型是不能定义为宏的。假设我们试图使用宏来定义一个“指向整数的指针”类型:
可惜的是,只有p是指针,q和r都成了普通的整型变量。类型定义不会有这样的问题。
其次, typedef命名的对象具有和变量相同的作用域规则;定义在函数体内的typedef名字在函 数外是无法识别的。另一方面,宏的名字在预处理时会在任何出现的地方被替换掉。
问:你提到“编译器本身通常就能够确定sizeof表达式的值”。难道编译器不总能确定sizeof表达式的值吗?(p.106)
答:在 C89 中编译器总是可以的,但在 C99 中有一个例外。编译器不能确定变长数组( 8.3 节)的大小,因为数组中的元素个数在程序执行期间是可变的。