如果我包含<stdlib.h>
或<stdio.h>
在 C 程序中,我不必在编译时链接这些,但我必须链接到<math.h>
,使用-lm
gcc,例如:
gcc test.c -o test -lm
这是什么原因?为什么我必须显式链接数学库而不是其他库?
如果我包含<stdlib.h>
或<stdio.h>
在 C 程序中,我不必在编译时链接这些,但我必须链接到<math.h>
,使用-lm
gcc,例如:
gcc test.c -o test -lm
这是什么原因?为什么我必须显式链接数学库而不是其他库?
中的函数stdlib.h
和stdio.h
有实现libc.so
(或libc.a
用于静态链接),默认情况下链接到您的可执行文件中(就像-lc
指定的一样)。可以使用-nostdlib
or-nodefaultlibs
选项指示 GCC 避免这种自动链接。
中的数学函数在(或用于静态链接)中math.h
有实现,默认情况下不链接。这种/分裂有历史原因,没有一个很有说服力。libm.so
libm.a
libm
libm
libc
有趣的是,C++ 运行时libstdc++
需要libm
.,所以如果你用 GCC ( g++
) 编译一个 C++ 程序,你会自动libm
链接进去。
请记住,C 是一种古老的语言,而 FPU 是一种相对较新的现象。我第一次在 8 位处理器上看到 C 语言,即使是 32 位整数运算也需要做很多工作。其中许多实现甚至没有可用的浮点数学库!
即使在最初的 68000 台机器(Mac、Atari ST、Amiga)上,浮点协处理器通常也是昂贵的附加组件。
要完成所有浮点数学运算,您需要一个相当大的库。数学会很慢。所以你很少使用花车。你试图用整数或缩放整数来做所有事情。当您必须包含 math.h 时,您咬紧牙关。通常,您会编写自己的近似值和查找表来避免它。
权衡取舍存在了很长时间。有时会有一些相互竞争的数学包,叫做“fastmath”之类的。数学的最佳解决方案是什么?真的准确但缓慢的东西?不准确但速度很快?三角函数的大表?直到协处理器被保证在计算机中,大多数实现才变得显而易见。我想现在某个地方有一些程序员,正在研究嵌入式芯片,试图决定是否引入数学库来处理一些数学问题。
这就是为什么数学不是标准的。许多甚至大多数程序都没有使用单个浮点数。如果 FPU 一直存在并且浮点数和双精度数总是很便宜,那么毫无疑问会有一个“标准数学”。
因为没有人愿意修复的荒谬的历史实践。将 C 和 POSIX 所需的所有函数合并到一个库文件中不仅可以避免反复询问这个问题,而且还可以在动态链接时节省大量时间和内存,因为每个.so
链接的文件都需要文件系统操作定位并找到它,以及它的静态变量、重定位等的几页。
所有函数都在一个库中并且-lm
, -lpthread
,-lrt
等选项都是无操作(或链接到空.a
文件)的实现完全符合 POSIX,当然更可取。
注意:我说的是 POSIX,因为 C 本身并没有指定任何关于如何调用编译器的内容。因此,您可以gcc -std=c99 -lm
将编译器视为必须为符合行为而调用的特定于实现的方式。
因为time()
和其他一些函数是builtin
在 C 库 ( ) 本身中定义的,除非您使用compile 选项,否则libc
GCC总是链接到 libc 。然而,数学函数存在于其中并没有被 gcc 隐式链接。-ffreestanding
libm
这里给出了解释:
因此,如果您的程序正在使用数学函数并包含
math.h
,那么您需要通过传递-lm
标志来显式链接数学库。这种特殊分离的原因是数学家对他们的数学计算方式非常挑剔,他们可能希望使用他们自己的数学函数实现而不是标准实现。如果将数学函数混为一谈libc.a
,就不可能做到这一点。
[编辑]
不过,我不确定我是否同意这一点。如果您有一个提供 . 的库sqrt()
,并且您在标准库之前传递它,那么 Unix 链接器将采用您的版本,对吗?
在An Introduction to GCC - Linking with external libraries中对链接到外部库进行了彻底的讨论。如果一个库是标准库的成员(如 stdio),那么您不需要指定编译器(实际上是链接器)来链接它们。
编辑:在阅读了其他一些答案和评论后,我认为libc.a 参考和它链接到的 libm 参考对于为什么两者是分开的有很多话要说。
请注意,“libm.a”(数学库)中的许多函数在“math.h”中定义,但在 libc.a 中不存在。有些是,这可能会让人感到困惑,但经验法则是这样的——C 库包含 ANSI 规定必须存在的那些函数,因此如果只使用 ANSI 函数,则不需要 -lm。相比之下,`libm.a' 包含更多功能并支持附加功能,例如在 FP 错误的情况下,matherr 回调和符合几个替代行为标准。有关更多详细信息,请参见 libm 部分。
正如 ehemient 所说,默认链接 C 库 libc,该库包含 stdlib.h、stdio.h 和其他几个标准头文件的实现。补充一下,根据“ An Introduction to GCC ”,C 中基本的“Hello World”程序的链接器命令如下:
ld -dynamic-linker /lib/ld-linux.so.2 /usr/lib/crt1.o
/usr/lib/crti.o /usr/libgcc-lib /i686/3.3.1/crtbegin.o
-L/usr/lib/gcc-lib/i686/3.3.1 hello.o -lgcc -lgcc_eh -lc
-lgcc -lgcc_eh /usr/lib/gcc-lib/i686/3.3.1/crtend.o /usr/lib/crtn.o
请注意第三行中链接 C 库的选项-lc。
我认为这有点随意。您必须在某处画一条线(哪些库是默认的,哪些需要指定)。
它使您有机会将其替换为具有相同功能的其他设备,但我认为这样做并不常见。
编辑:(来自我自己的评论):我认为 gcc 这样做是为了保持与原始 cc 的向后兼容性。我猜为什么 cc 这样做是因为构建时间—— cc 是为比我们现在拥有的功率少得多的机器编写的。许多程序没有浮点数学,他们可能会将每个不常用的库都排除在默认值之外。我猜想 UNIX 操作系统的构建时间和随之而来的工具是驱动力。
如果我放 stdlib.h 或 stdio.h,我不必链接它们,但我必须在编译时链接:
stdlib.h
,stdio.h
是头文件。为了方便起见,您将它们包括在内。如果您在正确的库中链接,他们只会预测哪些符号将可用。实现在库文件中,这是函数真正存在的地方。
包括math.h
只是获得所有数学函数访问权限的第一步。
此外,libm
如果您不使用它的功能,则不必链接,即使您执行的#include <math.h>
只是一个信息步骤,用于编译器关于符号的信息。
stdlib.h
,stdio.h
请参考 中可用的功能libc
,这些功能恰好总是链接在其中,这样用户就不必自己动手了。
stdio 是标准 C 库的一部分,默认情况下,gcc 将链接到该库。
数学函数实现位于一个单独的 libm 文件中,默认情况下未链接到该文件,因此您必须指定它 -lm。顺便说一句,这些头文件和库文件之间没有关系。
我猜想这是一种让完全不使用它的应用程序性能稍好一些的方法。这是我对此的想法。
x86 操作系统(我想其他操作系统)需要在上下文切换时存储 FPU 状态。然而,大多数操作系统只是在应用程序第一次尝试使用 FPU 后才费心去保存/恢复这个状态。
除此之外,数学库中可能还有一些基本代码,它们会在加载库时将 FPU 设置为正常的基本状态。
因此,如果您根本不链接任何数学代码,这一切都不会发生,因此操作系统根本不必保存/恢复任何 FPU 状态,从而使上下文切换更加高效。
不过只是猜测。
编辑:作为对一些评论的回应,相同的基本前提仍然适用于非 FPU 案例(前提是它使那些没有使用 libm 的应用程序性能稍好一些)。
例如,如果有一个在 C 的早期很流行的软 FPU。那么将 libm 分开可以防止大量(如果使用的话会很慢)代码不必要地被链接。
此外,如果只有静态链接可用,那么类似的论点适用,它会降低可执行文件的大小和编译时间。
这是一个错误。您不必再明确指定-lm
。也许如果有足够多的人抱怨它,它就会得到解决。(我并不认真相信这一点,因为维持这种区别的维护者显然非常固执,但我可以希望。)
默认情况下,所有库都喜欢stdio.h
并在链接器stdlib.h
中实现libc.so
或链接。libc.a
的库libc.so
在编译时自动链接并包含在可执行文件中。
但是math.h
它的实现在其中libm.so
或与之libm.a
分开,libc.so
并且默认情况下它不会被链接,您必须在gcc
使用-lm
标志编译程序时手动链接它。
gnu gcc 团队将其设计为与其他头文件分开,而其他头文件默认链接但 math.h 文件没有。
这里阅读第 14.3 项,如果您愿意,可以阅读全部内容: 为什么
需要
链接 math.h
看这篇文章:为什么我们必须在 gcc 中链接 math.h?
看看用法:
使用库
请注意,-lm
即使您使用某些 C 数学函数,也不一定总是需要指定。
例如下面的简单程序:
#include <stdio.h>
#include <math.h>
int main() {
printf("output: %f\n", sqrt(2.0));
return 0;
}
可以使用以下命令编译并成功运行:
gcc test.c -o test
在 gcc 7.5.0(在 Ubuntu 16.04)和 gcc 4.8.0(在 CentOS 7)上测试。
这里的帖子给出了一些解释:
您调用的数学函数由编译器内置函数实现
也可以看看: