7-10 在多个文件中共享全局常量(使用内联变量)

在某些应用程序中,特定符号常量可能需要在代码中多处使用(而非仅限于单一位置)。这类常量包括物理或数学中不变的常数(如圆周率π或阿伏伽德罗常数),以及应用程序特有的“调优”参数(如摩擦系数或重力系数)。与其在每个需要这些常量的文件中重复定义(这违反了“不要重复自己”原则),不如在中心位置统一声明,并在需要时调用。这样,若需修改常量,只需在单一位置进行变更,修改内容即可自动传播至全局。

本节将探讨实现此目标的最常用方法。


全局常量作为内部变量

在C++17之前,最简单且最常见的解决方案如下:

  1. 创建一个头文件来存储这些常量
  2. 在此头文件中定义一个命名空间(详见第7.2课——用户定义命名空间与作用域解析运算符
  3. 将所有常量置于命名空间内(确保它们是 constexpr)
  4. 在需要处包含该头文件

例如:

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;
}

image

当此头文件被#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;
}

image

现在符号常量将仅在 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;
}

image

我们可以将constants.h包含到任意数量的代码文件中,但这些变量仅会被实例化一次,并在所有代码文件中共享。
此方法仍存在一个缺点:若任何常量值发生变更,则所有包含该常量头文件的文件都需重新编译。
优点:

  • 可在包含它们的任何翻译单元中用于常量表达式。
  • 每个变量仅需一份副本。

缺点:

  • 仅适用于C++17及更高版本。
  • 修改头文件内容时,所有包含该头文件的文件均需重新编译。

最佳实践
若需使用全局常量且编译器支持C++17,建议在头文件中定义内联constexpr全局变量。

重要提示
使用std::string_view实现constexpr字符串。相关内容详见第5.8课——std::string_view介绍

相关内容
第7.12课--作用域、存储期与链接总结 系统梳理了各类变量的作用域、存续期及链接规则。

posted @ 2026-02-23 09:03  游翔  阅读(1)  评论(0)    收藏  举报