7-10 在多个文件中共享全局常量(使用内联变量)
在某些应用程序中,特定符号常量可能需要在代码中多处使用(而非仅限于单一位置)。这类常量包括物理或数学中不变的常数(如圆周率π或阿伏伽德罗常数),以及应用程序特有的“调优”参数(如摩擦系数或重力系数)。与其在每个需要这些常量的文件中重复定义(这违反了“不要重复自己”原则),不如在中心位置统一声明,并在需要时调用。这样,若需修改常量,只需在单一位置进行变更,修改内容即可自动传播至全局。
本节将探讨实现此目标的最常用方法。
全局常量作为内部变量
在C++17之前,最简单且最常见的解决方案如下:
- 创建一个头文件来存储这些常量
- 在此头文件中定义一个命名空间(详见第7.2课——用户定义命名空间与作用域解析运算符)
- 将所有常量置于命名空间内(确保它们是 constexpr)
- 在需要处包含该头文件
例如:
constants.h:
#ifndef CONSTANTS_H
#define CONSTANTS_H
// Define your own namespace to hold constants
namespace constants
{
// Global constants have internal linkage by default
constexpr double pi { 3.14159 };
constexpr double avogadro { 6.0221413e23 };
constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
// ... other related constants
}
#endif
然后使用作用域解析运算符(::),左侧为命名空间名称,右侧为变量名称,以便在.cpp文件中访问常量:
main.cpp:
#include "constants.h" // include a copy of each constant in this file
#include <iostream>
int main()
{
std::cout << "Enter a radius: ";
double radius{};
std::cin >> radius;
std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';
return 0;
}

当此头文件被#include到.cpp文件中时,头文件中定义的每个变量都会在包含点被复制到该代码文件中。由于这些变量位于函数外部,它们在被包含的文件中会被视为全局变量,因此你可以在该文件的任意位置使用它们。
由于 const 全局变量具有内部链接特性,每个 .cpp 文件都会获得链接器无法看到的独立全局变量副本。在大多数情况下,由于这些变量是 constexpr,编译器会直接优化掉它们。
虽然这种方式简单(且适用于小型程序),但每次将constants.h包含到不同代码文件时,这些变量都会被复制到包含文件中。因此,若constants.h被包含到20个不同代码文件中,这些变量将被复制20次。头文件保护机制无法阻止这种情况,因为它们仅能防止单个包含文件重复包含同一头文件,却无法阻止不同代码文件各自包含同一头文件。这带来了两大挑战:
- 修改单个常量值将导致所有包含常量头文件的文件都需要重新编译,大型项目可能因此耗费大量重建时间。
- 若常量体积庞大且无法被优化掉,将消耗大量内存。
优势:
- 在C++17之前即可使用。
- 可在包含它们的任何翻译单元中作为常量表达式使用。
缺点:
- 修改头文件中的任何内容都需要重新编译包含该头文件的文件。
- 每个包含该头文件的翻译单元都会获得该变量的独立副本。
全局常量作为外部变量
若需频繁修改常量值或添加新常量,前述方案可能存在问题——至少在方案稳定前如此。
避免此类问题的方案是将常量转换为外部变量,这样就能通过单个变量(仅初始化一次)实现跨文件共享。具体方法如下:在.cpp文件中定义常量(确保定义仅存在于单一位置),并在头文件中进行声明(供其他文件包含)。
constants.cpp:
#include "constants.h"
namespace constants
{
// We use extern to ensure these have external linkage
extern constexpr double pi { 3.14159 };
extern constexpr double avogadro { 6.0221413e23 };
extern constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
}
constants.h:
#ifndef CONSTANTS_H
#define CONSTANTS_H
namespace constants
{
// Since the actual variables are inside a namespace, the forward declarations need to be inside a namespace as well
// We can't forward declare variables as constexpr, but we can forward declare them as (runtime) const
extern const double pi;
extern const double avogadro;
extern const double myGravity;
}
#endif
在代码文件中的使用方式保持不变:
main.cpp:
#include "constants.h" // include all the forward declarations
#include <iostream>
int main()
{
std::cout << "Enter a radius: ";
double radius{};
std::cin >> radius;
std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';
return 0;
}

