在 C++ 中,常量(constant)是指值在程序运行期间不可被修改的量。根据"不可修改"的发生时机和约束强度,C++ 提供了多种定义常量的手段。本详解将聚焦两大核心关键字:const 和 constexpr,帮助你从初识走向深层理解。
1. 常量的本质与基础手段
字面量:直接写在代码中的值,如 42、3.14、'a'、"hello"。
宏定义:#define PI 3.14159(预处理器阶段文本替换,无类型检查,现代 C++ 不推荐)。
枚举常量:enum { SIZE = 100 }; 或 enum class,编译期确定值。
const 变量:带类型的常量,运行时或编译期确定。
constexpr 变量/函数:C++11 引入,强制要求在编译期求值,提供真正的"编译期常量"。
我们重点讨论 const 和 constexpr。
2. const:只读的承诺
const 用于声明一个不可修改的变量。它的核心是承诺"我不会通过这个名字去改变它",但不强制要求在编译期就能确定它的值。
2.1 基本用法
cpp
const int max_value = 100; // 编译期常量(用字面量初始化)
int n = 10;
const int size = n; // 运行时常量:值在运行时才确定,但之后不可改
const char a = 'a';
const double pi = 3.14159;
const char* ptr; // 指向常量字符的指针(所指内容不可改)
char ch = 'A';
char* const ptr2 = &ch; // 常量指针(指针本身不可改)
const char* const ptr3 = "Hello"; // 两者皆不可改
// 或者:
//const char* const ptr3 = &ch; // 如果 ch 是 char,它会隐式转换为 const char*
2.2 const 变量的特性
不可修改性:任何试图修改 const 变量的操作都将导致编译错误。
可能为编译期常量:如果用常量表达式(如字面量)初始化,const 变量可作为编译期常量使用,例如数组大小、模板非类型参数等。但这是有条件的。
内部链接性(默认):在全局或命名空间作用域声明的 const 变量默认具有内部链接(static),即每个翻译单元有独立副本。若希望跨文件共享,需用 extern const 声明。
cpp
// file1.cpp
extern const int global_const = 10; // 定义,显式 extern
// file2.cpp
extern const int global_const; // 声明,可访问同一变量
3. constexpr:真正的编译期常量
constexpr 是 C++11 引入的关键字,它显式要求变量或函数的值在编译期就能计算出来。它比 const 更严格,提供了编译期计算的强大能力。
3.1 constexpr 变量
声明为 constexpr 的变量必须用编译期可确定的常量表达式初始化,且它本身隐含 const。
cpp
constexpr int max = 100; // 编译期常量
constexpr int size = max * 2; // 编译期计算
int n = 10;
// constexpr int s = n; // 错误:n 不是常量表达式
constexpr int s2 = 42; // OK
constexpr double pi = 3.14; // 浮点数也可以
constexpr const char* msg = "Hi"; // OK
onstexpr 变量必定是编译期常量,因此它可以用在任何需要常量表达式的地方,比如数组大小、模板参数、static_assert 等。
cpp
constexpr int length = 5;
int arr[length]; // OK
std::array<int, length> my_arr; // OK
static_assert(length == 5, "!"); // OK
如果报错 不允许使用不完整的类型 "std::array<int, 5Ui64>"
添加 #include
csharp
#include <array>
3.2 constexpr 函数
constexpr 函数是可能在编译期被求值的函数。它并不保证一定在编译期执行------如果调用时传入的参数都是编译期常量,且调用发生在需要常量表达式的地方,编译器就会在编译期计算它;否则,它也可以像普通函数一样在运行期调用。
规则(C++11/14,C++17,C++20 逐步放宽):
函数体只能包含一条 return 语句(C++11),后续标准允许更多语句,如循环、分支,但不可有 goto、try 或静态变量定义(C++20 允许)。
参数和返回值必须是字面类型(标量、引用、聚合体等)。
C++14 起支持大部分控制流。
注意:虽然 square 被声明为 constexpr,且参数 5 是常量,但编译器仍然拒绝将其视为常量表达式。根本原因是:constexpr 函数的完整定义在调用点之前不可见。例如:
cpp
#include <iostream>
#include <cstdlib> // 添加,因为 get_rand 使用了 rand()
int get_rand(); // 可以只声明,定义可以放在后面
int main() {
constexpr int val = square(5); // 现在定义可见,编译通过
int runtime_val = square(get_rand());
std::cout << runtime_val << std::endl;
return 0;
}
int get_rand() {
return rand() % 100;
}
constexpr int square(int x) {
return x * x;
}
运行报错:
cpp
严重性 代码 说明 项目 文件 行 抑制状态 详细信息
错误 C3861 "square": 找不到标识符 StudyCpp C:\Study\StudyCpp\StudyCpp\main.cpp 7
错误 C2131 表达式的计算结果不是常数 StudyCpp C:\Study\StudyCpp\StudyCpp\main.cpp 7
错误 C3861 "square": 找不到标识符 StudyCpp C:Study\StudyCpp\StudyCpp\main.cpp 8
正确操作只需把
cpp
onstexpr int square(int x) {
return x * x;
提前即可。
完整代码:
cpp
#include <iostream>
#include <cstdlib> // 添加,因为 get_rand 使用了 rand()
constexpr int square(int x) {
return x * x;
}
int get_rand(); // 可以只声明,定义可以放在后面
int main() {
constexpr int val = square(5); // 现在定义可见,编译通过
int runtime_val = square(get_rand());
std::cout << runtime_val << std::endl;
return 0;
}
int get_rand() {
return rand() % 100;
}
3.3 constexpr 与编译期计算
利用 constexpr 可以将大量计算移至编译期,减少运行时开销,并生成常量数据表等。
cpp
#include <iostream>
#include <cstdlib>
#include <array>
constexpr int fib(int n) {
return (n <= 1) ? n : fib(n - 1) + fib(n - 2);
}
int main() {
constexpr int fib10 = fib(10); // 编译期计算 55
std::array<int, fib(10)> data; // 可使用,模板参数,编译期求值
std::cout << data[0] << std::endl;
return 0;
}
3.4 C++17:constexpr 与 Lambda
C++17 允许将 Lambda 声明为 constexpr(或隐式满足条件时自动成为 constexpr)。
直接用C++20.
cpp
#include <iostream>
#include <array> // 如果仅用于演示,可删除
int main() {
auto lambda = [](int x) { return x * 2; }; // C++17 自动推断为 constexpr
static_assert(lambda(3) == 6, "assertion failed"); // 必须提供第二个参数(C++17 单参数也可用)
std::cout << lambda(3) << std::endl;
return 0;
}
3.5 C++17:if constexpr
if constexpr 是一种编译期条件分支,用于模板或泛型代码中,根据模板参数在编译期选择代码路径。不成立的分支完全不会被实例化,从而避免了无效代码。
cpp
#include <iostream>
#include <type_traits> // 必须包含!
template<typename T>
auto get_value(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // T 为指针时解引用
}
else {
return t; // 非指针直接返回值
}
}
int main() {
int a = 42;
int* p = &a;
std::cout << get_value(a) << std::endl; // 输出 42
std::cout << get_value(p) << std::endl; // 输出 42
return 0;
}
4. const 与 constexpr 核心对比
|
| 特性 | const | constexpr |
|---|---|---|
| 主要语义 | 只读(不可修改) | 编译期求值 |
| 初始化 | 可在运行期初始化 | 必须用常量表达式初始化 |
| 是否隐含常量性 | 是(不能修改) | 是(隐含 const,且编译期确定) |
| 作为编译期常量 | 有条件(如果初始化器是常量表达式) | 无条件保证 |
| 用于数组大小、模板参数 | 仅当初始化器为常量表达式时 | 总是可以 |
| 修饰函数 | 成员函数(承诺不修改对象) | 普通函数/成员函数(可能编译期执行) |
| 内存分配 | 常规变量,通常有存储 | 可无运行时存储,完全内联 |
| 实际选择指南: |
如果仅需承诺"此值不会变",且值可能在运行时才能确定,用 const。
如果需要确保值在编译期就确定,用于模板、数组大小、性能关键处,用 constexpr。
设计接口时,对不可修改的引用/指针参数使用 const;对希望提供编译期计算的工具函数使用 constexpr。
5. 进阶话题简述
5.1 consteval (C++20)
consteval 强制函数必须在编译期执行,即创建"立即函数"。不同于 constexpr 可退化为运行期函数,consteval 函数若不能在编译期求值则会报错。
cpp
#include <iostream>
#include <cstdlib> // 用于 rand()
#include <ctime> // 用于 time()
constexpr int twice(int x) {
return x + x;
}
int get_runtime() {
return rand() % 100; // 返回一个运行时值
}
int main() {
constexpr int a = twice(5); // 编译期求值,OK
int r = get_runtime(); // 运行时值
int b = twice(r); // 运行时调用,OK(constexpr 函数允许运行时)
std::cout << "a = " << a << ", b = " << b << std::endl;
return 0;
}
5.2 constinit (C++20)
constinit 用于保证变量在编译期初始化,但其值仍可在运行时修改(即变量本身非 const)。它主要用于避免静态初始化顺序问题。
cpp
constinit static int global = 42; // 编译期初始化,但 global 可被修改
5.3 常量表达式中的 const 与 constexpr
当 const 变量用字面量或 constexpr 函数初始化时,它本身就是一个常量表达式,几乎等同于 constexpr 变量,只是缺少"强制编译期"的保险。
cpp
#include <iostream>
// 必须定义为 constexpr,才能在编译期求值
constexpr int square(int x) {
return x * x;
}
int main() {
// 这两个初始化器都是常量表达式
const int a = 10; // 字面量,常量表达式
constexpr int b = square(10); // 明确表示编译时计算 b也位const常量
// 可以用作数组大小(需要编译期常量)
int arr[a] = {}; // 大小为 10
int arr2[b] = {}; // 大小为 100
std::cout << "a = " << a << ", b = " << b << std::endl;
std::cout << "Array sizes: " << sizeof(arr) / sizeof(arr[0])
<< " and " << sizeof(arr2) / sizeof(arr2[0]) << std::endl;
return 0;
}
总结
常量是不可更改的值。
const 是"只读"的契约,值不一定编译期可知。
constexpr 是"编译期常量"的强制要求,是现代 C++ 编译期计算的基石。
善用 constexpr 可以将计算提前到编译期,提升性能与安全性;善用 const 可以明确代码意图,防止意外修改。
掌握 const 和 constexpr 的区别与联系,是深入 C++ 核心、写出高效安全代码的关键一步。