常量变量在头文件中无法正常工作

60

如果我在头文件中这样定义我的常量变量...

extern const double PI = 3.1415926535;
extern const double PI_under_180 = 180.0f / PI;
extern const double PI_over_180 = PI/180.0f;

我遇到了以下错误

1>MyDirectX.obj : error LNK2005: "double const PI" (?PI@@3NB) already defined in main.obj
1>MyDirectX.obj : error LNK2005: "double const PI_under_180" (?PI_under_180@@3NB) already defined in main.obj
1>MyDirectX.obj : error LNK2005: "double const PI_over_180" (?PI_over_180@@3NB) already defined in main.obj
1>MyGame.obj : error LNK2005: "double const PI" (?PI@@3NB) already defined in main.obj
1>MyGame.obj : error LNK2005: "double const PI_under_180" (?PI_under_180@@3NB) already defined in main.obj
1>MyGame.obj : error LNK2005: "double const PI_over_180" (?PI_over_180@@3NB) already defined in main.obj

但是,如果我从头文件中删除这些常量,并将它们放在包含头文件的文档中,就像这样...

const double PI = 3.1415926535;
const double PI_under_180 = 180.0f / PI;
const double PI_over_180 = PI/180.0f;

它可以工作

有人知道我可能做错了什么吗?

谢谢


你应该写180.0而不是180.0f,因为你正在处理双精度浮点数而不是单精度浮点数。此外,将PI重命名为更独特的名称。PI在许多库中用作宏,如果你使用它,可能会得到奇怪的结果。 - thebretness
去掉 extern 就没问题了。 - sellibitze
1
在C语言中,const对象默认具有外部链接性,这意味着extern不会改变任何东西。 - AnT stands with Russia
11个回答

140
问题在于你在头文件中定义了具有外部链接的对象。可预测的是,一旦将该头文件包含到多个翻译单元中,你将得到具有外部链接的同一对象的多个定义,这是一个错误。
正确的方法取决于你的意图。
  1. 你可以将定义放入头文件中,但确保它们具有内部链接。

    在 C 中,这需要一个显式的 static

  2. static const double PI = 3.1415926535; 
    static const double PI_under_180 = 180.0f / PI; 
    static const double PI_over_180 = PI/180.0f; 
    

    在C++中,static关键字是可选的(因为在C++中,默认情况下const对象具有内部链接)

    const double PI = 3.1415926535; 
    const double PI_under_180 = 180.0f / PI; 
    const double PI_over_180 = PI/180.0f; 
    
  3. 或者你可以将纯粹的非定义声明放在头文件中,将定义放在一个(且仅有一个)实现文件中。

    头文件中的声明必须明确包含extern修饰符,且不应该有初始化器。

  4. extern const double PI; 
    extern const double PI_under_180; 
    extern const double PI_over_180; 
    

    在一个实现文件中,定义应该如下所示。

    const double PI = 3.1415926535; 
    const double PI_under_180 = 180.0f / PI; 
    const double PI_over_180 = PI/180.0f; 
    

    如果在同一翻译单元中,上述声明在定义之前,那么定义中的显式extern是可选的。

    你将选择哪种方法取决于你的意图。

    第一种方法使编译器更容易优化代码,因为它可以看到每个翻译单元中常量的实际值。但同时,在概念上,您会在每个翻译单元中获得单独和独立的常量对象。例如,&PI 在每个翻译单元中会评估为不同的地址。

    第二种方法创建真正的全局常量,即整个程序共享的唯一常量对象。例如,&PI 在每个翻译单元中都会评估为相同的地址。但在这种情况下,编译器只能在一个翻译单元中实际看到这些值,这可能会妨碍优化。


    从C++17开始,你有了第三种选项,它将 "两全其美" 的方案结合在一起: 内联变量。内联变量可以安全地在头文件中定义,尽管具有外部链接。

    inline extern const double PI = 3.1415926535; 
    inline extern const double PI_under_180 = 180.0f / PI; 
    inline extern const double PI_over_180 = PI/180.0f; 
    
    在这种情况下,您将获得一个命名的常量对象,其初始化值在所有翻译单元中都可见。同时,该对象具有外部链接,即它具有全局地址标识(&PI 在所有翻译单元中相同)。
    当然,像这样的东西可能只适用于某些奇特的目的(C++中大多数用例需要第一种变体),但该功能是存在的。

1
+1. 只是想提一下,在 C 语言中,static const 变量不像你需要用于数组大小的常量表达式那样是一个常量表达式。我猜这就是为什么在 C 语言中 #define 更受欢迎的原因。 - sellibitze
3
C++中的常量对象默认具有内部链接。 - Michael Burr
@sellibitze:是的,但这主要是在整数常量方面成为问题。对于非整数常量对象的影响可以忽略不计,如果存在的话。 - AnT stands with Russia
喜欢在Stackoverflow上找到完整的答案,通常比文档更好阅读。 - Spence

9

extern的意思是变量的“真正”定义在别处,编译器应该相信在链接时会连接起来。将定义与extern内联是奇怪的,这就是破坏程序的原因。如果您想让它们成为extern,请确保在程序的其他地方仅定义一次。


