在C++中定义全局常量

97

我想在C++中定义一个常量,使其在多个源文件中可见。 我可以想象以下方法在头文件中定义它:

  1. #define GLOBAL_CONST_VAR 0xFF
  2. int GLOBAL_CONST_VAR = 0xFF;
  3. 返回值为该值的某些函数(例如int get_GLOBAL_CONST_VAR()
  4. enum { GLOBAL_CONST_VAR = 0xFF; }
  5. const int GLOBAL_CONST_VAR = 0xFF;
  6. extern const int GLOBAL_CONST_VAR;并在一个源文件中const int GLOBAL_CONST_VAR = 0xFF;

选项(1) - 绝对不是您想使用的选项

选项(2) - 使用头文件在每个对象文件中定义变量的实例

选项(3) - 在大多数情况下我认为太过复杂

选项(4) - 在许多情况下可能不好,因为枚举没有具体类型(C ++ 0X将添加定义类型的可能性)

因此,在大多数情况下,我需要在(5)和(6)之间进行选择。

我的问题:

  1. 您更喜欢(5)还是(6)?
  2. 为什么(5)可以,而(2)不行?

2
5与2:“const”意味着内部链接。当您将此版本5头文件包含到多个翻译单元中时,您不会违反“一个定义规则”。此外,const允许编译器进行“常量折叠”,而非const变量的值可能会更改。选项6是错误的。您还需要在cpp文件中使用“extern”来强制使用外部链接,否则您将获得链接器错误。选项6具有隐藏值的优点。但它也使常量折叠变得不可能。 - sellibitze
10个回答

83

绝对选择选项5-它是类型安全的,并允许编译器进行优化(不要获取该变量的地址 :)如果它在头文件中,请将其放入命名空间中以避免污染全局范围:

// header.hpp
namespace constants
{
    const int GLOBAL_CONST_VAR = 0xFF;
    // ... other related constants

} // namespace constants

// source.cpp - use it
#include <header.hpp>
int value = constants::GLOBAL_CONST_VAR;

8
当我试图在多个源文件中包含 header.hpp 时,出现了重新定义错误。 - LRDPRDX
2
不确定为什么这个还会被点赞 - 已经过去了将近十年,但现在我们有了constexpr和类型化枚举来处理这样的事情。 - Nikolai Fetissov
非常清晰的答案,谢谢。 - Stefan
应该使用#include <>来包含依赖的头文件(例如Boost,Qt等)。我会将该包含改为#include "header.hpp",以明确该头文件属于源代码。 - undefined

38

(5) 表达了您想要表达的内容,而且通常能让编译器优化掉它。然而,(6) 无法使编译器进行优化,因为编译器并不知道您将来是否会对其进行更改。


1
另一方面,5作为ODR的违规行为在技术上是非法的。然而,大多数编译器将忽略它。 - Joel
嗯,我更愿意认为这根本没有定义任何东西,我只是告诉编译器给一个数字起一个漂亮的名字。就所有目的而言,这就是 (5),这意味着在运行时没有额外开销。 - Blindy
3
(5)是否违反了ODR?如果是,那么(6)更可取。为什么在(6)的情况下编译器“不知道你是否会更改它”?extern const int ...const int ...都是常量,对吗? - D.Shawley
4
据我所知,在5)和6)之间,只有当常量类型不基于int时才允许使用6),不允许使用5)。 - Klaim
12
没有ODR违规,常量对象默认为静态的。 - avakar
显示剩余2条评论

28
(5)比(6)更好,因为它在所有翻译单元中将 GLOBAL_CONST_VAR 定义为整数常量表达式 (ICE)。例如,您可以将其用作所有翻译单元中的数组大小和 case 标签。对于(6),GLOBAL_CONST_VAR仅在定义它的翻译单元中成为ICE,并且仅在定义点之后才能成为ICE,在其他翻译单元中它将不起作用。
但是,请注意,(5)会为 GLOBAL_CONST_VAR 提供内部链接,这意味着每个翻译单元中的 "地址标识" 都不同,即 &GLOBAL_CONST_VAR 在每个翻译单元中都会给您一个不同的指针值。在大多数用例中,这并不重要,但是如果您需要一个具有一致全局 "地址标识" 的常量对象,则必须选择(6),从而损失常量的 ICE 特性。
此外,当常量的 ICE 特性不是问题时(不是整数类型),且类型的大小变大时(不是标量类型),通常情况下(6)比(5)更好。
(2)不行,因为(2)中的 GLOBAL_CONST_VAR 默认具有外部链接。如果将其放在头文件中,通常会得到多个定义的GLOBAL_CONST_VAR,这是一个错误。C++中的 const 对象默认具有内部链接,这就是为什么(5)起作用的原因(也是为什么,在每个翻译单元中,您都会得到一个单独的、独立的 GLOBAL_CONST_VAR)。
从C++17开始,您可以声明:
inline extern const int GLOBAL_CONST_VAR = 0xFF;

在头文件中这样做会导致在所有翻译单元中产生一个ICE(就像方法(5)一样),同时保持GLOBAL_CONST_VAR的全局地址标识 - 在所有翻译单元中它将具有相同的地址。


13

如果您使用C++11或更高版本,请尝试使用编译时常量:

constexpr int GLOBAL_CONST_VAR{ 0xff };

1
在我看来,这是解决这个问题唯一令人满意的方案。 - lanoxx

10

C++17 inline变量

