某些应用场景下,若干符号常量需要在整个代码中使用(不限于单一位置)。这些常量可以是永不改变的物理或数学常数(如 π 或阿伏伽德罗常数),也可以是应用专属的“调谐”值(如摩擦系数或重力系数)。与其在每个需要它们的文件中重复定义(违反“不要重复自己”原则),不如在单一中心位置声明,然后在任何需要的地方使用。如此,一旦需要修改,只需改动一处,即可传播到所有位置。
本课介绍实现这一目标的常见方法。
方法一:将全局常量设为内部变量(C++17 之前)
在 C++17 之前,最简单且最常用的方案如下:
- 创建一个头文件存放这些常量;
- 在该头文件中定义一个命名空间(参见《用户自定义命名空间与作用域解析运算符》);
- 将常量全部放在该命名空间内,并确保为
constexpr
; - 任何需要使用常量的文件只需
#include
该头文件。
示例:
constants.h
#ifndef CONSTANTS_H
#define CONSTANTS_H
// 自定义命名空间用于存放常量
namespace constants
{
// 全局常量默认具有内部链接
constexpr double pi { 3.14159 };
constexpr double avogadro { 6.0221413e23 };
constexpr double myGravity { 9.2 }; // m/s² —— 该行星重力较小
// ……其他相关常量
}
#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;
}
当此头文件被 #include
进 .cpp
文件时,其中定义的变量将在包含点被复制到该源文件。由于这些变量位于函数外,它们被视为该文件内的全局变量,因此可在文件任意位置使用。
由于 const
/constexpr
全局变量默认具有内部链接,每个 .cpp
文件都会获得一份独立的变量副本,链接器无法看到它们。在绝大多数情况下,编译器会直接将 constexpr
变量优化掉。
此方案简单(且对小项目足够),但存在以下问题:
- 每当
constants.h
被不同源文件包含一次,这些变量就被复制一次。若被 20 个文件包含,每个变量便重复 20 次。
头文件保护仅防止同一文件多次包含,无法阻止跨文件的一次包含。 - 修改任一常量值需重新编译所有包含该头文件的源文件,大型项目重编译耗时。
- 若常量体积较大且无法被优化消除,会占用大量内存。
优点:
- C++17 之前即可使用;
- 可在任何包含该头文件的翻译单元中用于常量表达式。
缺点:
- 头文件改动需重编译所有包含者;
- 每个翻译单元各持一份变量副本。
方法二:将全局常量设为外部变量
若需频繁修改或新增常量,上述方案可能不便。
另一种做法是把常量改为外部变量:在单一 .cpp
文件中定义(确保仅一份定义),在头文件中放置前向声明供其他文件使用。
constants.cpp
#include "constants.h"
namespace constants
{
// 使用 extern 确保具有外部链接
extern constexpr double pi { 3.14159 };
extern constexpr double avogadro { 6.0221413e23 };
extern constexpr double myGravity { 9.2 }; // m/s²
}
constants.h
#ifndef CONSTANTS_H
#define CONSTANTS_H
namespace constants
{
// 由于实际变量位于命名空间内,前向声明亦需在命名空间内
// 变量无法前向声明为 constexpr,可前向声明为(运行时)const
extern const double pi;
extern const double avogadro;
extern const double myGravity;
}
#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.cpp
实例化一次,所有使用均链接到该实例;修改 constants.cpp
只需重编译该文件。
然而,该方法存在以下缺点:
- 只有定义所在文件(
constants.cpp
)内,这些常量才是constexpr
;其他文件仅看到前向声明,无法用于常量表达式。 - 编译器无法进行与常量表达式同等程度的优化。
关键洞察
若需在编译期上下文中使用变量(如数组大小),编译器必须看到变量定义(而非仅前向声明)。
由于编译器以源文件为单位编译,.cpp
中的定义对编译 main.cpp
时不可见,因此 constexpr
变量无法拆分到头文件和源文件,必须定义于头文件。
鉴于上述缺点,推荐仍把常量定义在头文件中(见前一节或下一节)。若常量值频繁变动导致重编译耗时,可临时将相关常量移至 .cpp
文件(采用本方法)。
优点:
- C++17 之前可用;
- 每变量仅需一份实例;
- 修改常量值仅重编译一个文件。
缺点:
- 前向声明与定义需分处两文件并保持同步;
- 变量在定义文件外不可用于常量表达式。
方法三:C++17 内联变量(推荐)
7.9 课《内联函数与变量》介绍过:内联变量允许存在多份定义,只要这些定义完全一致。
将 constexpr
变量标记为 inline
,即可在头文件中定义,再 #include
到任意 .cpp
,既避免 ODR 违规,又消除变量重复。
提醒constexpr
函数隐式内联,但 constexpr
变量不隐式内联;若要内联 constexpr
变量,须显式添加 inline
。
关键洞察
内联变量默认具有外部链接,以便链接器去重;
非内联 constexpr
变量具有内部链接,若被多文件包含,每个翻译单元各持副本,不违反 ODR(因对链接器不可见)。
constants.h
#ifndef CONSTANTS_H
#define CONSTANTS_H
// 自定义命名空间存放常量
namespace constants
{
inline constexpr double pi { 3.14159 }; // 注意:inline constexpr
inline constexpr double avogadro { 6.0221413e23 };
inline constexpr double myGravity { 9.2 }; // m/s²
// ……其他相关常量
}
#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,首选在头文件中定义 inline constexpr
全局变量。
提醒
对于 constexpr
字符串,请使用 std::string_view
(参见 5.8 课《std::string_view 简介》)。
相关阅读
各类变量的作用域、存储期与链接属性的完整总结,参见《作用域、存储期与链接小结》。