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