log(10.0)可以编译,但log(0.0)无法编译且出现未定义的引用错误?

45

以下是C语言源代码:

#include <math.h>

int main(void)
{
    double          x;

    x = log(0.0);

    return 0;
}

当我使用gcc -lm编译时,出现以下错误:

/tmp/ccxxANVH.o: In function `main':
a.c:(.text+0xd): undefined reference to `log'
collect2: error: ld returned 1 exit status

但是,如果我将log(0.0)替换为log(10.0),那么它就能够成功编译。

我不太明白这一点,因为无论它们是否具有数学意义,它们都应该编译--没有语法错误。有人可以解释一下吗?

以防万一,我的gcc -v输出:

Configured with: ../src/configure -v --with-pkgversion='Ubuntu 4.8.2-19ubuntu1' --with-bugurl=file:///usr/share/doc/gcc-4.8/README.Bugs --enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.8 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.8 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --disable-libmudflap --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.8-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1)

请注意,本问题涉及常数折叠,但建议的重复问题是有关缺少链接库的问题。

14
这可能与常量传播和没有指定“-lm”有关。 - Mysticial
可能是重复的问题:sqrt()函数无法使用可变参数 - phuclv
1
@LưuVĩnhPhúc 这是两个不同的问题。请看我在最后一行所做的修改。 - xuhdev
@LưuVĩnhPhúc 这两个问题不同,前者是关于链接,而这个问题是关于优化的。 - Shafik Yaghmour
两者都可以通过内置函数来解释,因为即使没有链接库也可以编译。但是是的,它可能不是一个完全相同的副本。 - phuclv
显示剩余4条评论
2个回答

59

gcc可以在许多情况下使用内置函数,它们的文档说:

这些函数中的许多只在某些情况下进行优化;如果它们在特定情况下没有进行优化,则会发出对库函数的调用。

因此,在使用内置函数时,gcc不需要链接数学库,但由于log(0)未定义,它可能会强制gcc在运行时计算它,因为它具有副作用。

如果我们查看草案C99标准7.12.1错误条件的处理中的第4段,它说(我强调):

如果数学结果的数量级是有限的,但是太大以至于在指定类型的对象中不能表示数学结果而产生了非常大的舍入误差,那么浮点结果就会溢出。如果浮点结果溢出并且默认舍入有效,或者如果数学结果是有限参数的精确无穷大,则函数将返回宏HUGE_VAL、HUGE_VALF或HUGE_VALL的值,具体取决于返回类型,并带有与函数正确值相同的符号;如果整数表达式math_errhandling & MATH_ERRNO非零,则整数表达式errno将获得ERANGE的值;如果整数表达式math_errhandling & MATH_ERREXCEPT非零,则如果数学结果是精确无穷大,则引发“除以零”浮点异常,否则引发“溢出”浮点异常。
我们可以从使用-S标志生成汇编并使用grep log过滤调用log的实时示例中看到。

对于 log(0.0),将生成以下指令(在此查看实际效果):

call    log

但是在log(10.0)的情况下,不会生成call log指令,(现场演示)。

通常我们可以通过使用-fno-builtin标志来防止gcc使用内置函数,这可能是测试是否使用内置函数的更快速的方法。

请注意,-lm需要放在源文件之后, 例如(取自链接答案),如果main.c需要数学库,则应使用:

 gcc main.c -lm 

1
那是一个很棒的答案!是的,那正是问题所在。 - xuhdev
8
GCC在运行时无法计算log(0)的原因比较棘手。标准规定它返回-HUGE_VAL,但这也会导致一个范围错误的副作用(例如在errno中可见),因此不能消除该调用。 - Tavian Barnes
@TavianBarnes 确实,我也打算加上那个。 - Shafik Yaghmour
3
编译器可能会用代码替换log(0.0),将其转化为返回'HUGE_VAL'并设置'errno'的操作,但是对于非常量参数它必须生成函数调用,所以直接生成函数调用可能更简单。 - Keith Thompson
2
@KeithThompson 我想他们希望保持内置的评估代码简单,避免处理异常等问题,这会使运行时代码更加复杂。 - Shafik Yaghmour
显示剩余3条评论

8
编译本身没有问题,只是缺少链接器开关-lm
第二个版本可能能够编译和链接,因为gcclog(10.0)替换为常量,因此不需要调用数学库。在第二种情况下,结果在数学上是未定义的,并且求值会产生域错误。在这种情况下,该表达式无法被常量替换,因为运行时处理域错误的方式可能不同。
摘自C标准(draft):

发生域错误时,函数返回一个实现定义的值;如果整数表达式math_errhandling & MATH_ERRNO非零,则整数表达式errno获得值EDOM;如果整数表达式math_errhandling & MATH_ERREXCEPT非零,则引发“无效”的浮点异常。

因此,对于log(0.0)的评估结果要么返回值HUGE_VAL(而不是我之前所声称的NAN),要么会出现浮点异常。

编辑:根据收到的评论纠正了我的答案,并添加了C标准中的描述链接。


没问题。这就是 Stack Overflow 的用途... :-) - Axel
1
@TavianBarnes:我认为这是实现定义的。标准只是说:“对数函数计算x的自然对数(以e为底)。如果参数为负,则会发生域错误。如果参数为零,则可能会发生范围错误。” - Axel
2
如果数学结果是一个精确的无穷大(例如log(0.0)),那么函数将根据返回类型返回宏HUGE_VAL、HUGE_VALF或HUGE_VALL的值,符号与函数的正确值相同。 - Tavian Barnes
@T.C. 是的,你当然是对的。顺便说一下:回退编辑从-INF到NAN的不是我... - Axel
非常好,我会点赞的,但你已经在我后面不久了,所以我已经给你点赞了。 - Shafik Yaghmour
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接