C 语言基本类型:问与答

本文内容

问:C 语言整数类型简介 中说到 %o%x 分别用于以八进制和十六进制书写无符号整数。那么如何以八进制和十六进制书写普通的(有符号)整数呢?

问:C 语言整数类型简介 中说到 %o%x 分别用于以八进制和十六进制书写无符号整数。那么如何以八进制和十六进制书写普通的(有符号)整数呢?

答:只要有符号整数的值不是负值,就可以用 %o%x 显示。这些转换导致 printf 函数把有符号整数看作无符号的;换句话说,printf 函数将假设符号位是数的绝对值部分。只要符号位为 0,就没有问题。如果符号位为 1,那么 printf 函数将显示出一个超出预期的大数。

问:但是,如果是负数该怎么办呢?如何以八进制或十六进制书写它?

答:没有直接的方法可以书写负数的八进制或十六进制形式。幸运的是,需要这样做的情况非常少。当然,我们可以判定这个数是否为负数,然后自己显示一个负号:

1
2
3
4
if (i < 0)
  printf("-%x", -i);
else
  printf("%x", i);

问:浮点常量为什么存储成 double 格式而不是 float 格式?

答:由于历史的原因,C 语言更倾向于使用 double 类型,float 类型则被看作次要的。思考 Kernighan 和 Ritchie 的 The C Programming Language 一书中关于 float 的论述:“使用 float 类型的主要原因是节省大型数组的存储空间,或者有时是为了节省时间,因为在一些机器上双精度计算的开销格外大。”经典 C 要求所有浮点计算都采用双精度的格式。(C89 和 C99 没有这样的要求。)

问:十六进制的浮点常量是什么样子?使用这种浮点常量有什么好处?

答:十六进制浮点常量以 0x0X 开头,且必须包含指数(指数跟在字母 Pp 后面)。指数可以有符号,常量可以以 fFlL 结尾。指数以十进制数表示,但代表的是 2 的幂而不是 10 的幂。例如,0x1.Bp3 表示 $1.6875\times2^3=13.5$。十六进制位 B 对应的位模式为 1011;因为 B 出现在小数点的右边,所以其每一位代表一个 2 的负整数幂,把它们$(2^{-1}+2^{-3}+2^{-4})$相加得到 0.6875。

十六进制浮点常量主要用于指定精度要求较高的浮点常量(包括 eπ 等数学常量)。十进制数具有精确的二进制表示,而十进制常量在转换为二进制时则可能受到舍入误差的些许影响。十六进制数对于定义极值(例如 <float.h> 头中宏的值)常量也是很有用的,这些常量很容易用十六进制表示,但难以用十进制表示。

问:为什么使用 %lf 读取 double 类型的值,却用 %f 显示它呢?

答:这是一个很难回答的问题。首先注意,scanf 函数和 printf 函数都是不同寻常的函数,因为它们都没有将函数的参数限制为固定数量。scanf 函数和 printf 函数有可变长度的参数列表。当调用带有可变长度参数列表的函数时,编译器会安排 float 参数自动转换成为 double 类型,其结果是 printf 函数无法区分 float 类型和 double 类型的参数。这解释了在 printf 函数调用中为何可以用 %f 既表示 float 类型又表示 double 类型的参数。

另外,scanf 函数是通过指针指向变量的。%f 告诉 scanf 函数在所传地址位置上存储一个 float 类型值,而 %lf 告诉 scanf 函数在该地址上存储一个 double 类型值。这里 floatdouble 的区别是非常重要的。如果给出了错误的转换说明,那么 scanf 函数将可能存储错误的字节数量(更不用说 float 类型的位模式可能不同于 double 类型的位模式)。

问:char 的正确发音是什么?

答:没有普遍接受的发音。一些人把 char 发音成“character”的第一个音节 [Kæ],还有一些人把它念成 [t∫a:(r)],就像在 char broiled;中那样。

问:什么时候需要考虑字符变量是有符号的还是无符号的?

答:如果在变量中只存储 7 位的字符,那么不需要考虑,因为符号位将为零。但是,如果计划存储 8 位字符,那么变量可能最好是 unsigned char 类型。思考下面的例子:

1
ch = '\xdb';

如果已经把变量 ch 声明成 char 类型,那么编译器可能选择把它看作有符号的字符来处理(许多编译器这么做)。只要变量 ch 仅作为字符来使用,就不会有什么问题。但是如果 ch 用在一些需要编译器将其值转换为整数的上下文中,那么可能就有问题了:转换为整数的结果将是负数,因为变量 ch 的符号位为 1。

