type
status
date
slug
summary
tags
category
icon
password
Property
最初,并没有官方的C库。后来,基于UNIX的C实现成为了标准。ANSIC委员会主要以这个标准为基础,开发了一个官方的标准库。在意识到C语言的应用范围不断扩大后,该委员会重新定义了这个库,使之可以应用于其他系统。
访问C库
何访问C库取决于实现,因此要了解当前系统的一般情况。首先,可以在多个不同的位置找到库函数。例如,
getchar()
函数通常作为宏定义在stdio.h
头文件中,而strlen()
通常在库文件中。其次,不同的系统搜索这些函数的方法不同。下面介绍3种可能的方法。- 自动访问
在一些系统中,只需编译程序,就可使用一些常用的库函数。
在使用函数之前必须先声明函数的类型,通过包含合适的头文件即可完成。在描述库函数的用户手册中,会指出使用某函数时应包含哪个头文件。但是在一些旧系统上,可能必须自己输入函数声明。
过去,不同的实现使用的头文件名不同。ANSI C标准把库函数分为多个系列,每个系列的函数原型都放在一个特定的头文件中。
- 文件包含
如果函数被定义为宏,那么可以通过
#include
指令包含定义宏函数的文件。通常,类似的宏都放在合适名称的头文件中。例如,许多系统(包括所有的ANSI C系统)都有ctype.h
文件,该文件中包含了一些确定字符性质(如大写、数字等)的宏。
- 库包含 在编译或链接程序的某些阶段,可能需要指定库选项。即使在自动检查标准库的系统中,也会有不常用的函数库。必须通过编译时选项显式指定这些库。注意,这个过程与包含头文件不同。头文件提供函数声明或原型,而库选项告诉系统到哪里查找函数代码。
数学库
数学库中包含许多有用的数学函数,
math.h
头文件提供这些函数的原型:类型变体
基本的浮点型数学函数接受
double
类型的参数,并返回double
类型的值。当然,也可以把float
或 long double
类型的参数传递给这些函数,这些类型的参数会被转换成double
类型。这样做很方便,但并不是最好的处理方式。如果不需要双精度,那么用float
类型的单精度值来计算会更快些。而且把long double
类型的值传递给double
类型的形参会损失精度,形参获得的值可能不是原来的值。为了解决这些潜在的问题,C标准专门为
float
类型和long double
类型提供了标准函数,即在原函数名前加上f或l前缀。因此,sqrtf()
是sqrt()
的float版本,sqrtl()
是sqrt()
的long double
版本。利用C11 新增的泛型选择表达式定义一个泛型宏,根据参数类型选择最合适的数学函数版本。
tgmath.h库(C99)
C99标准提供的
tgmath.h
头文件中定义了泛型类型宏。如果在math.h
中为一个函数定义了3种类型(float、double和long double)的版本,那么tgmath.h
文件就创建一个泛型类型宏,与原来 double版本的函数名同名。例如,根据提供的参数类型,定义 sqrt()
宏展开为sqrtf()
、sqrt()
或 sqrtl()
函数。换言之,sqrt()
宏的行为和SQRT()
宏类似。如果编译器支持复数运算,就会支持
complex.h
头文件,其中声明了与复数运算相关的函数。例如,声明有csqrtf()
、csqrt()
和csqrtl()
,这些函数分别返回 float complex
、double complex
和long double complex
类型的复数平方根。如果提供这些支持,那么tgmath.h
中的sqrt()
宏也能展开为相应的复数平方根函数。如果包含了
tgmath.h
,要调用sqrt()
函数而不是sqrt()
宏,可以用圆括号把被调用的函数名括起来:不借助C标准以外的机制,C11新增的
_Generic
表达式是实现tgmath.h
最简单的方式。通用工具库
system()函数
执行 dos(windows系统) 或 shell(Linux/Unix系统) 命令,参数字符串command为命令名。另,在windows系统下参数字符串不区分大小写。
说明:在windows系统中,system函数直接在控制台调用一个command命令。在Linux/Unix系统中,system函数会调用fork函数产生子进程,由子进程来执行command命令,命令执行完后随即返回原调用的进程。
命令执行成功返回0,执行失败返回-1。
exit()和atexit()函数
atexit()函数的用法
这个函数使用函数指针。要使用
atexit()
函数,只需把退出时要调用的函数地址传递给 atexit()
即可。函数名作为函数参数时相当于该函数的地址,所以该程序中把sign_off()
或too_bad()
作为参数。然后,atexit()
注册函数列表中的函数,当调用exit()
时就会执行这些函数。ANSI保证,在这个列表中至少可以放 32 个函数。最后调用exit()
函数时,exit()
会执行这些函数(执行顺序与列表中的函数顺序相反,即最后添加的函数最先执行)。输入失败时,会调用
sign_off()
和too_bad()
函数;输入成功时只会调用sign_off()
。因为只有输入失败时,才会进入if
语句中注册too_bad()
。注意,最先调用的是最后一个被注册的函数。atexit()
注册的函数(如sign_off()
和too_bad()
)应该不带任何参数且返回类型为void。通常,这些函数会执行一些清理任务,例如更新监视程序的文件或重置环境变量。
注意,即使没有显式调用exit()
,还是会调用sign_off()
,因为main()
结束时会隐式调用exit()
。exit()函数的用法
exit()
执行完atexit()
指定的函数后,会完成一些清理工作:刷新所有输出流、关闭所有打开的流和关闭由标准I/O函数tmpfile()
创建的临时文件。然后exit()
把控制权返回主机环境,如果可能的话,向主机环境报告终止状态。通常,UNIX程序使用0表示成功终止,用非零值表示终止失败。UNIX返回的代码并不适用于所有的系统,所以ANSI C为了可移植性的要求,定义了一个名为
EXIT_FAILURE
的宏表示终止失败。类似地,ANSI C还定义了EXIT_SUCCESS表示成功终止。不过,exit()
函数也接受0表示成功终止。在ANSI C中,在非递归的main()
中使用exit()
函数等价于使用关键字return。尽管如此,在main()
以外的函数中使用exit()
也会终止整个程序。qsort()函数
对较大型的数组而言,“快速排序”方法是最有效的排序算法之一。该算法由C.A.R.Hoare于1962年开发。它把数组不断分成更小的数组,直到变成单元素数组。首先,把数组分成两部分,一部分的值都小于另一部分的值。这个过程一直持续到数组完全排序好为止。
快速排序算法在C实现中的名称是
qsort()
。qsort()
函数排序数组的数据对象,其原型如下:第1个参数是指针,指向待排序数组的首元素。ANSI C允许把指向任何数据类型的指针强制转换成指向void的指针,因此,
qsort()
的第1个实际参数可以引用任何类型的数组。第2个参数是待排序项的数量。函数原型把该值转换为
size_t
类型。size_t
定义在标准头文件中,是sizeof
运算符返回的整数类型。第3个参数是数组中每个元素占用的空间大小。
由于
qsort()
把第1个参数转换为void指针,所以qsort()
不知道数组中每个元素的大小。为此,函数原型用第 3 个参数补偿这一信息,显式指明待排序数组中每个元素的大小。例如,如果排序 double类型的数组,那么第3个参数应该是sizeof(double)
。最后,
qsort()
还需要一个指向函数的指针,这个被指针指向的比较函数用于确定排序的顺序。该函数应接受两个参数:分别指向待比较两项的指针。如果第1项的值大于第2项,比较函数则返回正数;如果两项相同,则返回0;如果第1项的值小于第2项,则返回负数。qsort()
根据给定的其他信息计算出两个指针的值,然后把它们传递给比较函数。qsort()
原型中的第4个函数确定了比较函数的形式:这表明
qsort()
最后一个参数是一个指向函数的指针,该函数返回 int 类型的值且接受两个指向const void的指针作为参数,这两个指针指向待比较项。mycomp()的定义
qsort()
的原型中规定了比较函数的形式:这表明
qsort()
最后一个参数是一个指向函数的指针,该函数返回 int 类型的值且接受两个指向const void的指针作为参数。程序中mycomp()
使用的就是这个原型:记住,函数名作为参数时即是指向该函数的指针。因此,mycomp与compar原型相匹配。
qsort()
函数把两个待比较元素的地址传递给比较函数。在该程序中,把待比较的两个double类型值的地址赋给p1和p2。注意,qsort()
的第1个参数引用整个数组,比较函数中的两个参数引用数组中的两个元素。这里存在一个问题。为了比较指针所指向的值,必须解引用指针。因为值是 double 类型,所以要把指针解引用为 double 类型的值。然而,qsort()要求指针指向void。要解决这个问题,必须在比较函数的内部声明两个类型正确的指针,并初始化它们分别指向作为参数传入的值:简而言之,为了让该方法具有通用性,
qsort()
和比较函数使用了指向void 的指针。因此,必须把数组中每个元素的大小明确告诉qsort()
,并且在比较函数的定义中,必须把该函数的指针参数转换为对具体应用而言类型正确的指针。断言库
assert.h
头文件支持的断言库是一个用于辅助调试程序的小型库。它由assert()
宏组成,接受一个整型表达式作为参数。如果表达式求值为假(非零),assert()
宏就在标准错误流中写入一条错误信息,并调用abort()
函数终止程序(abort()
函数的原型在stdlib.h
头文件中)。assert()
宏是为了标识出程序中某些条件为真的关键位置,如果其中的一个具体条件为假,就用 assert()
语句终止程序。通常,assert()
的参数是一个条件表达式或逻辑表达式。如果assert()
中止了程序,它首先会显示失败的测试、包含测试的文件名和行号。使用
assert()
有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭 assert()
的机制。如果认为已经排除了程序的 bug,就可以把下面的宏定义写在包含assert.h
的位置前面:并重新编译程序,这样编译器就会禁用文件中的所有
assert()
语句。如果程序又出现问题,可以移除这条#define
指令(或注释掉),然后重新编译程序,这样就重新启用了assert()
语句。_Static_assert(C11)
assert()
表达式是在运行时进行检查。C11新增了一个特性:_Static_assert
声明,可以在编译时检查assert()
表达式。因此,assert()
可以导致正在运行的程序中止,而_Static_assert()
可以导致程序无法通过编译。_Static_assert()
接受两个参数。第1个参数是整型常量表达式,第2个参数是一个字符串。如果第1个表达式求值为 0(或_False),编译器会显示字符串,而且不编译该程序。memcpy()和memmove()
不能把一个数组赋给另一个数组,所以要通过循环把数组中的每个元素赋给另一个数组相应的元素。有一个例外的情况是:使用
strcpy()
和strncpy()
函数来处理字符数组。memcpy()
和memmove()
函数提供类似的方法处理任意类型的数组。这两个函数都从 s2 指向的位置拷贝 n 字节到 s1 指向的位置,而且都返回 s1 的值。所不同的是,
memcpy()
的参数带关键字restrict,即memcpy()
假设两个内存区域之间没有重叠;而memmove()
不作这样的假设,所以拷贝过程类似于先把所有字节拷贝到一个临时缓冲区,然后再拷贝到最终目的地。如果使用
memcpy()
时两区域出现重叠,其行为是未定义的,这意味着该函数可能正常工作,也可能失败。在使用该函数时要确保两个区域不重叠。由于这两个函数设计用于处理任何数据类型,所有它们的参数都是两个指向 void 的指针。C 允许把任何类型的指针赋给
void *
类型的指针。如此宽容导致函数无法知道待拷贝数据的类型。因此,这两个函数使用第 3 个参数指明待拷贝的字节数。注意,对数组而言,字节数一般与元素个数不同。如果要拷贝数组中10个double类型的元素,要使用10*sizeof(double),而不是10。程序中最后一次调用
memcpy()
从 double 类型数组中把数据拷贝到 int 类型数组中,说明memcpy()
函数不知道也不关心数据的类型,它只负责从一个位置把一些字节拷贝到另一个位置(例如,从结构中拷贝数据到字符数组中)。而且,拷贝过程中也不会进行数据转换。如果用循环对数组中的每个元素赋值,double类型的值会在赋值过程被转换为int类型的值。这种情况下,按原样拷贝字节,然后程序把这些位组合解释成int类型。可变参数:stdarg.h
变参宏,即该宏可以接受可变数量的参数。
stdarg.h
头文件为函数提供了一个类似的功能,但是用法比较复杂。必须按如下步骤进行:- 提供一个使用省略号的函数原型;
- 在函数定义中创建一个va_list类型的变量;
- 用宏把该变量初始化为一个参数列表;
- 用宏访问参数列表;
- 用宏完成清理工作。
这种函数的原型应该有一个形参列表,其中至少有一个形参和一个省略号:
最右边的形参(即省略号的前一个形参)起着特殊的作用,标准中用parmN这个术语来描述该形参。在上面的例子中,第1行f1()中parmN为n,第2行f2()中parmN为k。传递给该形参的实际参数是省略号部分代表的参数数量。例如,可以这样使用前面声明的
f1()
函数:接下来,声明在
stdarg.h
中的va_list类型代表一种用于储存形参对应的形参列表中省略号部分的数据对象。变参函数的定义起始部分类似下面这样:在该例中,lim是parmN形参,它表明变参列表中参数的数量。
然后,该函数将使用定义在stdarg.h中的va_start()宏,把参数列表拷贝到va_list类型的变量中。该宏有两个参数:va_list类型的变量和parmN形参。接着上面的例子讨论,va_list类型的变量是ap,parmN形参是lim。所以,应这样调用它:
下一步是访问参数列表的内容,这涉及使用另一个宏va_arg()。该宏接受两个参数:一个va_list类型的变量和一个类型名。第1次调用va_arg()时,它返回参数列表的第1项;第2次调用时返回第2项,以此类推。表示类型的参数指定了返回值的类型。例如,如果参数列表中的第1个参数是double类型,第2个参数是int类型,可以这样做:
注意,传入的参数类型必须与宏参数的类型相匹配。如果第1个参数是10.0,上面tic那行代码可以正常工作。但是如果参数是10,这行代码可能会出错。这里不会像赋值那样把double类型自动转换成int类型。
最后,要使用va_end()宏完成清理工作。例如,释放动态分配用于储存参数的内存。该宏接受一个va_list类型的变量:
调用va_end(ap)后,只有用va_start重新初始化ap后,才能使用变量ap。
因为va_arg()不提供退回之前参数的方法,所以有必要保存va_list类型变量的副本。C99新增了一个宏用于处理这种情况:va_copy()。该宏接受两个va_list类型的变量作为参数,它把第2个参数拷贝给第1个参数:
此时,即使删除了ap,也可以从apcopy中检索两个参数。
第1次调用
sum()
时对3个数求和,第2次调用时对6个数求和。