C 的核心语言只提供 + - * / 和 %,除此之外什么都没有:没有幂运算符,没有平方根,也没有取整。算术以外的一切都在标准数学库中,声明在 <math.h> 里。
这些函数都以 double 精度工作:接收 double 参数,返回 double。这是首先要记住的一点,因为传入 int 时不会有任何提示(它会被自动转换),而把结果赋给 int 时,小数部分也会被悄悄丢掉。
入门,以及 -lm 参数
在你自己的电脑上,编译这段代码可能会失败:
/usr/bin/ld: /tmp/ccXYZ.o: in function `main':
program.c:(.text+0x1a): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
这是使用 math.h 时最常见的问题,而且并不是你的代码写错了。头文件提供了声明,所以编译器没有意见;但函数的实现在一个单独的库(libm)中,Linux 上的链接器默认不会包含它。加上 -lm:
gcc program.c -o program -lm
这个参数必须放在源文件之后:链接器按顺序处理输入,如果库列在需要它的代码之前,就什么也解析不了。在 macOS 上以及 Windows 的 MinGW 中,数学函数已经是标准 C 库的一部分,所以不需要 -lm(不过在 macOS 上加了也无妨)。
幂和根
对负数调用 sqrt(x) 会返回 NaN,而不会崩溃;如果参数可能是负数,要先做检查。hypot 值得了解,因为它计算的值和 sqrt(x*x + y*y) 相同,但在 x 或 y 很大时不会溢出。
对于较小的整数次幂,直接相乘既更快又精确:x * x 比 pow(x, 2.0) 好,后者要经过对数运算,在你期望得到 9 的地方可能返回 8.999999999999998。
取整:floor、ceil、round、trunc
四个函数,对负数给出四种不同的结果。这张表是值得记住的部分:
floor(x):不大于x的最大整数。总是向负无穷方向取整:floor(-2.3)是-3。ceil(x):不小于x的最小整数。总是向正无穷方向取整:ceil(-2.3)是-2。round(x):最接近的整数,恰好一半时远离零取整:round(2.5)是3,round(-2.5)是-3。trunc(x):直接去掉小数部分,向零取整:trunc(-2.7)是-2。
这四个函数都返回 double。需要整数时把结果强制转换为 int 没有问题,但要注意 (int)x 本身执行的是截断,所以 (int)(x + 0.5) 这种老式的“四舍五入”写法对负数是错误的。请使用 round()。
fabs 和 fmod
fabs 用于求浮点数的绝对值。误用 abs 是一个典型错误,因为 abs 接收的是 int,类型转换会悄悄发生:
% 完全不能用于 double:它是整数运算符,编译器会拒绝。fmod(a, b) 是它的浮点版本,结果的符号与 a 相同。
fabs 也是比较两个 double 的正确方法,因为对浮点数使用 == 是一个陷阱:
0.1 和 0.2 无法用二进制精确表示,所以它们的和与 0.3 有极其微小的差距。比较时要设定一个容差,永远不要用 ==。
三角函数
三角函数使用的是弧度,而不是角度,这是另一个经常导致错误结果的地方。
完整的函数集合:sin、cos、tan,它们的反函数 asin、acos、atan,接收两个参数的 atan2(y, x),以及双曲函数 sinh、cosh、tanh。求角度时优先用 atan2(y, x) 而不是 atan(y / x):它能处理 x == 0 的情况,也能正确判断象限。
大多数系统上的 math.h 都定义了 M_PI,但它是 POSIX 扩展而不是标准 C,所以在 -std=c17 下可能不存在。像上面那样自己定义常量,可移植性更好。
对数和指数
注意这个沿用自数学的命名陷阱:log() 是自然对数(以 e 为底),而不是以 10 为底。以 10 为底的是 log10()。
log(0.0) 返回负无穷,log(-1.0) 返回 NaN,两者都不会崩溃。这意味着一个错误的参数会在你的计算中悄悄传递下去,直到某处打印出 nan。
INFINITY、NAN 以及如何检查它们
浮点数中有一些不是数字的值,math.h 为它们起了名字:
最后一行最关键。你不能写 if (x == NAN),它永远为假。请使用 isnan(x)。(x != x 只有在 x 是 NaN 时才为真,isnan 通常就是这样实现的;如果遇到没有 isnan 的工具链,这也是一个方便的备用方法。)
无穷大来自溢出(exp(1000.0)),以及非零 double 除以零。与整数除以零不同,这种除法是有定义的,不会崩溃。
float 和 long double 版本
每个函数都有三种形式:double 版本、带 f 后缀的 float 版本,以及带 l 后缀的 long double 版本。
float sqrtf(float x);
double sqrt(double x);
long double sqrtl(long double x);
除非有经过测量的理由,否则就用普通的 double 版本:double 是 C 默认提升到的类型,混用 float 版本带来的类型转换开销通常比节省的还多。后缀唯一重要的场合,是在不支持 double 的硬件上,对只用 float 的计算有严格性能要求的时候。
一个实际例子
把其中几个函数组合起来,计算两点之间的距离和一元二次方程的根:
注意这里用的是 fabs(disc) < 1e-12,而不是 disc == 0.0。这和前面的容差规则一样:由测量值计算出的判别式几乎不可能恰好等于零。
<math.h> 只是一个更大工具箱中的一个头文件;标准库页面介绍了其余部分,随机数一节则介绍了 rand,它在 <stdlib.h> 中,而不在这里。
常见问题
C 语言中怎么使用数学函数?
在文件开头加上 #include <math.h>,然后直接调用:sqrt(16.0)、pow(2.0, 10.0)、fabs(-3.5)。它们接收并返回 double。在 Linux 上还必须用 -lm 链接数学库:gcc program.c -o program -lm。
为什么编译时会出现 “undefined reference to sqrt”?
头文件为编译器提供了声明,但函数的实现在一个单独的库里,Linux 上的链接器不会自动把它链接进来。在命令的末尾加上 -lm:gcc program.c -o program -lm。在 macOS 上以及 Windows 的 MinGW 中,数学代码是标准 C 库的一部分,所以不需要这个参数。
C 语言中 abs 和 fabs 有什么区别?
<stdlib.h> 中的 abs() 接收 int,返回 int。<math.h> 中的 fabs() 接收 double,返回 double。调用 abs(-3.7) 会先把参数转换为 int,得到 3,小数部分被悄悄丢掉了。浮点数要用 fabs。
C 语言中怎么判断 NaN?
使用 <math.h> 中的 isnan(x)。不能用 x == NAN 来判断,因为 NaN 与任何值比较都不相等,包括它自己。这个特点恰好可以作为备用的判断方法:只有 NaN 才会让 x != x 为真。用 isinf(x) 判断无穷大,用 isfinite(x) 判断是否为普通数字。