6
对于这些变量来说,extern存储类几乎肯定是你遇到问题的原因。如果你去掉它,代码可能会没问题(至少在这方面上不会有问题)。
编辑:我刚刚注意到你同时将这个标记为C和C++。在这方面,C和C++确实非常不同(但从错误消息中,显然你正在编译为C++,而不是C)。在C++中,你想要删除extern,因为(默认情况下)const变量具有static存储类。这意味着每个源文件(翻译单元)将获得自己的变量“副本”,并且不会在不同文件中的定义之间产生任何冲突。由于你(可能)只使用这些值,而不将它们视为变量,因此拥有多个“副本”不会造成任何损害——它们都不会分配存储空间。
在C中,extern则相当不同,去掉extern不会产生任何真正的区别,因为它们默认为extern。在这种情况下,你真的需要在一个地方初始化这些变量,并在头文件中声明它们为外部变量。或者,你可以添加C++将从头文件中删除extern时/如果添加的static存储类。

2
问题在于您正在头文件中初始化变量;这会创建一个定义声明,在包含该头文件的每个文件中都会重复出现,因此会出现多个定义错误。
您需要在头文件中使用非定义声明(没有初始化程序),并将定义声明放在一个实现文件中。

如果我们在头文件开头使用#ifndef #define,并且仍然定义常量,那么这样做就足够了吗? - quantum231
@quantum231:你的意思是,使用宏代替const变量吗? - John Bode

2
下面的许多回答是错误的。像 sellibitze 在他的评论中所说的那样,那些正确的回答都告诉你要删除 extern
因为这些被声明为 const,所以在头文件中定义没有问题。对于内置类型,C++会将const定义内联,除非您尝试获取其地址(指向const的指针),此时它将使用 static 链接进行实例化,您可能会在单独的模块中得到多个实例化,但是除非您期望所有指向相同const的指针具有相同的地址,否则这不是问题。

MSVC的行为可能与您的预期不同。如果您在头文件中将变量声明为“const”,则可能会出现重复符号。您需要同时使用“static const”以确保内部链接。也许这是编译器的一个错误。 - jww
@jww:在问题中报告说它没有使用静态变量也能工作。C的行为确实有所不同,但由于问题中的代码已经链接,必须使用了C++编译器。然而,该问题标记为C和C ++,因此可以说我应该涵盖两者,但该问题并不涉及C和C ++之间的区别,所以我假设标记错误。该问题已经七年了,因此不需要更新。 - Clifford

1

看起来这个头文件被多次包含了。你需要添加保护。

在每个头文件的顶部,你应该有类似以下的内容:

#ifndef MY_HEADER_FILE_NAME_H
#define MY_HEADER_FILE_NAME_H

...

// at end of file
#endif

如果您正在使用g++或MSVC,则可以添加以下内容:
#pragma once

在每个头文件的顶部,但这并不是100%可移植的。

此外,您不应该在头文件中定义常量,只能声明它们:

// In header file
extern const int my_const;


// In one source file
const int my_const = 123;

5
Include guards只能防止同一文件中的重复包含。如果缺少它们,在编译时会产生问题。他在链接时遇到了问题,因为尝试在多个文件中分别定义相同的符号。 - Jerry Coffin
我在我的答案中解决了那个问题。 - Peter Alexander

1
你需要在头文件中声明常量,然后在其中一个代码文件中定义它们。如果你没有在任何地方声明它们,那么当链接器试图将声明与实际定义联系起来时,就会出现链接错误。你也可以使用 #ifdef 语句在头文件中有一个定义。
确保它们在每个需要它们的人都包含的头文件中声明,并确保它们只被定义一次。
雅各布

1
如果你想在头文件中定义常量,请使用 static const。如果你使用 extern,链接器会因为多个定义而抱怨,因为每个包含源文件都会为变量提供内存,如果你赋值的话。

0

我也遇到了这个问题:

static const uint64 GameTexSignature = 0x0a1a0a0d58455489;

当在头文件中定义时,它在Linux上无法编译。在MSVC上编译正常。对我有效的解决方法是将其更改为:

static constexpr uint64 GameTexSignature = 0x0a1a0a0d58455489;

这只对uint64常量是必需的,而不是任何uint32常量。我认为下面的解释可以说明问题,但这也意味着Linux编译器不认为const uint64是一个整数常量。

https://exceptionshub.com/how-to-declare-a-static-const-char-in-your-header-file.html

祝福 约翰


0

虽然这是一个老问题,但确实缺少一个有用的答案。

通过将静态常量包装在“虚拟”类模板中,可以欺骗MSVC接受头文件中的静态常量:

template <typename Dummy = int>
struct C {
     static const double Pi;
};

template <typename Dummy = int>
const double C<Dummy>::Pi = 3.14159;

现在,C<>::PI 可以从其他地方访问。没有重新定义的投诉;常量可以直接在每个编译单元中访问,而无需进行复杂的链接时间优化。宏可以用来进一步美化这种方法(尽管宏是邪恶的)。

使用这种方法有什么缺点吗? - PolyMesh
正如前面提到的,有一点语法上的缺陷 - 在整个代码中都必须将Pi称为C<>::Pi。但是,松散的全局常量本来就应该避免使用(因此MathConst<>::Pi只比MathConst::Pi稍差一些)。 - oakad

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