标准库

Perhaps if we wrote programs from childhood on, as adults we’d be able to read them. [^1]

目录

[TOC]

一 标准库的使用

C89 标准库总共划分成 15 个部分,每个部分用一个头描述。C99 新增了 9 个头,总共有 24 个。

C89
<assert.h> <locale.h> <stddef.h>
<ctype.h> <math.h> <stdio.h>
<errno.h> <setjmp.h> <stdlib.h>
<float.h> <signal.h> <string.h>
<limits.h> <stdarg.h> <time.h>
C99 新增
<complex.h> <stdint.h>
<fenv.h> <tgmath.h>
<inttype.h> <wchar.h>
<iso646.h> <wctype.h>
<stdbool.h>

大多数编译器都会使用更大的库,其中包含很多上表中没有的头。额外添加的头当然不属于标准库的范畴,所以我们不能假设其他的编译器也支持这些头。这类头通常提供些针对特定机型或特定操作系统的函数(这也解释了为什么它们不属于标准库),它们可能会提供允许对屏幕或键盘做更多控制的函数。用于支持图形或窗口界面的头也是很常见的。

标准头主要由函数原型、类型定义以及宏定义组成。如果我们的文件中调用了头中声明的函数,或是使用了头中定义的类型或宏,就需要在文件开头将相应的头包含进来。当一个文件包含多个标准头时,#include指令的顺序无关紧要。多次包含同一个标准头也是合法的。

1. 对标准库 中所用名字的限制

任何包含了标准头的文件都必须遵守两条规则。

  • 第一,该文件不能将头中定义过的宏的名字用于其他目的。例如,如果某个文件包含了<stdio.h>,就不能重新定义NULL了,因为使用这个名字的宏已经在<stdio.h>中定义过了。
  • 第二,具有文件作用域的库名(尤其是typedef名)也不可以在文件层次重定义。因此,一旦文件包含了<stdio.h>,由于<stdio.h>中已经将size_ t定义为typedef名,那么在文件作用域内都不能将size_ t重定义为任何标识符。

上述这些限制是显而易见的,但C语言还有一些其他的限制,可能是你想不到的。

  • 由一个下划线和一个大写字母开头或由两个下划线开头的标识符是为标准库保留的标识符。程序不允许为任何目的使用这种形式的标识符。

  • 由一个下划线开头的标识符被保留用作具有文件作用域的标识符和标记。除非在函数内部声明,否则不应该使用这类标识符。

  • 在标准库中所有具有外部链接的标识符被保留用作具有外部链接的标识符。特别是所有标准库函数的名字都被保留。因此,即使文件没有包含<stdio.h>,也不应该定义名为printf的外部函数,因为在标准库中已经有一个同名的函数了。这些规则对程序的所有文件都起作用,不论文件包含了哪个头。虽然这些规则并不总是强制性的,但不遵守这些规则可能会导致程序不具有可移植性。

上面列出的规则不仅适用于库中现有的名字,也适用于留作未来使用的名字。至于哪些名字是保留的,完整的描述太冗长了,你可以在C标准的 “future library directions" 中找到。例如,C保留了以str和一个小写字母开头的标识符,从而具有这类名字的函数就可以被添加到 <string.h> 头中。

2. 使用宏隐藏的函数

C 程序员经常会用带参数的宏来替代小的函数,这在标准库中同样很常见。C 标准允许在头中定义与库函数同名的宏,为了起到保护作用,还要求有实际的函数存在。因此,对于库的头,声明一个函数并同时定义一个有相同名字的宏的情况并不少见。

我们已经见过宏与库函数同名的例子。getchar 是声明在<stdio.h>中的库函数,它具有如下原型:

int getchar (void);

<stdio .h> 通常也把 getchar 定义为一一个宏:

#define getchar() getc (stdin)

在默认情况下,对 getchar 的调用会被看作宏调用(因为宏名会在预处理时被替换)。在大多数情况下,我们喜欢使用宏来替代实际的函数,因为这样可能会提高程序的运行速度。然而在某些情况下,我们可能需要的是一个真实的函数,可能是为了尽量缩小可执行代码的大小。

如果确实存在这种需求,我们可以使用#undef指令来删除宏定义。例如,我们可以在包含了<stdio.h>后删除宏getchar的定义:

#include <stdio.h>
#undef getchar

即使 getchar 不是宏,这样的做法也不会带来任何坏处,因为当给定的名字没有被定义成宏时,#undef指令不会起任何作用。

此外,我们也可以通过给名字加圆括号来禁用个别宏调用:

ch = (getchar)(); /* instead of ch= getchar(); */

三 C89 & C99 标准宏概状

大家可以下去自行去看一看上面标准头表中的头都是主要用来做什么的,它们都有什么函数原型,类型定义或宏定义。

四 了解两个简单的头

前面我们已经了解了 <string.h><stdio.h> 大部分函数和<stdlib.h>中的一些函数。现在开始,我们就要继续了解一些常用的头中的函数。今天我们要学习的头是<stddef.h><stdbool.h>

1. <stddef.h>常用定义

stddef.h 头提供了常用类型定义,但没有声明任何函数。定义的类型包括一下几个:

  • ptrdiff_t 当进行指针相减运算时,其结果的类型。
  • size_t sizeof 运算符返回的类型
  • wchar_t 一种足够强大的,可以用于表示所有支持的地区所有字符的类型。

stddef.h 头中还定义了两个宏。

  • 一个是 NULL,用来表示空指针。
  • 另一个宏是 offsetof需要两个参数:类型(结构类型)和成员指示符(结构的一个成员)。offsetof 会计算计算结构起点到指定成员间的字节数。

考虑下面的结构:

struct S {
char a;
int b[2];
float c;
}

offsetof(struct s, a)的值一定是0,C 语言确保结构的第一个成员的地址与结构自身地址相同。我们无法确定地说出 b 和 c 的偏移量是多少。一种可能是offsetof(structs, b)是1 (因为 a 的长度是一个字节), 而offsetof(struct s, c)是9 (假设整数是32位)。然而,一些编译器会在结构中留下一些空洞,从而会影响到 offsetof 产生的值。例如,如果编译器在a后面留下了3个字节的空洞,那么 b 和 c 的偏移量分别是4和12。但这正是offsetof宏的魅力所在:对任意编译器,它都能返回正确的偏移量,从而
使我们可以编写可移植的程序。

offsetof有很多用途。例如,假如我们需要将结构 s 的前两个成员写入文件,但忽略成员 c。我们不使用 fwrite 函数来写 sizeof(struct s)个字节,因为这样会将整个结构写入,而只要写offsetof(struct s, c)个字节。

最后一点:一些在 <stddef .h> 中定义的类型和宏在其他头中也会出现。(例如, NULL宏不仅在C99的头<wchar .h>中定义,在<locale.h>、<stdio.h>、<stdlib.h>、<string.h>和<time.h>中也有定义。)因此,只有少数程序真的需要包含<stddef .h>)

2. stdbool.h 布尔类型和值

stdbool.h 头定义了 4 个宏:

  • bool (定义为_Bool
  • true(定义为 1)
  • false(定义为 0)
  • __bool_true_false_are_defined(定义为 1)

在自己定义 bool,true,false 之前可以使用预处理指令(#if 或 #endif)来测试这个宏。

[^1]: 从童年开始写程序,长大了就能读懂了。Epigrams on Programming 编程警句

参考资料:《C语言程序设计:现代方法》