现在符号常量将仅在 constants.cpp 中实例化一次,而非在每个包含 constants.h 的代码文件中实例化。所有对这些常量的引用都将链接到 constants.cpp 中实例化的版本。对 constants.cpp 的任何修改仅需重新编译 constants.cpp。
然而此方法存在两点缺陷:首先,由于仅变量定义具有constexpr属性(前向声明不具备且无法具备),这些常量仅在其实际定义的文件(constants.cpp)内为常量表达式。在其他文件中,编译器仅能识别前向声明(该声明未定义 constexpr 值且需由链接器解析)。这意味着在定义文件之外,这些变量无法用于常量表达式。其次,由于常量表达式通常比运行时表达式更易优化,编译器可能无法充分优化这些变量。
关键洞察
要在编译时上下文中使用变量(例如数组大小),编译器必须看到该变量的定义(而不仅仅是前向声明)。
由于编译器对每个源文件单独编译,它只能看到当前编译文件中出现的变量定义(包括任何包含的头文件)。例如,当编译器编译 main.cpp 时,constants.cpp 中的变量定义不可见。因此,constexpr 变量不能拆分到头文件和源文件中,必须在头文件中定义。
鉴于上述缺点,建议将常量定义在头文件中(可参照前文或下文方法)。若发现常量值频繁变更(例如程序调试阶段),导致编译耗时过长,可临时将问题常量移至.cpp文件(采用此方法)进行处理。
优点:
• 兼容C++17之前的版本
• 每个变量仅需一份副本
• 常量值变更时仅需重新编译单个文件
缺点:
• 前向声明与变量定义分属不同文件,需保持同步
• 变量在定义文件之外无法用于常量表达式
全局常量作为内联变量(C++17)
在第7.9节——内联函数与变量中,我们介绍了内联变量。这类变量可以拥有多个定义,前提是这些定义完全相同。通过将constexpr变量设为内联,我们可以在头文件中定义它们,然后通过#include引入到任何需要它们的.cpp文件中。这既避免了ODR违规问题,又规避了变量重复定义的弊端。
提醒
constexpr 函数默认隐式内联,但 constexpr 变量不具备隐式内联特性。若需实现内联 constexpr 变量,必须显式标记为 inline。
关键要点
内联变量默认具有外部链接,因此对链接器可见。此特性对链接器消除重复定义至关重要。
非内联 constexpr 变量具有内部链接。若被多个翻译单元包含,每个翻译单元将获得该变量的独立副本。这不构成 ODR 违规,因为它们不会暴露给链接器。
constants.h:
#ifndef CONSTANTS_H
#define CONSTANTS_H
// define your own namespace to hold constants
namespace constants
{
inline constexpr double pi { 3.14159 }; // note: now inline constexpr
inline constexpr double avogadro { 6.0221413e23 };
inline constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
// ... other related constants
}
#endif
main.cpp:
#include "constants.h"
#include <iostream>
int main()
{
std::cout << "Enter a radius: ";
double radius{};
std::cin >> radius;
std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';
return 0;
}

我们可以将constants.h包含到任意数量的代码文件中,但这些变量仅会被实例化一次,并在所有代码文件中共享。
此方法仍存在一个缺点:若任何常量值发生变更,则所有包含该常量头文件的文件都需重新编译。
优点:
- 可在包含它们的任何翻译单元中用于常量表达式。
- 每个变量仅需一份副本。
缺点:
- 仅适用于C++17及更高版本。
- 修改头文件内容时,所有包含该头文件的文件均需重新编译。
最佳实践
若需使用全局常量且编译器支持C++17,建议在头文件中定义内联constexpr全局变量。
重要提示
使用std::string_view实现constexpr字符串。相关内容详见第5.8课——std::string_view介绍。
相关内容
第7.12课--作用域、存储期与链接总结 系统梳理了各类变量的作用域、存续期及链接规则。

浙公网安备 33010602011771号