14.7 数值计算

C语言数值计算的标准头是“math.h”。C89和C99的“math.h”文件差别比较大,后者在数值计算方面增加了很多新的支持。有一点两者是相同的,那就是它们都提供了许多数值计算函数的函数原型。

14.7.1 math.h(C89)

C89的“math.h”中定义了一个宏:HUGE_VAL。这个值为double类型的宏一般用来表示很大的数,甚至用来代表无穷大。此外还给出了若干用于数值计算的函数原型,如表14-11所示,是对这些函数的简要描述。

表14-11 math.h(C89)中的数学函数

14.7 数值计算 - 图1

14.7 数值计算 - 图2

这些函数在调用时可能发生两种错误:定义域错或值域错。此时将引起外部变量“errno”的值发生改变。更详细的说明参见“errno.h”部分。

14.7.2 math.h(C99)

(1)一般特点

C99保留了C89中的全部数学函数。特别值得注意的是,C99中这些函数不但有“double”类型的版本,相应的还有“float”和“long double”版本。“float”和“long double”版本的函数名与“double”的函数名稍有区别。比如,对于求浮点数绝对值函数,在C99标准中有3个版本,如下所示。

14.7 数值计算 - 图3

值得注意的是,C99的“math.h”中定义了两种新的数据类型:float_t和double_t。在不同的实现或编译条件下,它们可以落实为不同的浮点类型组合。这显然是为了增强可移植性,并且也符合C语言的一贯风格(比如int的不同实现、size_t类型)。

对于C89中的“HUGE_VAL”宏,C99增加了float和long double类型版本:HUGE_VALF, HUGE_VALL。

(2)新增的宏

INFINITY、NAN:这两个宏一般表示无穷大和非数值,这实际上是两个特殊值的浮点数值。

14.7 数值计算 - 图4

这些都是类似函数的宏,用于对实浮点值分类。

14.7 数值计算 - 图5

这几个类似对象的宏用于描述前面各个类似函数宏的值的符号常量。

14.7 数值计算 - 图6

这几个类似函数的宏,用于比较实浮点值。

FP_FAST_FMA, FP_FAST_FMAF, FP_FAST_FMAL:这些是可选的,不是必需的;是关于浮点数计算描述硬件特性的宏。

FP_ILOGBO, FP_ILOGBNAN:用于表示ilogb()函数的返回值。

MATH_ERREXCEPT,MATH_ERRNO:两个值分别为1和2的宏,用于通过描述另一个宏math_errhandling,后者用于错误处理,其值要么等于前两个宏的值,要么是它们按位或的结果。

(3)新增的函数

C99增加了大量新的数学计算函数,限于篇幅,如表14-12所示,下面只是大致描述一下这些函数的功能。有兴趣的读者可查阅C99标准或所使用的编译器的参考手册。

表14-12 math.h(C99)中增加的数学函数

14.7 数值计算 - 图7

14.7 数值计算 - 图8

14.7.3 complex.h(C99)

“complex.h”是关于复数运算的标准头。其中定义了两个宏以表示虚数单位,还定义了一个小写的“complex”宏表示关键字“_Comolex”。此外的内容就是一些关于复数的数学计算函数。

复数函数是很复杂的数学函数。运用复数函数需要很多数学知识。据我所知,“复变函数”大约是大学数学系二、三年级开设的令大多数学生叫苦不迭的一门课程。有鉴于此,本书不打算在此罗列C99中增加的那些关于复数运算函数的名字了。那没有多少意义,我们能够知道的只是C99在数值计算方面走出了多远。

此外稍微提一下,以复数为自变量也有许多和以实数为自变量同名的数学函数,但它们的意义并不尽相同。

14.7.4 tgmath.h(C99)

由于C99中的许多数学函数都有至少有3套版本(float、double、long、double),甚至还有相应的复数类型版本。每一套版本都有自己的名字。这样使用起来非常不方便,而且缺乏可移植性。

为了解决这个问题,“tgmath.h”中定义了许多通用的类似函数的宏,使用这些宏时,预处理器能够根据参数的类型把宏展开为应当使用的特定的库函数。这就是“tgmath.h”标准头的意义之所在。

那些具体的宏名在此就不一一列举了。说实话,我从来没有真正见过这个标准头文件,一次也没有!

14.7.5 fenv.h(C99)

浮点数运算是比整数运算复杂得多的运算。

整数可以直截了当地表示成一个完整的二进制数,而浮点数是被分成几段存储的(符号、阶数、尾数)。

整数运算遇到的困境通常只有两种:除以0和溢出。浮点数运算不但会遇到除以0的问题,还会遇到很大的数除以很小的数这样的问题;而且浮点数的溢出显然可能发生在表示它的尾数段和阶数段,即所谓上溢和下溢。

整数运算是精确的,而浮点数运算几乎总是不精确的,这就产生了如何舍入的问题。

这些都要求浮点运算要有一整套的策略来处理这些问题:除数为0时怎么办?依据什么原则舍入?……

C语言把这些处理方法都分别表示成了“数”。这样每一组完整的数就可以表示一个完整的处理策略。这就可以被抽象成一种结构体数据类型,C语言把这种类型叫做“浮点环境类型”。

所以在“fenv.h”中,定义了

fenv_t:表示环境的数据类型。

此外还定义了用于记录表示浮点数状态的结构体类型。

fexcept_t:记录发生了怎样的异常情况。

C99的另一个想法大概是想通过这样的数据在处理浮点数据的过程中还能够选择改变处理策略。这无疑是特别有远见的想法。之所以这样说,是因为目前好像至少没有证据表明这个想法得到了普遍充分的实现。所以C99给出的允诺还是非常保守的——它并没有承诺很多。

“fenv.h”中的函数原型都是围绕着这些进行的:存储“浮点环境”、切换“浮点环境”、关闭异常处理、存储浮点数异常的状态、查询舍入方向、设置舍入方向……

看来,在不远的将来,在数值计算方面C语言和FORTRAN有得一拼。鹿死谁手,尚不可知。