——每个程序都可以是其他程序的一部分,但很少是正合适的。
前面几章中零散地介绍了一些C
语言标准库的相关知识。本章将完整地讨论标准库。21.1节
列举使用库的一些通用的指导原则,并介绍了会在一些库的头中发现的技巧:使用宏来“隐藏”函数
。21.2节
会对C89
库的每个头分别做概述性介绍,21.3节
会对C99
库的新头做概述性介绍,21.4
节会对C11
库的新头做概括性介绍。
随后几章将深入讨论标准库的头,并将相关联的头放在一起讨论。其中
、
、
和
非常简短,所以会在本章中加以讨论(分别在21.5节
、21.6节
、21.7节
和21.8节
)。
C89
标准库总共划分成15
个部分,每个部分用一个头描述。C99
新增了9
个头,C11
新增了5
个头,总共有29
个:
从
C99
开始引入(9
个):
从
C11
开始引入(5
个):
大多数编译器会使用更大的库,其中包含很多上述列表中没有的头。额外添加的头当然不属于标准库的范畴
,因此不能假设其他的编译器也支持这些头。这类头通常提供一些针对特定机型或特定操作系统的函数(这也解释了为什么它们不属于标准库),它们可能会提供允许对屏幕或键盘做更多控制的函数。用于支持图形或窗口界面的头也是很常见的。
标准头主要由函数原型、类型定义以及宏定义组成。如果我们的文件中调用了头中声明的函数,或是使用了头中定义的类型或宏,就需要在文件开头将相应的头包含进来。当一个文件包含多个标准头时,#include
指令的顺序无关紧要。多次包含同一个标准头也是合法的。
任何包含了标准头的文件都必须遵守
2
条规则。
,就不能重新定义NULL
了,因为使用这个名字的宏已经在
中定义过了。typedef
名)也不可以在文件层次重定义。因此,一旦文件包含了
,由于
中已经将size_t
定义为typedef
名,在文件作用域内都不能将size_t
重定义为任何标识符。上述这些限制是显而易见的,但
C
语言还有一些其他的限制,可能是你想不到的:
,也不应该定义名为printf
的外部函数,因为在标准库中已经有一个同名的函数了。这些规则对程序的所有文件都起作用,不论文件包含了哪个头都是如此。虽然这些规则并不总是强制性的,但不遵守这些规则可能会导致程序不具有可移植性。
上面列出的规则不仅适用于库中现有的名字,也适用于留作未来使用的名字。至于哪些名字是保留的,完整的描述太冗长了,你可以在C
标准的“future library directions”
中找到。例如,C
保留了以str
和一个小写字母开头的标识符,使得具有这类名字的函数可以被添加到
头中。
C
程序员经常会用带参数的宏来替代小的函数,这在标准库中同样很常见。C
标准允许在头中定义与库函数同名的宏,为了起到保护作用,还要求有实际的函数存在。因此,对于库的头,声明一个函数并同时定义一个有相同名字的宏的情况并不少见。
我们已经见过宏与库函数同名的例子。getchar
是声明在
中的库函数,具有如下原型:
int getchar(void);
通常也把getchar
定义为一个宏:
#define getchar() getc(stdin)
在默认情况下,对getchar
的调用会被看作宏调用(因为宏名会在预处理时被替换)。
在大多数情况下,我们喜欢使用宏来替代实际的函数,因为这样可能会提高程序的运行速度。然而在某些情况下,我们需要一个真实的函数,可能是为了尽量缩小可执行代码的大小。
如果确实存在这种需求,可以使用#undef指令(14.3节)
来删除宏定义。例如,可以在包含
后删除宏getchar
的定义:
#include
#undef getchar
即使getchar
不是宏,这样的做法也不会带来任何坏处,因为当给定的名字没有被定义成宏时,#undef
指令不会起任何作用。
此外,也可以通过给名字加
圆括号
来禁用个别宏调用:
ch = (getchar)(); /* instead of ch = getchar(); */
预处理器无法分辨出带参数的宏,除非宏名后跟着一个左圆括号。编译器则不会这么容易被欺骗,它仍可以将getchar
识别为函数。
现在简单讨论一下
C89
标准库中的头。本节可以作为一张“路线图”,帮助你分辨出需要的是C
标准库的哪一部分。本章及后续各章节会对每个头做更详细的介绍:
:诊断(断言)
仅包含assert
宏,它允许我们在程序中插入自我检查。一旦任何检查失败,程序就会被终止。
:字符处理
提供用于字符分类及大小写转换的函数。
:错误
提供了errno(“error number”)
。errno
是一个左值(lvalue)
,可以在调用特定库函数后进行检测,从而判断调用过程中是否有错误发生。
:浮点类型的特性
)提供了用于描述浮点类型特性的宏,包括值的范围及精度。
:整数类型的大小
提供了用于描述整数类型(包括字符类型)特性的宏,包括它们的最大值和最小值。
:本地化
提供一些函数来帮助程序适应针对某个国家或地区的特定行为方式。这些与本地化相关的行为包括显示数的方式(如用作小数点的字符)、货币的格式(如货币符号)、字符集以及日期和时间的表示形式。
:数学计算
提供了常见的数学函数,包括三角函数、双曲函数、指数函数、对数函数、幂函数、邻近舍入函数、绝对值运算函数以及取余函数。
:非本地跳转
提供了setjmp
函数和longjmp
函数。setjmp
函数会“标记”
程序中的一个位置,随后可以用longjmp
返回被标记的位置。这些函数可以用来从一个函数跳转到另一个(仍然在活动中的)函数中,而绕过正常的函数返回机制。setjmp
函数和longjmp
函数主要用来处理程序执行过程中出现的严重问题。
:信号处理
提供了用于处理异常情况(信号)的函数,包括中断和运行时错误。signal
函数可以设置一个函数,使系统会在给定信号发生后自动调用该函数;raise
函数用来产生信号。
:可变参数
提供了一些工具用于编写参数个数可变的函数,就像printf
和scanf
函数一样。
:常用定义
提供了经常使用的类型和宏的定义。
:输入/输出
提供了大量的输入/输出函数,包括对顺序访问和随机访问文件的操作。
:常用实用程序
包含了大量无法归入其他头的函数。包含在
中的函数可以将字符串转换成数、产生伪随机数、执行内存管理任务、与操作系统通信、执行搜索与排序,以及在多字节字符与宽字符之间进行转换。
:字符串处理
提供了用于进行字符串操作(包括复制、拼接、比较及搜索)的函数以及对任意内存块进行操作的函数。
:日期和时间
提供相应的函数来获取时间(和日期)、操纵时间,以及格式化时间的显示。
C99
对标准库的改变主要分为以下三类:
C99
标准库中有9
个头是C89
中没有的。事实上其中3
个(
、
和
) 在1995
年修订C89
时就增加到C
中,另外6
个(
、
、
、
、
和
)是C99
新增的。C99
标准在一些已有的头中增加了宏和函数,这些头主要有
、
和
。
头中增加了非常多的内容,将专门用一节(即23.4节
)来讲述。printf
和scanf
)在C99
中具有了更多的功能。接下来快速浏览一下
C99
标准库中新增的9
个头,就像在21.2节
中浏览C89
库中的头一样:
:复数算术
定义了complex
和I
宏,这两个宏对于复数运算来说非常有用。该头还提供了对复数进行数学运算的函数。
:浮点环境
提供了对浮点状态标志和控制模式的访问。例如,程序可以测试标志来判断浮点数运算过程中是否发生了溢出,或者设置控制模式来指定如何进行舍入。
:整数类型格式转换
定义了可用于
中声明的整数类型输入/输出的格式化字符串的宏,还提供了处理最大宽度整数的函数。
:拼写转换
定义了可代表特定运算符(包含字符&
、|
、~
、!
和^
的运算符)的宏。当编程环境的本地字符集没有这些字符时,这些宏非常有用。
:布尔类型和值
定义了bool
、true
和false
宏,同时还定义了一个可以用于测试这些宏是否已被定义的宏。
:整数类型
声明了指定宽度的整数类型,并定义了相关的宏(例如指定每种类型的最大值和最小值的宏),同时也定义了用于构建具体类型的整型常量的带参数的宏。
:泛型数学
在C99
中,
和
头中的许多数学函数有多个版本。
中的泛型宏可以检测传递给它们的参数类型,并替代为相应的
或
中函数的调用。
:扩展的多字节和宽字符实用工具
提供了宽字符输入/输出和宽字符串操作的函数。
:宽字符分类和映射实用工具
是
的宽字符版本,提供了对宽字符进行分类和修改的函数。
头提供了常用类型和宏的定义,但没有声明任何函数。定义的类型包括以下几个:
ptrdiff_t
。指针相减运算结果的类型。size_t
。sizeof
运算符返回的类型。wchar_t
。一种足够大的、可以用于表示所有支持的地区的所有字符的类型。以上这3
种类型都是整数类型。其中ptrdiff_t
必须是有符号类型,size_t
必须是无符号类型。关于wchar_t
的更多细节见25.2节
。
头中还定义了两个宏。一个宏是NULL
,用来表示空指针。另一个宏是offsetof
,需要两个参数:类型(一种结构类型)和成员指示符(结构的一个成员)。offsetof
宏会计算结构的起点到指定成员间的字节数。
考虑下面的结构:
struct s {
char a;
int b[2];
float c;
};
offsetof(struct s, a)
的值一定是0
,C
语言确保结构的第一个成员的地址与结构自身地址相同。我们无法确定地说出b
和c
的偏移量是多少。一种可能是offsetof(struct s, b)
是1
(因为a
的长度是1
字节),而offsetof(struct s, c)
是9
(假设整数是32
位)。然而,一些编译器会在结构中留下一些空洞(不使用的字节,见第16章
的“问与答”
部分),从而会影响到offsetof
产生的值。例如,如果编译器在a
后面留下了3
字节的空洞,那么b
和c
的偏移量分别是4
和12
。但这正是offsetof
宏的魅力所在:对任意编译器,它都能返回正确的偏移量,从而使我们可以编写可移植的程序。
offsetof
有很多用途。例如,假如我们需要将结构s
的前两个成员写入文件,但忽略成员c
。我们不使用fwrite函数(22.6节)
来写sizeof(struct s)
字节,因为这样会将整个结构写入。我们只需要写offsetof(struct s, c)
字节。
最后一点:一些在
中定义的类型和宏在其他头中也会出现。(例如,
NULL
宏不仅在C99
的头中有定义,在
、
、
、
和
中也有定义。)因此,只有少数程序的确需要包含
。
头定义了
4
个宏:
bool
(定义为_Bool
);true
(定义为1
);false
(定义为0
);__bool_true_false_are_defined
(定义为1
)。我们已经见过很多使用bool
、true
和false
的例子。对__bool_true_false_are_defined
宏的应用相对少一些。在尝试定义自己的bool
、true
或false
之前,可以使用预处理指令(如#if
或者#ifdef
)来测试这个宏。
从
C11
开始对标准库的改变主要体现在以下几个方面:
C11
标准库中有5
个头是之前没有的,它们分别是
、
、>、
和
。
C11
标准在一些已有的头中增加了宏和函数,这些头主要有
、
、
等。printf
和scanf
)在C11
中具有了更多的功能。同时,出于对安全性的考虑,从头
中移除了gets
函数,并将它从新标准中废除。接下来我们快速浏览一下
C11
标准库中新增的5
个头:
:原子类型和原子操作
头定义了现有数据类型的原子类型,并提供了大量的宏用于执行原子类型变量的初始化和读写操作。
:多线程环境
头提供了线程的创建和管理函数,以及互斥锁、条件变量和线程局部存储的功能。
:数据对齐
头提供了4
个宏定义(21.7节
)。
:新的宽字符类型和实用工具
头定义了新的宽字符类型char16_t
和char32_t
,并提供了从多字节字符到这些宽字符类型的转换函数。
:函数指定符_Noreturn
相关
头非常简单,只定义了一个宏noreturn(21.8节)
。
头定义了
4
个宏:
alignas
(定义为_Alignas
);alignof
(定义为_Alignof
);__alignas_is_defined
(定义为整型常量1
);__alignof_is_defined
(定义为整型常量1
)。以上的后两个宏适合在预处理指令#if
中使用。如果已经定义了这两个宏,则说明另外两个宏alignas
和alignof
也被定义(如果你想自行定义alignas
和alignof
,应当先做这样的测试)
头
非常简单,它只是定义了宏
noreturn
(被定义为_Noreturn
)。
问1:我注意到书中使用术语
“标准头”
,而不是“标准头文件”
。不使用“文件”
有什么具体原因吗?
答:是的。依据C
标准,“标准头”
不一定是文件。虽然大部分编译器确实将标准头以文件形式存储,但标准头实际上可以直接内置在编译器自身中。
问2:
14.3节
描述了用带参数的宏替代函数的一些缺点。鉴于这些缺点,为标准库函数提供同名的宏版本不是很危险吗?
答:根据C
标准,用于替代库函数的带参数的宏必须用圆括号“完全保护”起来,而且只能对参数进行一次求值。这些规则可以避免14.3节
提到的大多数问题。
本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!