提问人:Paolo Tedesco 提问时间:10/17/2008 最后编辑:Paolo Tedesco 更新时间:6/10/2015 访问量:54369
多个定义错误,包括包含来自多个源的内联代码的 C++ 头文件
multiple definition error including c++ header file with inline code from multiple sources
问:
我有一个包含类的 c++ 头文件。 我想在几个项目中使用这个类,但我不想为它创建一个单独的库,所以我将方法声明和定义都放在头文件中:
// example.h
#ifndef EXAMPLE_H_
#define EXAMPLE_H_
namespace test_ns{
class TestClass{
public:
void testMethod();
};
void TestClass::testMethod(){
// some code here...
}
} // end namespace test_ns
#endif
如果在同一个项目中,我包含来自多个cpp文件的此标头,则会出现错误,显示“”,而如果将方法定义放在类体中,则不会发生这种情况:multiple definition of test_ns::TestClass::testMethod()
// example.h
#ifndef EXAMPLE_H_
#define EXAMPLE_H_
namespace test_ns{
class TestClass{
public:
void testMethod(){
// some code here...
}
};
} // end namespace test_ns
#endif
既然类是在命名空间中定义的,那么这两种形式不应该是等价的吗?为什么在第一种情况下,该方法被认为被定义了两次?
答:
在类体内部,编译器认为类体是内联的。 如果在正文外部实现,但仍在标头中实现,则必须将方法显式标记为“内联”。
namespace test_ns{
class TestClass{
public:
inline void testMethod();
};
void TestClass::testMethod(){
// some code here...
}
} // end namespace test_ns
编辑
就我自己而言,通过意识到编译器看不到类似头文件的内容,通常有助于解决这些类型的编译问题。头文件是预处理的,编译器只看到一个巨大的文件,其中包含每个(递归)包含文件的每一行。通常,这些递归包含的起点是正在编译的 cpp 源文件。 在我们公司,即使是一个看起来不起眼的 cpp 文件也可以作为 300000 行怪物呈现给编译器。
因此,当一个未以内联方式声明的方法在头文件中实现时,编译器最终可能会在预处理文件中看到 void TestClass::testMethod() {...} 数十次。现在你可以看到这没有意义,与在一个源文件中多次复制/粘贴它时得到的效果相同。 即使你成功地在每个编译单元中只拥有一次它,通过某种形式的条件编译(例如使用包含括号),链接器仍然会发现此方法的符号位于多个编译单元(目标文件)中。
这些并不等同。给出的第二个示例在方法上有一个隐式的“内联”修饰符,因此编译器将自行协调多个定义(如果它不是内联的,则很可能与方法的内部链接有关)。
第一个示例不是内联的,因此,如果此标头包含在多个翻译单元中,则将有多个定义和链接器错误。
此外,标题确实应该始终受到保护,以防止在同一翻译单元中出现多个定义错误。这应该将您的标头转换为:
#ifndef EXAMPLE_H
#define EXAMPLE_H
//define your class here
#endif
评论
不要将函数/方法定义放在头文件中,除非它们是内联的(通过直接在类声明或 inline 关键字指定的显式中定义它们)
头文件(大部分)用于声明(无论您需要声明什么)。允许的定义是常量和内联函数/方法(以及模板)的定义。
您的第一个代码片段违反了 C++ 的“一个定义规则” - 请参阅此处以获取描述 ODR 的维基百科文章的链接。您实际上违反了第 #2 点,因为每次编译器将头文件包含在源文件中时,您都会遇到编译器生成全局可见定义的风险。当然,当你开始链接代码时,链接器将有小猫,因为它会在多个目标文件中找到相同的符号。test_ns::TestClass::testMethod()
第二个代码段之所以有效,是因为您已经内联了函数的定义,这意味着即使编译器没有为函数生成任何内联代码(例如,您关闭了内联或编译器认为函数太大而无法内联),为函数定义生成的代码将仅在翻译单元中可见。 就好像你把它放在一个匿名命名空间中一样。因此,在生成的对象代码中,您可以获得该函数的多个副本,链接器可能会或可能不会优化这些副本,具体取决于它的智能程度。
您可以在第一个代码片段中通过添加前缀来达到类似的效果。TestClass::testMethod()
inline
//Baseclass.h or .cpp
#ifndef CDerivedclass
#include "Derivedclass.h"
#endif
or
//COthercls.h or .cpp
#ifndef CCommonheadercls
#include "Commonheadercls.h"
#endif
I think this suffice all instances.
实际上,可以在单个头文件中具有定义(没有单独的 .c/.cpp 文件),并且仍然可以从多个源文件中使用它。
请考虑以下标头:foobar.h
#ifndef FOOBAR_H
#define FOOBAR_H
/* write declarations normally */
void foo();
void bar();
/* use conditional compilation to disable definitions when necessary */
#ifndef ONLY_DECLARATIONS
void foo() {
/* your code goes here */
}
void bar() {
/* your code goes here */
}
#endif /* ONLY_DECLARATIONS */
#endif /* FOOBAR_H */
如果仅在一个源文件中使用此标头,请包含并正常使用它。
就像在:main.c
#include "foobar.h"
int main(int argc, char *argv[]) {
foo();
}
如果项目中有其他源文件需要 ,则在包含它之前先进行宏。
你可以写:foobar.h
#define ONLY_DECLARATIONS
use_bar.c
#define ONLY_DECLARATIONS
#include "foobar.h"
void use_bar() {
bar();
}
编译后,use_bar.o 和 main.o 可以链接在一起而不会出错,因为其中只有一个 (main.o) 会实现 foo() 和 bar()。
这有点不惯用,但它允许将定义和声明放在一个文件中。我觉得这是一个穷人对真正模块的替代品。
上一个:仅标头库作为模块?
评论