还有另外一种情况:在一些程序中,习惯使用 char 类型变量存储单字节的整数。如果编写了这类程序,就需要决定每个变量应该是 signed char 类型还是 unsigned char 类型,这就像需要决定普通整型变量应该是 int 类型还是 unsigned int 类型一样。

问:我无法理解换行(new-line)符怎么会是 ASCII 码的回行(line-feed)符。当用户输入内容并且按下回车键时,程序不会把它作为回车符或者回车加回行符读取吗?

答:不会的。作为 C 语言的 UNIX 继承部分,行的结束位置标记一直被看作单独的回行符。[在 UNIX 文本文件中,单独一个回行符(但不是回车符)会出现在每行的结束处。]C 语言函数库会把用户的按键翻译成回行符。当程序读文件时,输入/输出函数库将文件的行结束标记(不管它是什么)翻译成单个的回行符。与之相对应的反向转换发生在将输出往屏幕或文件中写的时候。

虽然这些翻译可能看上去很混乱,但是它们都为了一个重要的目的:使程序不受不同操作系统的影响。

问:使用转义序列 \? 的目的是什么?

答:转义序列 \? 与三联序列有关,因为三联序列以 ?? 开头。如果需要在字符串中加入 ??,那么编译器很可能会把它误认为三联序列的开始。用 \? 代替第二个 ? 可以解决这个问题。

问:既然 getchar 函数的读取速度更快,为什么仍然需要使用 scanf 函数读取单个的字符呢?

答:虽然 scanf 函数没有 getchar 函数读取的速度快,但是它更灵活。正如前面已经看到的,格式串 "%c" 可以使 scanf 函数读入下一个输入字符," %c" 可以使 scanf 函数读入下一个非空白字符。而且,scanf 函数也很擅长读取混合了其他数据类型的字符。假设输入数据中包含一个整数、一个单独的非数值型字符和另一个整数。通过使用格式串 "%d%c%d" 就可以利用 scanf 函数读取全部三项内容。

问:在什么情况下,整值提升会把字符或短整数转换成 unsigned int 类型?

答:如果 int 类型整数没有大到足以包含所有可能的原始类型值,那么整值提升会产生 unsigned int 类型。因为字符的长度通常是 8 位,所以几乎总会转换为 int 类型(可以保证 int 类型至少为 16 位长度)。有符号短整数也总可以转换为 int 类型,但无符号短整数是有疑问的。如果短整数和普通整数的长度相同(例如在 16 位机上),那么无符号短整数必须被转换为 unsigned int 类型,因为最大的无符号短整数(在 16 位机上为 65 535)要大于最大的 int 类型数(即 32 767)。

问:如果把超出变量取值范围的值赋值给变量,究竟会发生什么?

答:粗略地讲,如果值是整值类型并且变量是无符号类型,那么会丢掉超出的位数;如果变量是有符号类型,那么结果是由实现定义的。把浮点数赋值给整型或浮点型变量的话,如果变量太小而无法承受,会产生未定义的行为:任何事情都可能发生,包括程序终止。

问:为什么 C 语言要提供类型定义呢?定义一个 BOOL 宏不是和用 typedef 定义一个 Bool 类型一样好用吗?

答:类型定义和宏定义存在两个重要的不同点。首先,类型定义比宏定义功能更强大。具体来说,数组和指针类型是不能定义为宏的。假设我们试图使用宏来定义一个“指向整数的指针”类型:

1
#define PTR_TO_INT int *

声明

1
PTR_TO_INT p, q, r;

在处理以后会变成

1
int * p, q, r;

可惜的是,只有 p 是指针,qr 都成了普通的整型变量。类型定义不会有这样的问题。

其次,typedef 命名的对象具有和变量相同的作用域规则;定义在函数体内的 typedef 名字在函数外是无法识别的。另外,宏的名字在预处理时会在任何出现的地方被替换。

问:本教程中提到“编译器本身通常就能够确定 sizeof 表达式的值”。难道编译器不总能确定 sizeof 表达式的值吗?

答:在 C89 中编译器总是可以的,但在 C99 中有一个例外。编译器不能确定变长数组的大小,因为数组中的元素个数在程序执行期间是可变的。

请参阅

(完)

comments powered by Disqus

本文内容