我有一个代码:
A.H
...
namespace X
{
const std::string Foo = "foo";
inline std::string getFoo()
{
return Foo;
}
}
...
a.cpp:
#include "a.h"
...
namespace X
{
const string Default_Foo = getFoo();
}
...
当然,项目中有更多文件包含a.h
该程序在开始时导致段错误。调查显示:
bash-4.2# nm -oC a.out | grep Foo a.out:3c162c50 b X::Foo a.out:3c162990 b X::Foo a.out:3c1641b0 b X::Foo
2.在初始化期间,Default_Foo调用getFoo(),它不从a.cpp编译单元获取已经初始化的Foo,而是从另一个编译单元获取Foo,这很可能尚未初始化。这显然会导致段错误。
有人可以给我这样的行为推理吗? 未来针对此类问题的最佳防御策略是什么?
最后我最感兴趣的是为什么getFoo()使用来自另一个编译单元的Foo。
答案 0 :(得分:3)
您的程序违反了一个定义规则(ODR)。
内联函数可以在多个编译单元中定义,但要求必须一致地定义它们,也就是说,每个定义都由相同的词汇序列组成,并且该序列中的每个使用的标识符必须表示相同的对象。
在您的情况下,标识符Foo
表示编译单元之间的不同对象(默认情况下,修饰符const
的名称空间级对象具有内部链接,因此每个编译单元都有自己的{{1} })。
用于构建程序的工具可能(但不是必须)诊断违反此规范的行为。在你的情况下,他们没有。链接器刚刚选择了X::Foo
的第一个定义而没有仔细考虑它。
答案 1 :(得分:2)
创建了X::Foo
的多个副本,因为您在标头中声明了定义 X::Foo
。因此,当您将X::Foo
包含在源中时,您将获得尽可能多的声明和定义。实际上,由于多个定义,它通常会在链接时导致错误,但是通过X::Foo
内部链接可以避免这种错误。 (请参阅 legends2k 评论以获得解释)
静态初始化本身只在编译单元内有一个顺序。在多个编译对象之间获得适当的静态初始化顺序确实是一个问题。有特定于编译器的扩展来实现这一点,但请注意它们有严重的限制。
提供的代码方式我看不出为什么X::getFoo()
调用不会被内联,从而消除了动态符号解析的任何机会。 X::Foo
本身也不应该被名字弄乱。您应检查生成二进制文件的符号名称/重定位表,以查看是否动态解析了X::getFoo()
或X::Foo
。
避免此问题的解决方案之一是避免全局静态变量。相反,使用这种方法:
static const std::string& getFoo()
{
static const std::string val = "abc";
return val;
}
您将获得以下承诺:
getFoo()
答案 2 :(得分:0)
在另一个论坛讨论这个问题之后,我倾向于认为问题的根源在于一个事实,即编译器为每个翻译单元创建多个getFoo()副本,每个翻译单元从该单元引用Foo。在链接期间,链接器认为所有getFoo()都是等效的并且拾取第一个,这不能保证是来自a.cpp转换单元的那个。
欢迎任何评论。