14.4 通用函数:stdlib.h

“stdlib.h”有点像杂货铺,凡是不好归类的内容都被放到了这里,并美名其曰:“通用”(General utilities)。

正因为如此,“stdlib.h”标准头里的功能又可以细分为以下若干类。

数值转换

整数算术

内存分配

伪随机数系列生成

搜索与排序

环境通信

多字节、宽字节和字符串转换

14.4.1 数值转换

所谓数值转换(Numeric Conversion),是指字符串形式的“数值文字”与内存中的二进制数值之间的相互转换。最常见的如

fprintf("%d",123);

其中“123”(内存中的0000 0000 0000 0000 0000 0000 0111 1011)被转换成了连续的'1'、'2'、'3'三个字符,就属于这种转换。正因为如此,实际上fprintf()、fscanf()的格式转换其实都是这些数值转换函数完成的,许多格式转换说明也必须借助这些数值转换函数才能说清楚,如表14-4所示。

表14-4 数值转换

14.4 通用函数:stdlib.h - 图1

有些函数的功能非常相近,这其中隐约可以窥得C语言的发展过程。

以“a”开头的函数都是在C语言早期就出现的函数。这些函数在转换出错时(比如得到的值超出相应数据类型的范围),行为是未定义的。

以“str”开头的函数都是C标准补充的。这些函数的功能通常更强,而且在发生错误时会通过改变“errno”(见“errno.h”)的值来通知其调用者发生了错误。

详细地考察这些函数对字符串格式的要求是很费神的事情,那需要静下心来仔细琢磨。所以这里就不详细说明了。

14.4.2 系列伪随机数生成函数

“stdlib.h”在该部分的内容包括一个宏和两个函数,如表14-5所示。

表14-5 系列伪随机数生成函数

14.4 通用函数:stdlib.h - 图2

(1)宏

RAND_MAX:生成的伪随机数的最大值,用于描述下面的rand()库函数的值域。标准要求这个值至少为32767。

(2)函数

Rand()返回的并非是真正的随机数,而是把一个数作为种子数乘以某一个确定的数得到的值,再把得到的值作为下次计算伪随机数的种子数。

显然,这并非真正的随机数,而是一种伪随机数。如果不能随机地指定种子,那么每次从一个特定的种子数开始,必然会得到相同的伪随机数序列。因而,为了更逼真地模拟序列随机数的产生,通常把程序运行当时的时间作为种子数。设定这个最初的种子数是srand()函数的功能。常见的写法如下所示。

14.4 通用函数:stdlib.h - 图3

不难猜测到,种子数多半是一个srand()和rand()所在模块的static外部变量。

这两个函数在许多模拟性的游戏程序中非常有用。但在测试这类程序时,种子数通常是固定的而不是随机的,其原因不难想到。

14.4.3 内存管理函数

这些函数在第11章中已经详细讨论过了,这里不再重复。

14.4.4 环境通信函数

“stdlib.h”的该部分内容包括2个宏和6个函数,如表14-6所示。

表14-6 环境通信函数

14.4 通用函数:stdlib.h - 图4

(1)宏

EXIT_SUCCESS:这个宏实际上就是前面无数段程序中main()里面的“return 0;”中的那个“0”。这个数会在程序退出后传递给操作系统,从而在操作系统的层面上可以知道程序退出时的一些信息(比如程序是完成任务正常退出,还是遇到意外无法继续工作而退出的)。

EXIT_FAILURE:这个宏通常在程序无法继续工作时返回给操作系统,以报告程序离开的状态。

也可以向操作系统返回其他值,但其他的值都与具体的环境有关。C语言不能保证以上两种以外的值在另外的环境下也有效。

(2)函数

这里有几个名词需要解释一下。

首先是所谓的“非正常退出”,它是与正常退出相对的。正常退出时,一般会清空缓冲区、关闭打开的流以及处理临时文件等。调用abort()退出时则做不到这些。

调用exit()退出时,它可以返回给操作系统一个main()中“return 0”的0那样的状态值。显然这种值至少应该有两种:EXIT_FAILURE和EXIT_SUCCESS。在main()中return语句和exit()都可以退出程序运行,但是在其他函数中退出return语句就无能为力了。

Exit()还有另外一种玩法,可以在退出前再另外做点什么事情,免得有权不用,过期作废。这要借助atexit()在退出之前注册需要运行的函数来实现。下面是一个示意性的代码。

程序代码14-1

14.4 通用函数:stdlib.h - 图5

它的运行结果如图14-1所示。

14.4 通用函数:stdlib.h - 图6

图14-1 调用exit()退出

可以看到,被atexit()注册的两个函数都被执行,但次序是“后来居上”。

