常量 C++ 定义了一套完整的只读量定义方法,被 const
修饰的变量都是只读量,编译器会在编译期进行冲突检查,避免对只读量的修改,同时可能会执行一些优化。
在通常情况下,应该尽可能使用 const
修饰变量、参数,提高代码健壮性。
const
类型限定符常量 const 修饰的变量在初始化后不可改变值
const int a = 0 ; // a 的类型为 const int
// a = 1; // 不能修改常量
常量引用、常量指针 常量引用和常量指针均限制了对指向的值的修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 int a = 0 ;
const int b = 0 ;
int * p1 = & a ;
* p1 = 1 ;
const int * p2 = & a ;
// *p2 = 2; // 不能通过常量指针修改变量
// int *p3 = &b; // 不能用 int* 指向 const int 变量
const int * p4 = & b ;
int & r1 = a ;
r1 = 1 ;
const int & r2 = a ;
// r2 = 2; // 不能通过常量引用修改变量
// int &p3 = b; // 不能用 int& 引用 const int变量
const int & r4 = b ;
另外需要区分开的是常量指针(const t*
)和指针常量(t* const
),例如下列声明
int * const p1 ; // 指针常量,初始化后指向地址不可改,可更改指向的值
const int * p2 ; // 常量指针,解引用的值不可改,可指向其他 int 变量
const int * const p3 ; // 常量指针常量,值不可改,指向地址不可改
// 使用别名能更好提高可读性
using const_int = const int ;
using ptr_to_const_int = const_int * ;
using const_ptr_to_const_int = const ptr_to_const_int ;
在函数参数里使用 const
限定参数类型,可以避免变量被错误地修改,同时增加代码可读性
void sum ( const std :: vector < int > & data , int & total ) {
for ( auto iter = data . begin (); iter != data . end (); ++ iter )
total += * iter ; // iter 是迭代器,解引用后的类型是 const int
}
const
成员函数类型中 const
限定的成员函数,可以用来限制对成员的修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 #include <iostream>
struct ConstMember {
int s = 0 ;
void func () { std :: cout << "General Function" << std :: endl ; }
void constFunc1 () const { std :: cout << "Const Function 1" << std :: endl ; }
void constFunc2 ( int ss ) const {
// func(); // const 成员函数不能调用非 const 成员函数
constFunc1 ();
// s = ss; // const 成员函数不能修改成员变量
}
};
int main () {
int b = 1 ;
ConstMember c {};
const ConstMember d = c ;
// d.func(); // 常量不能调用非 const 成员函数
d . constFunc2 ( b );
return 0 ;
}
常量表达式 constexpr(C++11) 常量表达式是指编译时能计算出结果的表达式,constexpr
则要求编译器能在编译时求得函数或变量的值。
编译时计算能允许更好的优化,比如将结果硬编码到汇编中,消除运行时计算开销。与 const
的带来的优化不同,当 constexpr
修饰的变量满足常量表达式的条件,就强制要求编译器在编译时计算出结果而非运行时。
实际上把 const
理解成 "readonly" ,constexpr
理解成 "const" ,这样更加直观 constexpr int a = 10 ; // 直接定义常量
constexpr int FivePlus ( int x ) { return 5 + x ; }
void test ( const int x ) {
std :: array < x > c1 ; // 错误,x在编译时不可知
std :: array < FivePlus ( 6 ) > c2 ; // 可行,FivePlus编译时可知
}
以下例子很好说明了 const
和 constexpr
的区别,代码使用递归实现计算斐波那契数列,并用控制流输出。
实现 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 #include <iostream>
using namespace std ;
constexpr unsigned fib0 ( unsigned n ) {
return n <= 1 ? 1 : ( fib0 ( n - 1 ) + fib0 ( n - 2 ));
}
unsigned fib1 ( unsigned n ) { return n <= 1 ? 1 : ( fib1 ( n - 1 ) + fib1 ( n - 2 )); }
int main () {
constexpr auto v0 = fib0 ( 9 );
const auto v1 = fib1 ( 9 );
cout << v0 ;
cout << ' ' ;
cout << v1 ;
}
编译后的可能的汇编代码(使用 Compiler Explorer,Clang 19) 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 fib1(unsigned int):
push r14
push rbx
push rax
mov ebx, 1
cmp edi, 2
jb .LBB0_4
mov r14d, edi
xor ebx, ebx
.LBB0_2:
lea edi, [r14 - 1]
call fib1(unsigned int)
add r14d, -2
add ebx, eax
cmp r14d, 1
ja .LBB0_2
inc ebx
.LBB0_4:
mov eax, ebx
add rsp, 8
pop rbx
pop r14
ret
main:
push r14
push rbx
push rax
mov edi, 9
call fib1(unsigned int) # `v1` 的初始化进行了函数调用
mov ebx, eax
mov r14, qword ptr [rip + std::__1::cout@GOTPCREL]
mov rdi, r14
mov esi, 55 # `v0` 被最终计算结果替代
call std::__1::basic_ostream<char, std::__1::char_traits<char>>::operator<<(unsigned int)@PLT
mov byte ptr [rsp + 7], 32
lea rsi, [rsp + 7]
mov edx, 1
mov rdi, r14
call std::__1::basic_ostream<char, std::__1::char_traits<char>>& std::__1::__put_character_sequence[abi:ne200000]<char, std::__1::char_traits<char>>(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, char const*, unsigned long)
mov rdi, r14
mov esi, ebx # 读取了变量值
call std::__1::basic_ostream<char, std::__1::char_traits<char>>::operator<<(unsigned int)@PLT
xor eax, eax
add rsp, 8
pop rbx
pop r14
ret
constexpr
修饰的 fib0
函数在唯一的调用处用了常量参数,使得整个函数仅在编译期运行。由于函数没有运行时执行,编译器也就判断不需要生成汇编代码。
在同时注意到汇编中,v0
没有初始化代码,在调用 cout
输出 v0
的代码中,v0
已被最终结算结果替代,说明变量值已在编译时求出,优化掉了运行时运算。 而 v1
的初始化还是普通的 fib1
递归调用。
所以 constexpr
可以用来替换宏定义的常量,规避 宏定义的风险 。
算法题中可以使用 constexpr
存储数据规模较小的变量,以消除对应的运行时计算开销。尤为常见在「打表 」技巧中,使用 constexpr
修饰的数组等容器存储答案。
编译时计算量过大会导致编译错误 编译器会限制编译时计算的开销,如果计算量过大会导致无法通过编译,应该考虑使用 const
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 #include <iostream>
using namespace std ;
constexpr unsigned long long fib ( unsigned long long i ) {
return i <= 2 ? i : fib ( i - 2 ) + fib ( i - 1 );
}
int main () {
// constexpr auto v = fib(32); evaluation exceeded maximum depth
const auto v = fib ( 32 );
cout << v ;
return 0 ;
}
使用 constexpr 时 Clang 给出的编译错误 <source>:10:20: error: constexpr variable 'v' must be initialized by a constant expression
10 | constexpr auto v = fib(32);
| ^ ~~~~~~~~~~~~
<source>:6:25: note: constexpr evaluation exceeded maximum depth of 512 calls
6 | return i <= 2 ? i : fib(i - 2) + fib(i - 1);
| ^
<source>:6:25: note: in call to 'fib(32)'
6 | return i <= 2 ? i : fib(i - 2) + fib(i - 1);
| ^~~~~~~~~~
<source>:6:25: note: in call to ...
参考资料 本页面最近更新:2024/10/28 13:31:12 ,更新历史 发现错误?想一起完善? 在 GitHub 上编辑此页! 本页面贡献者:cmpute , ouuan , billchenchina , c0nstexpr , CCXXXI , CoelacanthusHex , Enter-tainer , gi-b716 , mgt , SakamotoKurome , Tiphereth-A , Xeonacid 本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用