const 与 constexpr
定义与作用
C++ 中的 const 和 constexpr 是两个不同层次的不变性机制:
| 关键字 | 含义 | 引入版本 |
|---|---|---|
const | 运行期只读——变量初始化后不可修改 | C++98 |
constexpr | 编译期常量——值在编译时确定 | C++11 |
const 回答"能不能改"的问题;constexpr 回答"什么时候求值"的问题。一个 const 变量可能在运行期初始化,而 constexpr 变量必须在编译期就能求值。
核心原理
const 的多重角色
顶层 const 与底层 const
const int ci = 10; // 顶层 const:ci 本身不可变
const int* pi = &ci; // 底层 const:pi 可改,但所指对象不可改
int* const cp = &x; // 顶层 const:cp 不可改,所指对象可改
const int* const ccp = &ci; // 顶层 + 底层
这个区分在 auto 推导时至关重要——auto 会丢弃顶层 const 但保留底层 const。
constexpr 的本质
C++11 中 constexpr 函数限制严格——函数体只能包含一条 return 语句。C++14 大幅放宽,允许局部变量、循环和条件分支。
完整示例
示例一:const 正确性——学生成绩管理
场景说明:设计一个 GradeBook 类,通过 const 成员函数保证查询操作不修改对象状态,演示 const 正确性的好处。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
class GradeBook {
public:
void add_student(const std::string& name, double score) {
names_.push_back(name);
scores_.push_back(score);
}
// const 成员函数:承诺不修改对象状态
double average() const {
if (scores_.empty()) return 0.0;
double total = std::accumulate(scores_.begin(), scores_.end(), 0.0);
return total / scores_.size();
}
double highest() const {
if (scores_.empty()) return 0.0;
return *std::max_element(scores_.begin(), scores_.end());
}
size_t student_count() const {
return names_.size();
}
// 返回 const 引用:防止外部意外修改
const std::vector<std::string>& names() const { return names_; }
const std::vector<double>& scores() const { return scores_; }
private:
std::vector<std::string> names_;
std::vector<double> scores_;
mutable size_t average_query_count_{0}; // 即使在 const 函数中也可修改
};
// 接收 const 引用:承诺不修改传入对象
void print_report(const GradeBook& book) {
std::cout << "===== 成绩报告 =====" << std::endl;
std::cout << "学生人数: " << book.student_count() << std::endl;
std::cout << "平均分: " << book.average() << std::endl;
std::cout << "最高分: " << book.highest() << std::endl;
const auto& names = book.names();
const auto& scores = book.scores();
for (size_t i = 0; i < names.size(); ++i) {
std::cout << " " << names[i] << ": " << scores[i] << std::endl;
}
}
int main() {
GradeBook book;
book.add_student("张三", 88.5);
book.add_student("李四", 92.0);
book.add_student("王五", 76.5);
book.add_student("赵六", 85.0);
print_report(book);
// 验证 const 安全性:
// book.names().push_back("攻击者"); // 编译错误:返回的是 const&
// print_report 中 book.average() 合法,因为 average() 是 const
return 0;
}
预期输出:
===== 成绩报告 =====
学生人数: 4
平均分: 85.5
最高分: 92
张三: 88.5
李四: 92
王五: 76.5
赵六: 85
逐段分析:
average()等查询函数标记为const,编译器会在编译期验证它们确实不修改成员print_report参数是const GradeBook&,内部只能调用 const 成员函数——这是编译器强制保证的- 返回
const std::vector<T>&而非值拷贝:零拷贝开销 + 防外部修改 mutable允许在 const 函数中修改特定成员(如缓存、计数),用于逻辑 const 而非物理 const
示例二:constexpr 编译期计算
场景说明:使用 constexpr 在编译期计算阶乘和斐波那契数列,用于数组大小声明和模板参数。
#include <iostream>
#include <array>
// C++11 写法:单一 return 语句
constexpr int factorial_11(int n) {
return n <= 1 ? 1 : n * factorial_11(n - 1);
}
// C++14 写法:允许多条语句(编译器支持时可用)
constexpr int factorial_14(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
// 编译期计算斐波那契
constexpr int fibonacci(int n) {
return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
// 编译期常量用于数组大小
constexpr size_t size = factorial_11(5); // 120
std::array<int, size> arr{}; // 编译期确定大小
std::cout << "数组大小: " << arr.size() << std::endl;
// 编译期常量用于模板参数
std::array<int, factorial_11(4)> fib_arr{}; // 24
// 运行期也可调用 constexpr 函数
int n;
std::cout << "输入 n: ";
std::cin >> n;
std::cout << n << "! = " << factorial_14(n)
<< " (运行期计算)" << std::endl;
// const vs constexpr
const int rt_const = n * 2; // 运行期只读
constexpr int ct_const = factorial_11(6); // 编译期常量 = 720
// constexpr 的威力:保证编译期可求值
// constexpr int bad = n * 2; // 编译错误:n 不是编译期常量
std::cout << "6! = " << ct_const << " (编译期常量)" << std::endl;
std::cout << "fibonacci(10) = " << fibonacci(10)
<< " (可在编译期求值)" << std::endl;
return 0;
}
编译命令:
g++ -std=c++14 constexpr_demo.cpp -o constexpr_demo
预期交互:
数组大小: 120
输入 n: 5
5! = 120 (运行期计算)
6! = 720 (编译期常量)
fibonacci(10) = 55 (可在编译期求值)
逐段分析:
factorial_11符合 C++11 约束(单一 return),但风格较晦涩factorial_14写法直观,但需 C++14 支持constexpr size_t size = factorial_11(5)在编译期求值,结果是真正的编译期常量,可用于std::array的模板参数const int rt_const = n * 2只是运行期只读,不能用于模板参数constexpr函数的双重特性:参数是编译期常量时编译期求值,参数是运行期变量时运行期求值
示例三:const 指针的"左右"辨析
#include <iostream>
int main() {
int x = 10, y = 20;
// 从右向左读
const int* p1 = &x; // p1 是 → 指针 → 指向 → int → const
// 指向的 int 不可修改
int const* p2 = &x; // 与 p1 完全相同(const 在 * 前即可)
int* const p3 = &x; // p3 是 → const → 指针 → 指向 → int
// 指针本身不可修改
const int* const p4 = &x; // 指针和所指对象都不可修改
// *p1 = 30; // 错误:p1 指向 const int
p1 = &y; // 正确:p1 本身不是 const
*p3 = 30; // 正确:p3 指向的 int 可修改
// p3 = &y; // 错误:p3 本身是 const
std::cout << "x=" << x << ", y=" << y << std::endl;
return 0;
}
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
混淆 const int* 和 int* const | 编译错误或语义错误 | 从右向左读:const 在 * 前修饰所指,在 * 后修饰指针 |
const 成员函数中修改成员 | 编译错误 | 使用 mutable 或重新设计接口 |
| 返回非 const 引用暴露内部状态 | 外部可绕过封装修改 | 返回 const T& |
constexpr 函数内写 C++11 不允许的语句 | 编译错误 | 使用递归或升级到 C++14 |
把 const 变量当编译期常量 | const int 不能用于模板参数 | 用 constexpr 保证编译期求值 |
常见面试问题
const和constexpr的区别?——const只保证运行期不可修改,值可能在运行期才确定;constexpr强制编译期求值,是真正的编译期常量。const int*、int const*、int* const、const int* const分别是什么意思?——前两者等价(指向 const int 的指针);第三个是 const 指针(指向 int);第四个是 const 指针指向 const int。const成员函数的作用?什么情况下需要两个版本(const 和非 const)?——承诺不修改成员变量,使函数可在 const 对象上调用。重载 const 和非 const 版本可提供读写和只读两种接口(如operator[])。mutable的作用?——允许在 const 成员函数中修改标记为 mutable 的成员变量,常用于缓存、互斥锁、访问计数等"逻辑不变但物理可变"的场景。C++11 中
constexpr函数的限制是什么?为什么?——函数体只能包含一条 return 语句。这是为了简化编译器的实现复杂度。C++14 放宽了这一限制。constexpr、宏常量#define、const如何选择?——优先constexpr(类型安全、作用域控制),其次const(运行期只读),避免用宏(无类型、无作用域)。
小结
const= 运行期只读,"我不会改";constexpr= 编译期常量,"我早就知道"- 从右向左读指针声明,区分顶层 const 和底层 const
const成员函数是接口契约,配合const&传参形成 const 正确性体系constexpr让大量计算从运行期移到编译期,提升性能同时保证类型安全- 避免使用宏常量,始终优先
constexpr