如果在代码中某种可能的情况下不希望执行这些被注册过的函数,那么就需要调用_Exit()函数。

getenv()用于从环境列表(Environment List)中获得环境变量(List Member)的值。这里需要先解释一下什么叫“环境列表”。在Windows操作系统下运行下面的程序。

程序代码14-2

14.4 通用函数:stdlib.h - 图7

运行的结果如图14-2所示。

14.4 通用函数:stdlib.h - 图8

图14-2 更规范的写法

这就是操作系统的环境列表,这是为了运行操作系统所设置的参数,它可以让用户更方便且以符合自己要求的方式使用操作系统,运行自己要运行的程序。你所运行的程序会得到这个列表的一个副本。其中的“ProgramFiles=C:\Program Files”那种东西就是环境列表中的一项。那个“ProgramFiles”被就叫做环境变量,“C:\Program Files”就是它的值。Getenv()函数可以得到这个值。不信?试试!

程序代码14-3

14.4 通用函数:stdlib.h - 图9

它的运行结果如图14-3所示。

14.4 通用函数:stdlib.h - 图10

图14-3 getenv()的用法

这下你明白了吧?

System()函数的作用是执行操作系统层面的某个命令。通过下面极其简短的代码你就能明白它的效用。

程序代码14-4

14.4 通用函数:stdlib.h - 图11

14.4 通用函数:stdlib.h - 图12

它的运行结果如图14-4所示。

14.4 通用函数:stdlib.h - 图13

图14-4 system()函数的作用

14.4.5 查找与排序函数

如表14-7所示,这两个函数最有趣的就是它们的函数原型。

表14-7 查找与排序

14.4 通用函数:stdlib.h - 图14

Qsort()的前三个参数“void base”、“size_t nmemb”、“size_t size,”实际上是在向函数传递一个数组。由于编写这个函数的人并不知道你要传递一个什么样的数组,所以没有办法把形参写成一个指向数组起始元素的指针类型以及数组元素个数这样的形式。他只能把你传过去的指向数组起始元素的指针作为“void ”类型来接收。

这样,问题出现了:他不清楚数组元素的类型,因此他又补了一个“size_t size”来了解数组元素所占据的空间。

这个函数的编写者同样不清楚你要求他按照升序还是降序来对数组排序。所以他又增加了一个传递这种比较两个元素“大”、“小”或先后次序的“比较准则”的参数。这个参数是以指向函数的指针形式出现的。这很有趣,对于这个参数,从某种意义上来说,“哥传过去的不是‘数据’,是‘动作’”。

不仅如此,这个“动作”的定义也是由你指定的。很多情况下你需要亲自定义这个函数。哪怕你只是要求qsort()做一个最简单的int类型元素数组的递增排序,你也得自己定义一个这样的函数。

14.4 通用函数:stdlib.h - 图15

其中的“(int *)”表示你知道p_zsl和p_zs2其实是由两个指向“int”数据的指针转换来的。

这样,更有趣的事情出现了,qsort()必须借助你定义的函数才能完成工作。在你调用了它之后,它总是要回来调用你定义的函数,这就是所谓的“回调”。

顺便说一下,很多书声称qsort()函数使用的是“快速排序法”(一个很有名气的排序算法)。我不清楚它们的理由是什么?除非编译器在使用手册中这么说。C语言标准中并没有提到过这个函数应该使用“快速排序法”。

此外还要讲一下,qsort()函数的结果是根据“compar”降序排列。

bsearch()函数的功能是在一个已经排好序的数组中查找指定的数据。

了解了qsort()函数,就没必要对bsearch()的参数费太多口舌了。它只比qsort()函数多一个参数,就是要查找的“key”,这实际上是对指向所要查找的数据的指针进行了“(void *)”类型转换所得到的那个值。而这里的“compar”用来判断元素是否与要查找的元素相等。

14.4.6 整数算术函数

这部分专门定义了3种数据类型:div_t、ldiv_t、lldiv_t,专门用来表示两个整数商的数据类型。本质上它们都是一种结构体数据类型。这种结构体有两个成员(quot和rem)分别表示除法运算的商和余数。

所声明的关于整数算术运算的各个函数如表14-8所示。

表14-8 整数算术1

14.4 通用函数:stdlib.h - 图16

这种整数的除法和“/”、“%”运算的定义并不一定相同。在C89的年代,两个相异符号整数的“/”、“%”运算实际上是编译器定义的行为。在C99中重新明确并统一了“/”、“%”运算的定义,如表14-9所示。

表14-9 整数算术2

14.4 通用函数:stdlib.h - 图17

14.4.7 多字节、宽字节字符

这方面的话题涉及其他一些标准头,因此留在后面一并介绍。