每个标准库函数都会被声明在一个或多个标准头(standard header)中。这些标准头也包括了 C 语言标准提供的所有宏和类型的定义。
每个标准头都包含一组相关的函数声明、宏和类型定义。例如,数学函数声明在头文件 math.h 中。标准头也称之为头文件(header file),因为每个标准头内容通常被存储在一个文件中。然而,严格来说,C 语言标准并没有强制要求将标准头组织成文件。
C 标准定义了以下 29 个头文件(其中有星号标识的是 C11 新增的):
assert.h | inttypes.h | signal.h | stdint.h |
threads.h* | complex.h | iso646.h | stdalign.h* |
stdio.h | time.h | ctype.h | limits.h |
stdarg.h | stdlib.h | uchar.h* | errno.h |
locale.h | stdatomic.h* | stdnoreturn.h* | wchar.h |
fenv.h | math.h | stdbool.h | string.h |
wctype.h | float.h | setjmp.h | stddef.h |
gmath.h |
头文件 complex.h、stdatomic.h 和 threads.h 是可选的。有一些 C11 实现版本可以定义一些标准宏,以指示该版本不包括前述可选的头文件。这里,如果宏 __STDC_NO_COMPLEX__、宏 __STDC_NO_ATOMICS__,或宏 __STDC_NO_THREADS__ 被定义为值 1,则该实现版本就没有包括这些宏所对应的可选头文件。
通过 #include 命令,可以把一个标准头文件内容插入到一个源代码文件中。其中 #include 命令必须放在所有函数的外面。你可以以任意顺序包含多个标准头文件。然而,在针对某个头文件使用 #include 命令之前,程序不可以定义任何与该头文件内标识符相同的宏名称。为了确保程序符合该条件,总是在源代码开始的地方,首先包含所需的标准头文件,然后再包含自己定义的头文件。
C 程序只会在两种运行环境中执行:宿主(hosted)环境或独立(freestanding)环境。大多数程序在宿主环境中执行:也就是说,在操作系统的控制和支持之下执行。在宿主环境中,可以使用标准库的全部功能。而且,为宿主环境编译的程序必须定义 main()函数,它是程序启动后第一个执行的函数。
为独立环境设计的程序,执行时不会获得操作系统的支持。在独立环境中,由所使用的实现版本自身决定程序启动后第一个执行的函数是什么。独立环境的程序无法使用复数浮点类型,并且只能使用下面的头文件:
float.h | stdalign.h | stddef.h |
iso646.h | stdarg.h | stdint.h |
limits.h | stdbool.h | stdnoreturn.h |
特定的实现版本可能还提供另外的标准库资源。
所有的标准函数都有外部链接。因此,你可以通过在自己的程序中声明标准库函数来使用它们,而不需要包含对应的头文件。然而,如果标准函数需要头文件中定义的某个类型,那么必须包含对应的头文件。
标准库的函数不一定确保可重入(reentrant)。也就是说,在一个进程中两次调用同一个函数并行执行可能是不安全的行为。之所以制定该规则,其中一个原因是:部分标准函数会使用和修改同一个静态变量或线程变量。
因此,你不能在信号处理进程(signal handling rountine)中调用标准库函数。信号是异步的,也就是说,程序可能在任何时候收到信号,甚至是正在执行标准库函数时。当发生这种情况时,如果信号处理器再调用的标准函数与正在执行的是同一个,那么该函数则必须是可重入的。由各个实现版本自行决定哪些函数可重入,或者是否需要对所有标准库提供可重入版本。
大部分标准库函数(除了一些显式指定的函数)都是线程安全的(thread-safe),意思是它们可以“同时”被几个线程安全地执行。换句话说,标准函数必须这样实现:当多个线程调用它们时,所有它们使用的内部对象都不会造成数据竞争。尤其是它们在不能确保同步性的前提下,不得使用静态对象。然而,作为程序员,需要协调好不同线程对函数参数直接或间接引用对象的访问。
在执行操作前,每个流都具有一个相应的锁,I/O 链接库中的函数使用该锁以获得对这个流的独占访问权限。在这种方式下,当几个线程访问同一个给定的流时,标准库函数防止了数据竞争。
程序员要保障调用函数和类函数宏时,传入有效的参数。错误的参数会造成严重的运行错误。需要避免的典型错误包括:
(1) 参数值超出函数的值域,如下例所示:
double x = -1.0, y = sqrt(x)
(2) 指针参数没有指向一个对象或函数,相当于使用一个未经初始化的指针参数进行函数调用,如下例所示:
char *msg; strcpy( msg, "eror" );
(3) 参数类型不符合可选参数函数的类型要求。在下面的示例中,转换修饰符%f调用时需要一个浮点型指针参数,但 &x 是一个 double 指针:
double x; canf( "%f", &x );
(4) 数组地址参数所指向的数组不够大,不足以容纳该函数所要写入的数据。如下例所示:
char name[] = "Hi "; strcat( name, "Alice" );
标准库中的宏充分利用了括号,所以可以像使用一般标识符一样在表达式中使用这些标准宏。而且,标准库中每个类函数宏都只会使用其参数一次。这意味着,可以像调用普通函数一样调用这些宏,即便把具有副作用的表达式作为这些类函数宏的参数也可以。如下例所示:
int c = 'A';
while ( c <= 'Z' ) putchar( c++ ); // 输出:'ABC… XYZ'
标准库函数可能同时以宏和函数方式实现。如果这样的话,对于一个给定的函数名,同一个头文件中会包含一个函数原型和一个宏定义。因此,在包含该头文件之后,每次使用该函数名都会调用宏。下面的例子调用宏或函数 toupper()把小写字母转换为大写字母:
#include <ctype.h>
/* ... */
c = toupper(c); // 调用宏toupper(),如果存在的话
然而,如果指定需要调用一个函数,而不是同名的宏,可以利用 #undef 命令取消宏定义:
#include <ctype.h>
#undef toupper // 移除任何同名的宏定义
/* ... */
c = toupper(c) // 调用函数toupper()
把名称放在括号内,也可以调用函数而非宏:
#include <ctype.h>
/* ... */
c = (toupper)(c) // 调用函数toupper()
最后的一个做法,你可以忽略包含宏定义的头文件,直接在源代码文件中明确声明该函数:
extern int toupper(int);
/* ... */
c = toupper(c) // 调用函数toupper()
在程序中选择标识符使用时应特别注意,必须知道哪些标识符被保留给标准库使用。保留的标识符包括:
(1) 所有以下划线开始后面接着第二个下划线或者大写字母的标识符,均被保留。因此不能使用诸如 _x 或 _Max 形式的标识符,甚至不能作为局部变量或标签。
(2) 以下划线开始,但不符合上一点的所有其他标识符,都被保留为文件。因此,不能使用诸如 _a_ 形式标识符作为函数名或全局变量名,但是可以作为参数、局部变量和标签名称。结构成员和联合成员也可以使用下划线开头的名称作为标识符,但第二个字符不可以是下划线或大写字母。
(3) 在标准头文件中被声明为外部链接的标识符,被保留为外部链接标识符。这类标识符包括函数名以及全局变量名,例如 errno。虽然无法把这些外部链接标识符声明为自己的函数或对象名称,但是可以用于其他目的。例如,在一个没有包含 string.h 的源文件内,可以定义一个名为 strcpy()的静态函数。
(4) 在所包含的头文件中定义的所有宏标识符,都是被保留的。
(5) 在标准头文件中被声明为文件的标识符,在它们自身命名空间范围内,是被保留的。一旦在源文件中包含一个头文件,在同一个命名空间中,不能将在该头文件中声明为文件的标识符用作其他目的,或作为宏名称。
虽然这里所列出的一些条件有“漏洞”,允许在某些命名空间或配合静态链接重复使用某些标识符,但是标识符过多重用很容易造成混淆,通常最安全的方式是完全规避标准头文件中的标识符。
在下面的各节中,为了未来 C 标准的扩充,我们也会列出一些被保留的标识符。前面列表中的最后三点也适用于这些保留的标识符。