14.1 使用标准库的一些常识

14.1.1 标准头与标准头文件

标准库中的子程序,主要就是一些被编译好了的常用函数。标准库一般以库文件的形式存放在磁盘中特定的文件夹中。

仅有这些编译好的函数是不够的,因为源程序中不但要调用这些函数,还要写这些函数的声明或函数原型,甚至可能还要用到一些特殊的常量及特殊的数据类型。为此,标准库除了提供编译好了的目标代码文件外(一般是扩展名“.lib”的文件形式(1)),还需要提供库中所用到的宏定义、数据类型的定义以及函数原型。这些内容被分门别类地组织到所谓的标准头(Standard Header)中。

多数情况下,标准头被组织成源文件,即所谓的标准头文件(Standard Header File)(2)(常规的是扩展名为“.h”的文本文件)。本书后面均假设标准头是以标准头文件形式组织的。

通过“#include”预处理命令可以很容易地达到写出相应的函数原型、定义相关的符号常量、定义相关新的数据类型等目的。

每一个库函数的函数原型都出现在某个标准头中。标准库通常包含了一组标准头文件和一个或几个库文件。在调用某个库函数时,一般需要用#include预处理命令引入相关的标准头。

C89标准中规定的标准头如下所示。

14.1 使用标准库的一些常识 - 图1

C99对其中许多进行了扩充。C99标准另外增加了几个新的标准头如下所示。

14.1 使用标准库的一些常识 - 图2

下面首先先介绍使用库的一些常识和禁忌,然后分门别类地对这些标准头中所涉及的内容(数据类型的定义、宏的定义以及函数原型)进行概括性介绍。希望通过这些介绍,读者能大致了解标准库提供了哪些功能。

14.1.2 使用库的禁忌

使用标准库最大的禁忌就是重名。为此,C标准规定了一些保留的标识符。这些标识符有如下所示。

■ 由下划线开头,后面跟一个大写字母或另一条下划线的标识符。这些标识符通常用于预定义的宏名或防止发生文件重叠包含的宏名。

■ 由下划线开头的标识符。可以在函数内使用,但一般不可作为外部变量名。

■ 标准库用到的函数名或其他extern类别的外部变量名。这里,“extern类别的外部变量名”的说法不是很准确,有些标识符看起来和用起来与“外部变量名”一样,但其实不是“变量名”。

■ 由“#include”命令引入的宏名。比如“#include<stdio.h>”之后就不可以使用“NULL”作为其他意义的标识符。

■ 由“#include”命令引入的类型名称。比如“#include<stddef.h>”之后就不可以使用“size_t”作为其他意义的标识符。

一旦发生重名,经验上来说是发生“链接”错误。但按照C标准的说法是,后果是“未定义的”。

14.1.3 并存的宏与函数

出于效率的考虑,在某种编译器下的库函数在另一种编译器中可能被定义为一种等效的宏。甚至理论上来说也存在同一个编译器存在同名的函数与宏的情形。

正因如此,所以通常声明库函数原型时应该使用“#include”命令,而不是把头文件中的函数原型照抄在程序代码中进行显式的声明。

此外,有时可能出于某些特殊的原因,希望代码进行函数调用而不是进行宏展开,这应该怎么办呢?

可以用这样一些办法避开宏展开。比如,假设某个编译器同时提供了x()函数和x()宏,如果希望调用函数,可以“(x)()”。如果读者理解函数名本身就是个表达式,那么可以看出这个表达式和x()函数调用表达式是完全等价的。但“(X)()”在形式上就绝对不可能是宏。因为类似函数的宏名后面总是紧跟“(”。

把“x”的值赋值给某个同类型的变量,然后通过该指针变量进行函数调用是避开宏展开的另一种办法。

另外一种办法是通过预处理命令:

14.1 使用标准库的一些常识 - 图3

在这条命令后就可以安心地进行“x()”函数调用了。

14.1.4 函数定义域问题

调用库函数,应该特别注意实参的有效性。这有两个方面需要考虑:一是类型的有效性;二是值的有效性。

实参类型与函数原型中不同时,编译器通常会给出警告。在有些情况下,程序虽然可以通过隐式类型转换正常运行,但就一般情况而言,这是鲁莽的行为。优秀的程序员不能容忍自己的代码中存在编译“警告”。

值的有效性问题在程序运行时才会发现,比如对负数开平方。这类错误在编译时是发现不了的。

老生常谈的一个问题是,调用库函数时,代码编写者还必须为对象预备适当的存储空间。下面是一个反面的例子。

14.1 使用标准库的一些常识 - 图4

这种情况以及实参值无效情况下的后果,C标准都规定是“未定义行为”。没有人知道会发生什么。