C++中的多重定义错误

9
我的 'Headers.h' 文件包含了基本的 C++ 头文件。
#include <iostream>
#include <cstring>
// and many header files.

我已经为文件存在检查编写了函数定义,并将其保存在“common_utility.h”中 - ifFileExist()

common_utility.h

bool ifFileExist()
{
   // ... My code
}

编写了 Class A 的代码 classA.h

class A
{
// Contains class A Declarations.

};

classA.cpp

// Contains
#include "Headers.h"
#include "common_utility.h"
#include "classA.h"

// class A Method definition

编写了Class B的代码 我在Class B中使用了Class A。

classB.h

 class B
    {
// Contains class A Declarations.
}

classB.cpp

// Contains
#include "Headers.h"
#include "common_utility.h"
#include "classA.h"
#include "classB.h"

// class B Method definition
// calling the function ifFileExist() in class B also.

编写主程序的代码 main.cpp

// Contains
#include "Headers.h"
#include "common_utility.h"
#include "classA.h"
#include "classB.h"

// I am using class A and Class B in main program
// calling the function ifFileExist() in Main program also.

当我将整个程序编译为以下命令时:

g++ -std=c++0x classA.cpp classB.cpp main.cpp -o main

我遇到了以下错误:

In function ifFileExist()': classB.cpp:(.text+0x0): multiple definition ofifFileExist()' /tmp/ccHkDT11.o:classA.cpp:(.text+0x2b6e): first defined here

所以我在Headers.h中声明了ifFileExist()函数为extern。
extern bool ifFileExist();

但是我仍然遇到了同样的错误。

我在每个.cpp文件中都包含“Headers.h”。该文件包含基本的c++库。但是我没有为那些头文件得到任何多重定义错误。但是只有在我的自己的函数中,我才会遇到“多重定义”的错误。

我想要在需要使用“common_utility.h”文件时使用它。如果我在主程序中不需要使用common_utility函数,则不应该将其包含。

我希望我的程序能够在以下所有情况下运行:

g++ -std=c++0x classA.cpp main.cpp -o main
g++ -std=c++0x classB.cpp> main.cpp -o main
g++ -std=c++0x classA.cpp classB.cpp main.cpp -o main

在任何情况下,我都不应该出现多重定义错误。现在我该怎么办?


1
寻找关于ODR的重复问题,但没有找到好的答案。我相信它存在,也许有人能找到它? - SergeyA
1
可能是重复问题:多个C++文件导致“多重定义”错误? - BlackDwarf
1
由于我没有找到一个好的重复内容,我将自己写一些东西。 - SergeyA
1
你在头文件中使用了预编译指令吗? - Jepessen
2个回答

31

由于我找不到完整(在我看来)的重复问题,因此我将编写一个(希望是)权威和完整的答案。

什么是ODR,并且为什么我应该关心它

一种定义规则(通常简称为ODR)是一种规则,它指出(简化)程序中使用的任何实体(非正式术语)应该被定义一次,且仅一次。定义超过一次的实体通常会引起编译器或链接器错误,但有时可能被编译器忽略并导致非常难以跟踪的错误。

我不打算在这里正式定义实体,但人们可以将其视为函数、变量或类。在进一步介绍之前,必须非常清楚地理解C++中定义声明之间的区别,因为尽管禁止双重定义,但通常无法避免双重声明。

定义与声明

代码中使用的每个实体都应该在给定的翻译单元(翻译单元通常是一个cpp源文件,以及直接或间接包含在其中的所有标头文件)中进行声明。声明实体的方式根据实体本身的不同而不同。请参见下面有关如何声明不同类型实体的说明。实体通常在标头文件中进行声明。由于大多数复杂应用程序中有多个翻译单元(多个cpp文件),并且不同的cpp文件通常包含相同的标头文件,因此应用程序可能会对许多用途声明具有多个声明。正如我上面所说的,这不是问题。