这个厉害的C++17新特性使我们可以:

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

inline constexpr int notmain_i = 42;

const int* notmain_func();

#endif

notmain.cpp

#include "notmain.hpp"

const int* notmain_func() {
    return &notmain_i;
}

编译并运行:

g++ -c -o notmain.o -std=c++17 -Wall -Wextra -pedantic notmain.cpp
g++ -c -o main.o -std=c++17 -Wall -Wextra -pedantic main.cpp
g++ -o main -std=c++17 -Wall -Wextra -pedantic main.o notmain.o
./main

GitHub上游

另请参阅: 内联变量是如何工作的?

C++标准中的内联变量

C++标准保证地址相同。 C++17 N4659标准草案 10.1.6 "The inline specifier":

6 具有外部链接的内联函数或变量在所有翻译单元中必须具有相同的地址。

cppreference https://en.cppreference.com/w/cpp/language/inline 解释说,如果没有给出 static,则它具有外部链接。

内联变量实现

我们可以观察到它是如何实现的:

nm main.o notmain.o

其中包含:

main.o:
                 U _GLOBAL_OFFSET_TABLE_
                 U _Z12notmain_funcv
0000000000000028 r _ZZ4mainE19__PRETTY_FUNCTION__
                 U __assert_fail
0000000000000000 T main
0000000000000000 u notmain_i

notmain.o:
0000000000000000 T _Z12notmain_funcv
0000000000000000 u notmain_i

man nm中提到了关于u的内容:

"u"符号是一种独特的全局符号。这是GNU ELF符号绑定标准集的扩展内容。对于这样的符号,动态链接器将确保在整个进程中只有一个带有该名称和类型的符号在使用。

因此我们可以看到,ELF为此专门设计了一个扩展内容。

在GCC 7.4.0和Ubuntu 18.04上测试通过。


5

如果它是一个常量,那么你应该将其标记为常量 - 这就是为什么在我看来2是不好的原因。

编译器可以利用值的const属性来扩展一些数学运算,以及使用该值的其他操作。

在5和6之间的选择 - 嗯;对我来说,5感觉更好。

在6)中,该值与其声明不必要地分离。

通常我会有一个或多个仅在其中定义常量等内容的头文件,然后没有其他“聪明”的东西 - 漂亮的轻量级头文件可以轻松地包含在任何地方。


3
(6)并非不必要的分离,而是一种有意的选择。如果您有许多大的常量,在可执行文件中不将它们声明为(6),则会浪费很多空间。这可能在数学库中发生...浪费可能不到100k,但有时甚至那也很重要。(某些编译器有其他方法来解决这个问题,我认为MSVC有一个“once”属性或类似的东西。) - Dan Olson
使用(5)你不能确定它会保持const(你总是可以强制转换掉const)。这就是为什么我仍然更喜欢枚举类型。 - fmuecke
@丹·奥尔森 - 你说得非常好 - 我的答案是基于这个事实:涉及到的类型是 int; 但是当处理更大的值时,extern声明确实是一个更好的方案。 - Andras Zoltan
@fmuecke - 是的,你说得对 - 在这种情况下,枚举值确实可以防止这种情况发生。但是,这是否意味着我们应该总是以这种方式保护我们的值免受写入?如果程序员想要滥用代码,有很多地方可以使用(target_type*)((void*)&value)强制转换造成灾难,我们无法捕捉到,有时我们必须放心信任他们;并且我们自己也是,不是吗? - Andras Zoltan
1
@fmuecke 声明为const的变量不能被程序更改(尝试这样做是未定义的行为)。const_cast仅在原始变量未声明为const的情况下定义(例如,将非const值作为const&传递到函数中)。 - David Stone

4
回答你的第二个问题:
(2)是不合法的,因为它违反了“一次定义规则”(One Definition Rule)。它在每个包含它的文件中定义了 GLOBAL_CONST_VAR ,即超过一次。 (5)是合法的,因为它不受“一次定义规则”的限制。每个 GLOBAL_CONST_VAR 都是单独的定义,局部于包含它的文件中。所有这些定义当然共享相同的名称和值,但它们的地址可能不同。

3
const int GLOBAL_CONST_VAR = 0xFF;

因为它是一个常量!

(因为它的值不会改变)

1
而且它不会像宏一样被处理,这使得使用它进行调试更加容易。 - kayleeFrye_onDeck
-1,当在多个源文件中包含头文件时,这将导致重新定义警告/错误。此外,这个答案是 Nikolai Fetissov 的答案的重复。 - lanoxx

1

这取决于您的需求。对于大多数正常使用情况,(5) 是最好的选择,但经常会导致每个对象文件中都占用存储空间的常量。在重要情况下,(6) 可以解决这个问题。

如果您的优先级是确保不分配存储空间,那么(4) 也是一个不错的选择,但它仅适用于整数常量。


0
#define GLOBAL_CONST_VAR 0xFF // this is C code not C++
int GLOBAL_CONST_VAR = 0xFF; // it is not constant and maybe not compilled
Some function returing the value (e.g. int get_LOBAL_CONST_VAR()) // maybe but exists better desision
enum { LOBAL_CONST_VAR = 0xFF; } // not needed, endeed, for only one constant (enum elms is a simple int, but with secial enumeration)
const int GLOBAL_CONST_VAR = 0xFF; // it is the best
extern const int GLOBAL_CONST_VAR; //some compiller doesn't understand this

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