由于我找不到完整(在我看来)的重复问题,因此我将编写一个(希望是)权威和完整的答案。
什么是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.cpp
和2.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;
void foo() {
int x;
}
预防措施: 不要这样做。
同一实体定义了两次,但应该是声明。
(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>
以上代码很少以其书写形式出现,但是通过中间文件两次包含相同的文件非常容易:
(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
。
有些编译器提供其他控制包含的方式,但迄今为止,包含保护仍然是在不同编译器之间进行统一的方法。