应用程序中使用的每个实体都必须定义一次且仅一次。这里有一点值得注意,即库(静态和动态)可以留下其中的实体(此时通常称为符号)未定义,并且链接到使用动态库的可执行文件也可以具有未定义的符号。相反,指的是在所有库已经静态或动态链接到它之后、符号解析之后,最终运行的某物。此外,每个定义也同时作为一个声明,这意味着每当您定义某些内容时,您也在声明相同的内容。

与声明一样,根据实体的类型,定义实体的方法也各不相同。以下是基于实体类型如何声明/定义3种基本类型 - 变量、类和函数。

变量

变量使用以下结构进行声明:

extern int x;

这声明了一个变量x,但并没有定义它!紧接着的代码将能够编译通过,但如果尝试在没有任何其他输入文件的情况下链接它(例如使用g++ main.cpp),则会因为未定义的符号产生链接错误:

extern int x;
int main() {
    return x;
}

下面的代码定义了变量x:

int x;

如果将此单行放入文件x.cpp中,并使用g++ x.cpp main.cpp -o test将该文件与上面的main.cpp一起编译/链接,它会在不出问题的情况下编译和链接。甚至可以运行生成的可执行文件,如果在运行可执行文件后检查退出代码,您会注意到它为0。(因为全局变量x将被默认初始化为0)。

函数

通过提供其原型来声明函数。典型的函数声明如下所示:

double foo(int x, double y);

这个结构声明了一个名为foo的函数,返回类型为double,接受两个参数 - 一个是int类型,另一个是double类型。这个声明可以出现多次。

下面的代码定义了上述提到的foo

void foo(int x, double y) {
    return x * y;
}

这个定义在整个应用程序中只能出现一次。

函数定义与变量定义有一个额外的怪癖。 如果上面的foo定义放入头文件foo.h中,然后由两个cpp文件1.cpp2.cpp包含,这些文件使用g ++ 1.cpp 2.cpp -o test编译/链接在一起,您将收到链接器错误,指出foo()被定义了两次。 这可能是通过使用以下形式的foo声明来防止的:

inline void foo(int x, double y) {
    return x * y;
}

这里有一个inline。它告诉编译器, foo 可以被多个 .cpp 文件包含,并且这种包含不应该产生链接错误。编译器有几种选项来实现这一点,但可以信赖它完成工作。请注意,在同一翻译单元中定义两次仍然会导致编译错误!例如,以下代码将会产生编译器错误:

inline void foo() { }
inline void foo() { }

值得注意的是,定义在类内部的任何类方法都是隐式的内联函数,例如:

class A {
public:
    int foo() { return 42; }
};

这里定义了 A::foo(),使用了 inline

声明类的结构如下:

class X;

上述声明声明了X类(此时X被正式称为不完整类型),因此在不需要有关其内容的信息(例如大小或成员)时,可以使用它。例如:

X* p; // OK - no information about class X is actually required to define a pointer to it
p->y = 42; // Error - compiler has no idea if X has any member named `y`

void foo(X x); // OK - compiler does not need to generated any code for this

void foo(X x) { } // Error - compiler needs to know the size of X to generate code for foo to properly read it's argument
void bar(X* x) { } // OK - compiler needs not to know specifics of X for this

类的定义对每个人都非常熟悉,遵循这种结构:

class X {
   public:
   int y;
};

这会使得一个名为X的类被定义,现在可以在任何上下文中使用。重要提示-类定义必须对于每个翻译单元是唯一的,但不必对于每个应用程序是唯一的。也就是说,您可以在每个翻译单元中仅定义X一次,但可以在多个链接在一起的文件中使用它。

如何正确遵循ODR规则

当同一实体在生成的应用程序中定义了多次时,就会发生所谓的ODR违规。大多数情况下,链接器将看到这种违规并报错。但是,在某些情况下,ODR违反不会破坏链接,而是导致错误。例如,当定义全局变量X的同一.cpp文件放入应用程序和按需加载(使用dlopen)的动态库中时,可能会发生这种情况。(我花了几天时间跟踪由此引起的错误。)

ODR违规更常见的原因包括:

在同一作用域的同一文件中定义相同的实体两次

int x;
int x; // ODR violation

void foo() {
   int x;
} // No ODR violation, foo::x is different from x in the global scope

预防措施: 不要这样做。

