Menu

C 语言数学函数:math.h、sqrt、pow 和 -lm 链接

带你了解 math.h:sqrt、pow、fabs、floor 和 ceil、round、fmod、三角函数和对数函数、INFINITY 和 NAN,以及导致经典 undefined reference 错误的 -lm 链接参数。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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) 判断是否为普通数字。

Coddy 编程语言插图

和 Coddy 一起学编程

开始