多个定义错误,包括包含来自多个源的内联代码的 C++ 头文件

multiple definition error including c++ header file with inline code from multiple sources

提问人:Paolo Tedesco 提问时间:10/17/2008 最后编辑:Paolo Tedesco 更新时间:6/10/2015 访问量:54369

问:

我有一个包含类的 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

既然类是在命名空间中定义的,那么这两种形式不应该是等价的吗?为什么在第一种情况下,该方法被认为被定义了两次?

C++语言

评论


答:

25赞 QBziZ 10/17/2008 #1

在类体内部,编译器认为类体是内联的。 如果在正文外部实现,但仍在标头中实现,则必须将方法显式标记为“内联”。

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() {...} 数十次。现在你可以看到这没有意义,与在一个源文件中多次复制/粘贴它时得到的效果相同。 即使你成功地在每个编译单元中只拥有一次它,通过某种形式的条件编译(例如使用包含括号),链接器仍然会发现此方法的符号位于多个编译单元(目标文件)中。

23赞 workmad3 10/17/2008 #2

这些并不等同。给出的第二个示例在方法上有一个隐式的“内联”修饰符,因此编译器将自行协调多个定义(如果它不是内联的,则很可能与方法的内部链接有关)。

第一个示例不是内联的,因此,如果此标头包含在多个翻译单元中,则将有多个定义和链接器错误。

此外,标题确实应该始终受到保护,以防止在同一翻译单元中出现多个定义错误。这应该将您的标头转换为:

#ifndef EXAMPLE_H
#define EXAMPLE_H

//define your class here

#endif

评论

0赞 Paolo Tedesco 10/17/2008
感谢您指出这一点...我忘记了示例中的包含守卫(但在实际代码中没有)。
4赞 PW. 10/17/2008 #3

不要将函数/方法定义放在头文件中,除非它们是内联的(通过直接在类声明或 inline 关键字指定的显式中定义它们)

头文件(大部分)用于声明(无论您需要声明什么)。允许的定义是常量和内联函数/方法(以及模板)的定义。

1赞 Timo Geusch 10/18/2008 #4

您的第一个代码片段违反了 C++ 的“一个定义规则” - 请参阅此处以获取描述 ODR 的维基百科文章的链接。您实际上违反了第 #2 点,因为每次编译器将头文件包含在源文件中时,您都会遇到编译器生成全局可见定义的风险。当然,当你开始链接代码时,链接器将有小猫,因为它会在多个目标文件中找到相同的符号。test_ns::TestClass::testMethod()

第二个代码段之所以有效,是因为您已经内联了函数的定义,这意味着即使编译器没有为函数生成任何内联代码(例如,您关闭了内联或编译器认为函数太大而无法内联),为函数定义生成的代码将仅在翻译单元中可见。 就好像你把它放在一个匿名命名空间中一样。因此,在生成的对象代码中,您可以获得该函数的多个副本,链接器可能会或可能不会优化这些副本,具体取决于它的智能程度。

您可以在第一个代码片段中通过添加前缀来达到类似的效果。TestClass::testMethod()inline

-1赞 Mahendra 7/16/2012 #5
//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.
5赞 sastanin 6/10/2015 #6

实际上,可以在单个头文件中具有定义(没有单独的 .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_DECLARATIONSuse_bar.c

#define ONLY_DECLARATIONS
#include "foobar.h"

void use_bar() {
    bar();
}

编译后,use_bar.o 和 main.o 可以链接在一起而不会出错,因为其中只有一个 (main.o) 会实现 foo() 和 bar()。

这有点不惯用,但它允许将定义和声明放在一个文件中。我觉得这是一个穷人对真正模块的替代品。