C++ 中的 const 和 constexpr: 深入对比与最佳实践
C++ 是一门强调性能和灵活性的编程语言, const
和 constexpr
是其中两个非常重要的修饰符, 用于处理常量. 然而, 许多开发者在使用时容易混淆它们的用法和适用场景. 本文将深入对比这两个关键字, 提供全面的指导, 帮助您更好地理解和使用它们.
一. 使用场景对比
我们将通过不同的使用场景对 const
和 constexpr
的表现进行对比:
1. 存储空间
const
:const
变量可能存储在常量数据区, 也可能存储在栈上, 这取决于变量是否有链接性以及编译器优化.const int runtime_const = 42; // 在某些情况下可能存储在 .rodata 区域 int main() { const int local_const = 10; // 编译器可能将其存储在栈上 return 0; }
- 对于大多数场景, 运行期的
const
变量不会直接节省存储空间.
constexpr
:constexpr
变量在编译期求值, 其结果直接嵌入到代码中, 可以显著减少运行时的内存占用.- 这种优化能避免运行时的额外存储开销, 尤其在嵌入式系统中具有巨大优势.
汇编代码constexpr int compile_time_const = 42; int main() { int result = compile_time_const + 10; // 会直接将计算结果嵌入到指令中,无需分配存储空间 return result; }
main: push rbp mov rbp, rsp mov DWORD PTR [rbp-4], 52 mov eax, DWORD PTR [rbp-4] pop rbp ret
2. 修饰普通变量
const
:- 运行期或编译期均可定义常量.
const int a = 42; // 可以绑定常量 void fun(std::string& s) { // 可以绑定动态值, 只表明size为只读属性 const auto size = s.size(); for (int i = 0; i < size; i++) { // ... } }
- 运行期或编译期均可定义常量.
constexpr
:- 必须在编译期确定值.
constexpr int square(int x) { return x * x; } constexpr int b = 42; // OK, 常量 constexpr int c = square(b); // OK, 常量函数返回值
- 一个自定义的类需要有
constexpr
构造函数,才可以定义constexpr
实例.
- 必须在编译期确定值.
3. 修饰指针
-
const
:-
可以修饰指针或指针所指向的值.
const int* ptr = nullptr; // 指针所指的值是常量 int* const cptr = nullptr; // 指针本身是常量 int a = 0; const int c = 1; ptr = &a; // OK ptr = &c; // OK // ptr = 3; // Error, 不能通过常量指针修改值 // cptr = &a; // Error, 不能改变指针指向 // cptr = &c; // Error, 不能改变指针指向
-
-
constexpr
:-
修饰的指针必须在编译期求值.
constexpr int* np = nullptr; // OK static int sa = 0; // 静态变量 constexpr int* psa = &sa; // OK static const int ca = 0; // 静态常量 constexpr const int* pca = &ca; // OK static constexpr int sb = 0; // 静态constexpr constexpr const int* psb = &sb; // OK constexpr int x = 42; constexpr const int* p2 = &x; // Error const int cy = 1; constexpr const int* p3 = &cy; // Error
-
4. 修饰引用
-
const
:-
可以修饰引用, 使其引用的值不可更改.
int x = 0; int& ref = x; ref = 1; // OK const int& cref = x; cref = 1; // Error, 不可以通过常量引用改变绑定变量值
-
-
constexpr
:-
只能绑定到静态变量.
static int sa = 1; constexpr const int& rsa = sa; // rsa 是绑定到 sa 的引用 sa = 1; // OK rsa = 1; // Error static constexpr int sca = 42; // 静态constexpr constexpr const int& ref = sca; // ref 是绑定到 sca 的引用 sca = 1; // Error ref = 1; // Error
-
5. 修饰函数
const
:- 不能修饰函数本身.
- 可用于修饰类成员函数, 表示不会修改类的成员变量, 后面会讲到.
constexpr
:- 支持修饰函数.
constexpr
函数在编译期求值, 但需要满足以下限制:- 返回值和参数必须是常量表达式.
- 函数体中仅允许使用常量表达式.
constexpr int square(int x) { return x * x; }
6. 修饰类成员函数
-
const
:-
表示成员函数不会修改类的成员变量.
class MyClass { public: // 表示该函数不修改成员变量 int size() const { return size_; } private: int size_ = 0; };
-
-
constexpr
:-
表示成员函数可以在编译期求值.
class MyClass { public: // 表示该函数不修改成员变量, 且可以在编译时求值 constexpr int size() const { return size_; } private: int size_ = 0; };
-
二. const
和 constexpr
的历史演进
随着 C++ 标准的迭代, constexpr
的功能得到了极大增强.
C++11
- 引入
constexpr
, 允许声明编译期常量和常量表达式函数. - 受限于当时的标准,
constexpr
函数:- 返回值必须是常量表达式.
- 函数体中只能包含单一
return
语句. - 不支持
if
,for
等复杂语句.
C++14
- 解除了一些限制:
- 函数体中允许包含局部变量.
- 支持更复杂的控制流, 如
if-else
和for
.
C++17
- 引入
inline
constexpr
, 支持更灵活的代码组织. - 扩展了
constexpr
对常量表达式构造的支持.
C++20
- 几乎解除所有限制:
- 允许动态分配内存(通过
new
). - 可以捕获异常.
- 允许动态分配内存(通过
五. 常见问题解答(FAQ)
Q: const
和 constexpr
可以互相替代吗?
A: 不可以. constexpr
适用于编译期计算, 而 const
主要限制值的修改.
Q: 什么时候优先使用 constexpr
?
A: 当值必须在编译期确定时, 优先使用 constexpr
.
Q: constexpr
的性能优势体现在什么地方?
A: 通过将运行时计算迁移到编译期, 可以减少运行时开销.
六. 总结回顾
通过本文, 我们详细比较了 const
和 constexpr
在存储空间, 变量修饰, 函数修饰, 指针与引用修饰, 以及类成员函数中的表现. 同时, 我们探讨了它们的历史演进和模板应用, 并列举了常量表达式的具体应用场景.
掌握这两个关键字的区别与用法, 不仅可以写出更加高效的代码, 还能避免很多潜在的错误. 希望本文对您有所帮助!