1

我编写了一个非常简单的 c++ 代码,在其中定义了一个名为 sqrt 的函数,它只调用 std::sqrt。出乎意料的是,我遇到了分段错误。如果我将函数 sqrt 重命名为其他名称,则问题不存在。但是,我看不到任何命名冲突,因为我定义的 sqrt 函数不在命名空间 std 中,因此两者应该完全分开。那么问题的真正原因是什么?谢谢!

#include<iostream>
#include<cmath>

double sqrt(double d);

double sqrt(double d) {
    return std::sqrt(d);
}

int main() {
    double x = 3.0;
    std::cout << "The square root of " << x << " is " << sqrt(x) << '\n';
    return 0;
}
4

3 回答 3

4

<cmath>是一个有趣的标题。允许(但不要求)制作::sqrtstd::sqrt同义词。如果你包含它,最好假设两者都存在(或者只是 include <math.h>,在这种情况下,::sqrt你应该得到的只是)。在您的情况下可能发生的情况是 1)std::sqrt实际上是 的同义词 (via using) ::sqrt,并且 2) 链接器正在获取您的::sqrt第一个,因此您最终会出现无休止的递归。除了更改名称之外,唯一的解决方案是将您 sqrt的名称放在名称空间中。

编辑:

需要明确的是:以上是 C++11。早期版本的 C++不允许<cmath>任何东西引入全局命名空间。然而,所有的实现都做了,所以标准被改变以支持这种做法。(我想这是让编译器符合标准的一种方法。)

编辑:

一些关于图书馆如何“拾取”符号的附加信息,以回应评论中的问题。形式上,根据 C++ 标准,一个程序中可能没有同一函数的两个定义(相同的名称、命名空间和参数类型)。如果这两个定义在不同的翻译单元中,则行为未定义。考虑到这一点,有几个实际的考虑。

第一个可以被认为是库的定义(或至少是传统的定义)。就标准而言,库是一组模块——翻译单元。(通常,但不总是,模块由编译的目标文件组成。)然而,在库中链接并不会引入其中的所有模块。仅当库中的模块解析未解析的外部时,它才会合并到您的程序中。因此,如果::sqrt在链接器查看库之前已经定义(解析),则包含 ::sqrt在库中的模块将不会成为您程序的一部分。

在实践中,图书馆这个术语近年来一直被滥用,以至于人们可能会说它的含义已经发生了变化。特别是,微软所说的“动态加载的库”(以及很久以前在 Unix 中被称为“共享对象”的东西)并不是传统意义上的库,上述内容并不适用于它们。但是,其他问题取决于动态加载器的工作方式。在 Unix 中,如果多个共享对象具有相同的符号,所有共享对象都将解析为第一个加载的对象(默认情况下,这可以通过传递给的选项来控制dlopen)。在 Windows 的情况下,默认情况下,如果可能,符号将在 DLL 中解析;在你的情况下,如果std::sqrt是一个内联函数,或者被指定为using ::sqrt,这将是调用的 DLL std::sqrt; 如果在标头中是__declspec(dllexport),这将是包含 std::sqrt.

最后,今天几乎所有的链接器都支持某种形式的弱引用。这通常用于模板实例化:类似的东西std::vector<int>::vector( size_t, int )将在每个使用它的翻译单元中实例化,但作为“弱”符号。然后链接器选择一个(可能是它遇到的第一个,但没有指定),并抛出所有其他的。虽然这种技术主要用于模板实例化,但编译器可以使用弱引用定义任何函数(如果函数是内联的,也会这样做)。在这种情况下,如果定义不同(如您的情况 ::sqrt),那么我们可以真正说该程序是非法的,因为它违反了单一定义规则。但结果是未定义的行为,不需要诊断。例如,如果您在两个不同的翻译单元中以不同的方式定义内联函数或函数模板,您几乎不会出错;如果编译器实际上没有内联它们,链接器将选择一个,并在两个翻译单元中使用它。在您的情况下(::sqrt),我怀疑这是否适用;我希望这是一个真正的库函数,而不是内联的。(如果它被内联,定义将在 header<cmath>中,并且您会收到重复定义错误,因为两个定义都在同一个翻译单元中。)

于 2013-03-24T21:29:06.697 回答
2

问题似乎<cmath>是引入了sqrt名称(没有std::命名空间),以及std::sqrt. 恐怕你需要使用另一个名字。

请参阅此示例,使用 GCC 4.8 的快照:

#include<iostream>
#include<cmath>

int main() {
    double x = 9.0;
    std::cout << sqrt(x) << '\n'; // look, no std::sqrt
}
于 2013-03-24T21:23:48.230 回答
1

根据第 17.6.1.2/4 段:

除了第 18 到 30 条和附录 D 中的说明外,每个头文件 cname 的内容应与相应的头文件 name.h 的内容相同,如 C 标准库 (1.2) 或 C Unicode TR 中规定的,视情况而定,好像通过包含。然而,在 C++ 标准库中,声明(在 C 中定义为宏的名称除外)在命名空间 std 的命名空间范围 (3.3.6) 内。未指定这些名称是否首先在全局命名空间范围内声明,然后通过显式使用声明(7.3.3)注入命名空间 std 。

此外,根据附件 D.5/2:

每个 C 标头(每个都有一个名为 name.h 的名称)的行为就好像每个由相应的 cname 标头放置在标准库命名空间中的名称都放置在全局命名空间范围内一样。未指定这些名称是否首先在命名空间 std 的命名空间范围 (3.3.6) 内声明或定义,然后通过显式使用声明 (7.3.3) 注入全局命名空间范围。

由于用于使全局函数可用的确切技术留给实现,因此您的实现可能using在命名空间内具有如下指令std

namespace std
{
    using ::sqrt;

    // ...
}

这意味着它std::sqrt实际上成为了 的别名::sqrt,并且您提供了一个定义,::sqrt它实际上最终以递归方式调用自身。

唯一的解决方案是选择一个不同的名称。

于 2013-03-24T21:32:41.763 回答