同一实体定义了两次,但应该是声明。

(in x.h)
int x;

(in 1.cpp)
#include <x.h>
void set_x(int y) {
   x = y;
}

(in 2.cpp)
#include <x.h>
int get_x() {
    return x;
}

尽管上述代码的智慧可疑,但它阐明了ODR规则的重点。在上述代码中,变量x应该在两个文件1.cpp和2.cpp之间共享,但却编码不正确。相反,代码应该如下:

(in x.h)
extern int x; //declare x

(in x.xpp)
int x; // define x

// 1.cpp and 2.cpp remain the same

预防措施 知道你在做什么。当你需要声明实体时,请声明它们,不要定义它们。 如果我们在上面的例子中使用函数而不是变量,像下面这样:

(in x.h)
int x_func() { return 42; }

我们有一个问题,可以用两种方式来解决(如上所述)。我们可以使用inline函数,或者将定义移到cpp文件中:

我们有一个问题,可以用两种方式来解决(如上所述)。我们可以使用inline函数,或者将定义移到cpp文件中:

(in x.h)
int x_func();

(in x.cpp)
int x_func() { return 42; } 

同一个头文件被包含两次,导致相同的类被定义两次。 这是一个有趣的问题。想象一下,你有以下代码:

(in a.h)
class A { };

(in main.cpp)
#include <a.h>
#include <a.h> // compilation error!

以上代码很少以其书写形式出现,但是通过中间文件两次包含相同的文件非常容易:

(in foo.h)
#include <a.h>

(in main.cpp)
#include <a.h>
#include <foo.h>

预防措施传统的解决方法是使用所谓的包含保护器,即特殊的预处理器定义,可以防止重复包含。在这方面,a.h 应该按以下方式重新设计:

(in a.h)
#ifndef INCLUDED_A_H
#define INCLUDED_A_H

class A { };

#endif

上述代码将防止a.h被多次包含到同一翻译单元中,因为INCLUDED_A_H在第一次包含后就会被定义,并将在所有后续的包含中失败#ifndef

有些编译器提供其他控制包含的方式,但迄今为止,包含保护仍然是在不同编译器之间进行统一的方法。


对于在多个源文件中包含的头文件中定义的函数:使用预编译指令可以防止ODR冲突吗?或者即使在头文件中使用了预编译指令,我仍然需要使用“inline”关键字来声明该函数吗? - zardosht
2
@zardosht,是的,你必须使用inline - include guard仅防止在同一翻译单元(cpp文件)中多次包含。 - SergeyA
我看到人们在头文件中定义常量,例如 const double SOME_CONSTANT = 2.5;。这不会导致多重定义问题吗?(我非常认为这与 const 关键字有关。但是您能否解释其中的原因?) - zardosht
1
@zardosht const定义隐式地是内联的(具有内部链接),除非它们之前已经声明为“extern”。请参阅https://en.cppreference.com/w/cpp/language/storage_duration以获取更多详细信息。 - SergeyA

0

在实际编译源代码之前,需要从 .cpp 文件生成编译单元。这基本上意味着计算所有预处理指令:所有的 #include 将被替换为所包含文件的内容,所有 #define 的值将被替换为相应的表达式,所有 #if 0 ... #endif 将被删除等。因此,在此步骤之后,您将获得两个不带任何预处理指令的 C++ 代码片段,它们都有相同函数 bool ifFileExist() 的定义,这就是为什么会出现多重定义错误的原因。

快速解决方案是将其标记为 inline bool ifFileExist()。基本上,您要求编译器用函数本身的内容替换所有相应的函数调用。

另一种方法是将函数的声明留在 common_utility.h 中,并将定义移动到 common_utility.cpp 中。


1
“inline”关键字和实际的内联只有间接关系。通过另一个来解释其中一个是不正确的。编译器可以内联未声明为“inline”的函数,也可以无法内联已声明为“inline”的函数。这对程序语义没有影响。 - n. m.
2
基本上,你要求编译器用函数本身的内容替换所有相应的函数调用,这是完全错误的。关键字“inline”与函数内联无关。 - SergeyA

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