目录
一、const关键字/限定符
1.1 基本的const变量【const修饰变量(包括函数参数、函数返回值)】
1.2 const指针【分为指针常量(指向常量的指针)和常量指针】
1.2.1 指向常量的指针【const int*p;】
1.2.2 常量指针【int* const p;】
1.2.3 指向常量的常量指针【const int* const p;】
1.3 修饰引用
1.4 修饰类成员函数
1.5 总结
二、顶层const
三、constexpr关键字和常量表达式
3.1 常量表达式
3.2 constexpr变量
一、const关键字/限定符
const是一种类型修饰符,用于说明永不改变的对象【用于声明常量或不可修改的变量】。const对象一旦定义就无法再赋新值,所以必须初始化。作用:主要用于保证变量的只读性,提高代码的安全性和可读性。
const关键字可以用于修饰变量、指针、引用、成员函数。
1.1 基本的const变量【const修饰变量(包括函数参数、函数返回值)】
1.const修饰变量,变量的值不能再次修改。
#include <iostream>int main() {const int x = 10; // 定义常量 x,并初始化为 10// x = 20; // ❌ 错误!x 不能被修改std::cout << "x = " << x << std::endl;return 0;
}
✅ 特点:
x
在定义时赋值,之后不能修改。- 如果尝试
x = 20;
,编译器会报错。
2.const修饰函数参数
void func(const int x) {// x = 20; // ❌ 错误!x 不能被修改std::cout << "x = " << x << std::endl;
}
✅ 作用: 确保参数在函数内部不会被修改。
3.const修饰函数返回值
在 C++ 中,
const
可以修饰函数的返回值,以控制返回值是否可以被修改。根据const
作用的位置不同,可以分为以下几种情况:
1.
const
修饰返回的基本数据类型(无意义)const int func() {return 10; }
🔹 这里
const
修饰的是返回值本身,但没有实际作用。
🔹 因为返回的是 值,本来就是拷贝出去的,函数调用后不会影响原来的数据:int a = func(); // ✅ a = 10 a = 20; // ✅ 仍然可以修改 a,const 失效
❌ 误区:
const
修饰基本数据类型的返回值没有意义,因为返回的是拷贝,const
不影响拷贝值的修改。
2.
const
修饰指针返回值const int* getPointer() {static int value = 10;return &value; }
🔹
const int*
表示 返回的是指向const int
的指针,不能修改指针指向的值:const int* p = getPointer(); *p = 20; // ❌ 错误:不能修改 *p p = nullptr; // ✅ 允许修改 p
✔ 适用于:返回一个不希望被修改的指针(如字符串、配置数据等)。
3.
int* const
(不常见)int* const getPointer() {static int value = 10;return &value; }
🔹
int* const
返回一个常量指针,即指针本身不可修改,但指向的值可修改:int* const p = getPointer(); *p = 20; // ✅ 允许修改 *p p = nullptr; // ❌ 错误:p 是 const 不能修改
✔ 不常见,因为返回值是右值,无法绑定到
int* const
类型的变量上。
4.
const
修饰返回的引用const int& getRef() {static int value = 10;return value; }
🔹
const int&
返回一个const
引用,即不能修改返回值:const int& ref = getRef(); ref = 20; // ❌ 错误:ref 不能修改
✔ 适用于:
- 避免拷贝,提高效率
- 不希望调用者修改返回值
- 返回类成员变量时,防止修改对象内部数据
5.
const
修饰类成员函数的返回值(1)
const
修饰返回的类对象class Test { public:const Test getObject() {return *this;} };
🔹
const Test
返回的对象本身是const
,所以调用者不能修改它:Test obj; const Test newObj = obj.getObject();
✔ 意义不大,因为返回的是拷贝,
const
仅影响拷贝的newObj
,但不影响obj
本身。
(2)返回
const
引用class Test { private:int value; public:Test(int v) : value(v) {}const int& getValue() const { // 返回 const 引用return value;} };
🔹
const int&
返回类成员变量的引用,并且不能修改:Test obj(10); const int& ref = obj.getValue(); ref = 20; // ❌ 错误:不能修改 ref
✔ 适用于:
- 防止调用者修改类的内部数据
- 避免拷贝,提高效率
6.
const
修饰类的成员函数class Test { public:int value;int getValue() const { // const 成员函数return value;} };
🔹
const
放在成员函数后面,表示该函数不能修改类的成员变量:Test obj; obj.getValue(); // ✅ 允许调用
✔ 适用于:
- 保证成员函数是只读的
- 防止修改类的状态
总结
写法 作用 适用场景 const int func();
❌ 无意义,返回值拷贝不会受 const
影响不建议使用 const int* func();
返回 const
指针,指向的值不可修改适用于保护数据 int* const func();
返回 const
指针,指针本身不可修改不常见 const int& func();
返回 const
引用,不能修改避免拷贝,提高效率 const Test func();
返回 const
对象,拷贝的const
限制意义不大 const Test& func();
返回 const
对象的引用,不能修改提高效率,防止修改 int getValue() const;
const
成员函数,不能修改成员变量保证函数只读 你现在清楚了吗?😃
1.2 const指针【分为指针常量(指向常量的指针)和常量指针】
const
可以修饰指针,使指针本身或者指针指向的内容不可变:
const int a = 10;
const int* p1 = &a; // 指向常量的指针(指向的值不可修改)
int* const p2 = &a; // 常量指针(指针本身不可修改)
声明方式 | 解释 |
---|---|
const int* p | 指向常量的指针(指针指向的值不可变,指针本身可变) |
int* const p | 常量指针(指针本身不可变,但指向的值可以变) |
const int* const p | 指向常量的常量指针(指针本身和指向的值都不可变) |
技巧:根据口诀"左定值,右定向"可以来判定到底是指向常量的指针还是常量指针。左定值说的是const在*的左边,例如:const int* p,那么这就是指向常量的指针,指针指向的值不可变,指针本身可变;右定向说的是const在*的右边,例如:int* const p,这就是常量指针,指针本身不可变,但指向的值可以变。
1.2.1 指向常量的指针【const int*p;】
用法示例:
const int* p; //示例
指向常量的指针(pointer to const)不能用于改变其所指对象的值。但指针本身的值(存放指针指向对象的地址)是可以修改的。
1.要想存放常量对象的地址,只能使用指向常量的指针,使用普通的指针是不行的。
#include<iostream>int main(){const double pi=3.14; // pi是一个常量,它的值不能够改变double *ptr = π //错误:ptr是一个普通指针const double* ptr = π //正确:ptr是一个指向常量的指针return 0;
}
第二行代码报错:a value of type "const double *" cannot be used to initialize an entity of type "double *"C/C++(144)
译文:“const double*”类型的值不能用于初始化“double*”C/C++类型的实体(144)
2. 对于指向常量的指针,可以修改指针的值,不能修改指针指向的内容。
修改指针的值:
#include<iostream>int main(){int a=1;int b=2;std::cout<<"a的地址:"<<&(a)<<std::endl;std::cout<<"b的地址:"<<&(b)<<std::endl;const int* ptr=&a; // ptr是一个指向常量的指针,不能修改指针指向的内容,可以修改指针本身的指向std::cout<<"ptr指针修改前的值为:"<<ptr<<std::endl;ptr=&b; // ptr指向b,ptr存放的是b对象的地址std::cout<<"ptr指针修改后的值为:"<<ptr<<std::endl;return 0;
}// 输出结果:
// a的地址:0x61fe44
// b的地址:0x61fe40
// ptr指针修改前的值为:0x61fe44
// ptr指针修改后的值为:0x61fe40
不能修改指向的内容:
#include<iostream>int main(){int a=1;const int* ptr=&a; // ptr是一个指向常量的指针,不能修改指针指向的内容,可以修改指针本身的指向*ptr=5; // 错误:不能通过指向常量的指针修改该指针指向的内容return 0;
}
3. 指向常量的指针可以指向非常量对象。指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象。注意和第一点的区别,第一点说的是普通指针不能够指向常量对象,而这一点说的是指向常量的指针能够指向非常量对象。也就是说,指向常量的指针既可以指向常量对象(用const修饰过的变量),也可以指向非常量对象。
#include<iostream>int main(){double pi=3.14; // pi是一个非常量const double* ptr = π //正确:ptr是一个指向常量的指针,指向常量的指针可以指向非常量对象return 0;
}
指向常量的指针没有规定其所指的对象必须是一个常量。 所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
1.2.2 常量指针【int* const p;】
用法示例:
int* const p;//示例
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的内容。
#include<iostream>int main(){int a = 0;int* const ptr = &a; // ptr将一直指向a(ptr是一个常量指针,不可以修改指针的指向,但可以修改指针指向的内容)std::cout<<"ptr指针的值为:"<<ptr<<std::endl;std::cout<<"ptr指向的内容为:"<<a<<std::endl;
}
// 程序输出:
// ptr指针的值为:0x61fe44
// ptr指向的内容为:0
1.对于常量指针,不能修改指针的值,能够修改指针指向的内容。
不能修改指针的值:
#include<iostream>int main(){int a=1;int b=3;int* const ptr=&a; // ptr是一个常量指针ptr=&b; // 报错,ptr是常量指针,该指针的值无法修改return 0;
}
能够修改指针指向的内容:
#include<iostream>int main(){int a=1;int b=3;// std::cout<<"a的地址为:"<<&(a)<<std::endl;// std::cout<<"b的地址为:"<<&(b)<<std::endl;int* const ptr=&a; // ptr是一个常量指针*ptr=5; //能够通过常量指针修改指向的内容,a将被修改为5std::cout<<"a的值为:"<<a<<std::endl; // 输出:a的值为:5return 0;
}
2.不能用常量指针修改用const修饰的变量。
#include<iostream>int main(){const int a=1;int* const ptr=&a; // ptr是一个常量指针*ptr=5; //能够通过常量指针修改指向的内容,a将被修改为5std::cout<<"a的值为:"<<a<<std::endl; // 输出:a的值为:5return 0;
}
错误点如下图:
报错原因:a value of type "const int *" cannot be used to initialize an entity of type "int *const"C/C++(144)
分析:如果使用const修饰变量,那么这个变量的地址类型为指向常量的指针,此时该地址是无法赋值给常量指针的。
类中的this指针就属于常量指针,this指针的形式为Type*const this。对象地址作为实参传递给this形参,对象不存储this指针,通过形参传递,由寄存器来传递。
逐句分析并补充说明:
1. "this 指针就属于常量指针,this 指针的形式为
Type* const this
。"✅ 正确,但需要明确 "常量指针" 的含义
this
指针在 C++ 类的成员函数中表示当前对象的地址。this
指针的类型是Type* const this
,意思是:
this
是一个 常量指针(const
限制了指针本身不能修改)。- 但
this
指向的对象可以修改(前提是成员函数本身没有const
修饰)。- 错误示范(试图修改
this
,会报错):class A { public:void func() {this = nullptr; // ❌ 错误,this 是常量指针,不能修改} };
2. "对象地址作为实参传递给 this 形参。"
✅ 正确,但可以更精准
- 在成员函数调用时,编译器会 隐式地 将对象的地址作为参数传递给成员函数。
- 这个过程是在编译时由编译器自动处理的,开发者无须显式传递。
示例(相当于
this
被传递):class A { public:void func() {cout << "this 指向的地址: " << this << endl;} };int main() {A obj;obj.func(); // 实际上相当于 func(&obj); }
3. "对象不存储 this 指针。"
✅ 正确
this
不是 类对象的成员变量,不会占用对象的存储空间。this
是在函数调用时传递的,不存储在对象中。- 类对象的大小只由其成员变量决定,不会因为有
this
而变大。示例(验证对象大小):
#include <iostream> using namespace std;class A {int x; // 只有一个成员变量 public:void func() {} // 有成员函数,但不会影响对象大小 };int main() {cout << "sizeof(A) = " << sizeof(A) << endl; // 通常是 4(取决于 int 大小) }
即使
A
有成员函数func()
,对象的大小仍然只与成员变量x
相关,而不会因为this
变大。
4. "通过形参传递,由寄存器来传递。"
✅ 基本正确,但取决于编译器实现
- 现代 C++ 编译器(如 GCC、Clang、MSVC)通常会 通过寄存器 传递
this
指针,以提高效率。- 具体用哪个寄存器 取决于 平台和 ABI:
- 在 x86(32位) 上,
this
通常通过 ECX 寄存器传递。- 在 x86_64(64位) 上,
this
通常通过 RDI 或 RSI 传递。- 在某些情况下(如参数较多时),
this
可能会存入栈中传递。示例:查看汇编代码(x86_64)
class A { public:void func() { cout << "Hello" << endl; } };int main() {A obj;obj.func(); // 实际调用时,this 指针会传入某个寄存器 }
使用
g++ -S -O2 test.cpp
生成汇编代码,你会看到类似:mov rdi, rbx ; 将对象地址(this)存入 rdi 寄存器 call A::func ; 调用成员函数
这表明
this
通过 rdi 寄存器 传递。
总结
句子 评价 解释 "this 指针就属于常量指针,this 指针的形式为 Type* const this
。"✅ 正确 this
是 常量指针,指针地址不能修改,但指向的内容可以改(如果成员函数不是const
)。"对象地址作为实参传递给 this 形参。" ✅ 正确 this
是 隐式参数,成员函数调用时 自动传递。"对象不存储 this 指针。" ✅ 正确 this
不会增加对象大小,只是在调用时传递。"通过形参传递,由寄存器来传递。" ✅ 基本正确 大多数编译器会用 寄存器(如 rdi
) 传递this
,但有例外(如栈传递)。
1.2.3 指向常量的常量指针【const int* const p;】
不能通过指向常量的常量指针修改指针的值以及指针指向的内容。
#include<iostream>int main(){const double pi=3.14;const double* const ptr = π //ptr是一个指向常量对象的常量指针
}
1.3 修饰引用
在 C++ 中,const
可以修饰 引用,影响的是 引用的行为 和 所引用的对象的可变性。主要有两种情况:
1. const
修饰引用的对象(常引用)
语法:
const 类型 &引用名 = 对象;
或者:
类型 const &引用名 = 对象; // const 可以放前面或后面,等价
✅ 作用:
- 不能修改 所引用的对象(只读)。
- 适用于 函数参数 以 防止修改传入的对象,提高代码安全性。
- 常用于引用大对象,避免拷贝,提高效率。
示例:
#include <iostream>
using namespace std;void print(const int &x) { // 不能修改 x// x = 10; ❌ 错误,x 是 const 不能被修改cout << x << endl;
}int main() {int a = 5;const int &ref = a;// ref = 10; ❌ 错误,不能修改 ref 绑定的值a = 10; // ✅ 允许修改 a 本身,ref 也会感知变化cout << ref << endl; // 输出 10
}
上述代码中 const int &ref = a;
这个引用绑定的本质。我们来拆解分析:
1.
const int &ref = a;
发生了什么?
ref
是a
的引用,它并不拥有a
,只是a
的一个别名。const int&
限定了ref
不能修改所引用的值,也就是a
不能通过ref
被修改。- 但
a
本身是可以修改的,所以a = 10;
是合法的。2. 为什么
ref = 10;
会报错?const int &ref = a; ref = 10; // ❌ 错误,无法修改 ref 绑定的值
ref
不是变量本身,它只是a
的一个别名,但由于ref
是const
,它禁止通过ref
修改a
的值。- 编译器会阻止任何通过
ref
修改a
的尝试,因为ref
作为const
绑定的引用,表达了“只能读取a
,不能修改a
”的语义。a
本身并不是const
,所以可以直接修改a
:a = 10; // ✅ 允许修改 a
3. 为什么
cout << ref;
还能输出 10?int a = 5; const int &ref = a; a = 10; cout << ref << endl; // 输出 10
ref
本质上只是a
的别名,并没有拷贝a
,所以a
变成10
,ref
读取a
的值时自然也是10
。
4. 关键点总结
声明 含义 const int &ref = a;
ref
绑定a
,但 不能通过ref
修改a
a = 10;
✅ 直接修改 a
是允许的ref = 10;
❌ 错误, const int&
不能修改所指对象cout << ref;
✅ ref
只是a
的别名,a
变了,ref
也变
5. 进一步思考
如果
a
本身是const
,情况会不一样:const int a = 5; const int &ref = a; a = 10; // ❌ 错误,a 是 const,不能修改 ref = 10; // ❌ 也错误,ref 不能修改 a
- 这里
a
本身 是const
,所以a
根本不能修改,ref
也一样无法修改。所以
const int &ref = a;
的const
只影响ref
不能修改a
,但不影响a
自己被修改。
适用场景:
- 作为 函数参数,防止修改传入的对象:
void print(const string &s) { cout << s; } // 传引用避免拷贝
作为 类成员函数参数,提高安全性:
class A {
public:void func(const A& obj) { /* 不能修改 obj */ }
};
1.4 修饰类成员函数
在类中,被const修饰的成员函数不能修改类对象的成员变量,相当于修饰了成员函数中的隐藏this指针。
在类中,const
成员函数不能修改类的成员变量:
class Test {
public:int data;void show() const { // const 成员函数// data = 20; // ❌ 错误!不能修改成员变量std::cout << "data = " << data << std::endl;}
};
✅ 作用: 确保 show()
这个函数不会修改 Test
类的成员变量。
1.5 总结
const
变量必须初始化,定义后不可修改。const
可用于常见变量、指针、引用、函数参数、类成员函数,确保数据不可变。- 使用
const
可以提高代码的安全性和可读性,防止意外修改数据。
指针类型 | 是否能修改指针的值(地址) | 是否能修改指针指向的值 |
---|---|---|
int* p | ✅ 可以修改 | ✅ 可以修改 |
int* const p | ❌ 不能修改 | ✅ 可以修改 |
const int* p | ✅ 可以修改 | ❌ 不能修改 |
const int* const p | ❌ 不能修改 | ❌ 不能修改 |
想要搞懂这些指针的类型,最行之有效的方法是从右向左阅读。以int*p为例,离p最近的是*号,表示p 是一个指针变量。再往左是该声明语句的基本数据类型部分确定了该指针指向的是一个int 对象。
再以int* const p为例,离p最近的符号是cosnt,意味着 p 本身是一个常量对象,对象的类型由声明符的其余部分确定。声明符中的下一个符号是*,意思是p是一个常量指针。最后,该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。
再以const int* p为例,离p最近的符号是*,表示p是一个指针变量,接下来是基本数据类型部分,确定了指针变量指向的是一个int对象,再往左是const,表示指针变量指向的int对象是一个常量值,不能被修改。
再以const int* const p为例,离p最近的符号是const,表示p是一个常量对象,接下来是*号,表示p是一个不能修改的常量指针,在接下来是int,表示该常量指针指向的是一个int对象,再接下来是const,表示该常量指针指向的int对象也是常量。
指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。若所指对象为非常量类型,则可以修改,若所指对象是常量,则不可以修改。
二、顶层const
指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的内容是不是一个常量就是两个独立的问题。用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量。
更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层cosnt则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const【常量指针】也可以是底层const【指向常量的指针】。
int i = 0;
int *const p1 = &i; //不能改变p1的值,这是一个顶层const
const int ci=42; // 不能改变ci的值,这是一个顶层const
const int *p2=&ci; // 允许改变p2的值,这是一个底层const
const int *const p3=p2; //靠右的const是顶层const,靠左的是底层const
const int &r = ci; // 用于声明引用的const都是底层const
在 C++ 中,
const
有“顶层const
”和“底层const
”的概念,它们的区别在于const
作用的对象不同。
1. 什么是“顶层
const
”?顶层
const
表示 变量本身 是const
,也就是说,变量不可修改。
- 适用于 基本数据类型、指针本身、对象本身 等。
- 但如果是指针,顶层
const
不影响指针指向的值。🔹 示例 1(普通变量的顶层
const
)const int a = 10; // a 是顶层 const,不能修改 a = 20; // ❌ 错误:a 是 const,不可修改
🔹 示例 2(指针本身的顶层
const
)int x = 5, y = 10; int* const p = &x; // p 是顶层 const,不能修改指向 p = &y; // ❌ 错误:p 本身是 const,不可修改指向 *p = 20; // ✅ 允许修改指向的值
int* const p
表示p
本身是常量指针,不能修改p
的指向,但*p
可以修改。
2. 什么是“底层
const
”?底层
const
影响的是 指针或引用所指向的对象,而不是指针本身。
- 不能通过该指针或引用修改所指向的值,但指针本身仍然可以改变指向。
🔹 示例 1(指向
const
的指针,底层const
)const int x = 10; const int* p = &x; // p 是指向 const int 的指针(底层 const) *p = 20; // ❌ 错误:不能修改 *p(底层 const) p = nullptr; // ✅ 允许修改 p 的指向
const int* p
表示*p
不能修改,但p
可以指向别的const int
。- 这里
p
不是const
,所以p = nullptr;
是允许的。🔹 示例 2(常引用,底层
const
)int a = 5; const int& ref = a; // ref 只能读取 a,不能修改 a(底层 const) ref = 10; // ❌ 错误:不能通过 ref 修改 a a = 10; // ✅ 允许修改 a(因为 a 不是 const)
const int& ref
表示ref
不能修改a
,但a
本身可以修改。
3. 顶层
const
vs. 底层const
类别 示例 作用 顶层 const
const int a = 10;
a
本身 不能修改int* const p = &x;
p
本身 不能修改(指向不变),但*p
可变底层 const
const int* p = &x;
*p
不能修改(指向const
),但p
本身可变const int& ref = a;
ref
不能修改a
(但a
本身可变)
4. 结合使用:顶层
const
+ 底层const
当
const
同时修饰指针本身和指向的对象,两者都会受到约束:const int a = 5; const int* const p = &a; // p 不能修改指向,*p 也不能修改 p = nullptr; // ❌ 错误:p 本身是 const *p = 10; // ❌ 错误:*p 也是 const
- 前一个
const
(const int*
)是底层const
,限制*p
不能改- 后一个
const
(const p
)是顶层const
,限制p
不能改
5.
const
在函数参数中的作用在函数参数中,
const
是顶层const
还是底层const
,会影响参数的传递方式。🔹 顶层
const
(值传递,不影响函数内部)void func(const int a) { // a 是顶层 consta = 10; // ❌ 错误:a 不能修改 }
- 这里
const
仅限于函数内部,外部调用时仍然会复制a
。🔹 底层
const
(引用或指针)void func(const int* p) { // p 指向 const int*p = 10; // ❌ 错误:不能修改 *pp = nullptr; // ✅ 允许修改 p }
- 这里
p
是 底层const
,它可以修改指向,但不能修改*p
。
6. 什么时候
const
既是顶层又是底层?如果
const
既修饰变量本身,又修饰它指向的内容,就既是顶层const
,也是底层const
:const int* const p = &x;
p
本身是const
(顶层const
),不能改变指向。*p
也是const
(底层const
),不能修改指向的值。
7. 总结
类型 作用 示例 顶层 const
限制变量本身不可修改 const int a = 5;
限制指针本身不可修改 int* const p = &x;
底层 const
限制指针或引用指向的值不可修改 const int* p = &x;
限制引用的值不可修改 const int& ref = a;
既是顶层又是底层 const
指针本身和指向的值都不能修改 const int* const p = &x;
这样清楚了吧?😃
三、constexpr关键字和常量表达式
3.1 常量表达式
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。【这句话中,要满足一个对象是常量表达式的条件有两个:第一个条件是用常量表达式初始化;第二个条件是该对象必须由const修饰】。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:
const int max_files = 20; // max_files 是常量表达式
const int limit = max_files+1; // limit是常量表达式
int staff_size = 27; // staff_size 不是常量表达式
const int sz = get_size(); // sz不是常量表达式
尽管staff_size 的初始值是个字面值常量,但由于它的数据类型只是一个普通的int而非const int,所以它不属于常量表达式。另一方面,尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。
3.2 constexpr变量
constexpr是一种函数,用于代表一条常量表达式。
在一个复杂的系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。
C++11新标准规定,允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20; // 20是常量表达式
constexpr int limit = mf+1; // mf+1是常量表达式
constexpr int sz=size(); // 只有当size是一个constexpr函数时,该语句才是一条正确的声明语句
如果你认定变量是一个常量表达式,那就把它声明成constexpr类型。
字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明 constexpr 时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把他们称为“字面值类型”(literal type)。
算术类型、引用和指针都属于字面值类型。自定义类、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。
尽管指针和引用都能定义成constexpr,但他们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或0,或者是存储于某个固定地址中的对象【这里的“存储于某个固定地址中的对象”,指的是在编译期就能确定地址的变量,例如:constexpr
变量、static
变量、全局变量】。
constexpr int* ptr1=nullptr; // 正确
constexpr int* ptr2=0; // 正确constexpr int &ptr3 = nullptr; // ❌ 错误,引用不能绑定 `nullptr`,引用必须绑定有效对象,nullptr 不是对象,所以不允许!constexpr int a = 10;
constexpr int &ptr4 = a; // ✅ 正确,引用绑定到 `constexpr` 变量
🔍 解析“存储于某个固定地址中的对象”
在 C++ 中,
constexpr
指针的初始值必须是:
nullptr
或0
- 存储于某个固定地址中的对象
这里的“存储于某个固定地址中的对象”,指的是在编译期就能确定地址的变量,例如:
constexpr
变量static
变量- 全局变量
📌 为什么有这个限制?
C++ 要求
constexpr
表达式必须在编译期求值。但大多数普通局部变量的地址只有在运行时才能确定,所以不能用于constexpr
指针初始化。🚫 非固定地址示例(错误)
constexpr int* p; // ❌ 错误,必须初始化int x = 10; constexpr int* p1 = &x; // ❌ 错误,x 是普通变量,地址在运行时才能确定
- 变量
x
存储在栈上,地址是在运行时才分配的,constexpr
需要编译期确定地址,所以会报错。
✅ 允许的情况
✅ 1. 指向
constexpr
变量constexpr int a = 10; // ✅ a 是编译期常量 constexpr const int* p = &a; // ✅ 正确,a 存在于固定地址
✔
a
在编译期就能确定地址,所以p
可以是constexpr
。
✅ 2. 指向
static
变量static int b = 20; // ✅ static 变量存放在静态存储区,地址固定 constexpr int* p = &b; // ✅ 正确
✔
static
变量在程序启动时就分配了固定地址,符合constexpr
要求。
✅ 3. 指向全局变量
int g = 30; // ✅ 全局变量存放在静态存储区,地址固定 constexpr int* p = &g; // ✅ 正确
✔ 全局变量和
static
变量类似,它们的地址在编译期就可以确定。
🚀 结论
“存储于某个固定地址中的对象” 指的是
constexpr
变量、static
变量、全局变量,因为它们的地址在编译期就能确定,而普通局部变量的地址是在运行时分配的,所以不能用于constexpr
指针初始化。💡 记住这个规则
constexpr int a = 10; // ✅ constexpr 变量 static int b = 20; // ✅ static 变量 int g = 30; // ✅ 全局变量//constexpr int* p1 = &a; // ❌ 错误 //这里,p1 是 constexpr int* 类型,即一个常量指针,指向一个**int 类型的常量**。然而,a 是 constexpr int 类型的常量变量,它的地址 &a 的类型是 const int*,而 p1 的类型是 int*,这两者是不匹配的。constexpr const int* p1 = &a; // ✅ 正确constexpr int* p2 = &b; // ✅ 正确 constexpr int* p3 = &g; // ✅ 正确 int x = 40; constexpr int* p4 = &x; // ❌ 错误,x 地址在运行时确定
指针和constexpr
必须明确一点, 在constexpr声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关:
const int *p = nullptr; // p是一个指向整型常量的指针
constexpr int *q = nullptr; // q 是一个指向整型的常量指针
p和q的类型相差甚远,p是一个指向常量的指针,而q是一个常量指针,其中的关键在于constexpr把它所定义的对象置为了顶层 const。
constexpr
关键字详解1.
constexpr
的作用
constexpr
用于声明必须在编译期求值的常量或函数,以提高程序运行效率并支持编译期优化。相比const
只是保证值不变,constexpr
还强制表达式在编译时计算。
2.
constexpr
适用于哪些场景?
- 修饰变量(编译时常量)
- 修饰函数(编译期计算函数)
- 修饰类的成员函数
- 修饰构造函数(使对象在编译期构造)
3.
constexpr
修饰变量constexpr int x = 10; // ✅ 编译时计算 const int y = 10; // ❌ 可能在运行时初始化
🔹
constexpr
必须用编译期常量初始化,否则报错:int getValue() {return 20; } constexpr int a = getValue(); // ❌ 错误,getValue() 运行时求值
✔ 使用
constexpr
的变量可以用于数组大小、模板参数等:constexpr int size = 5; int arr[size]; // ✅ 正确,数组大小必须是编译期常量
4.
constexpr
修饰函数constexpr int square(int x) {return x * x; }
🔹 如果参数是编译期常量,那么
square(5)
在编译期计算:constexpr int result = square(5); // ✅ 直接计算出 25
🔹 但如果参数是运行时变量,函数会在运行时计算:
int n; std::cin >> n; int value = square(n); // ❌ 运行时计算
✔
constexpr
函数可以用于数组大小、模板参数等:constexpr int size = square(4); int arr[size]; // ✅ 正确
5.
constexpr
修饰类的成员函数class Test { public:constexpr int getValue() const {return 10;} };
✔ 如果
getValue()
在constexpr
语境下调用,就能在编译期求值:constexpr Test obj; constexpr int val = obj.getValue(); // ✅ 编译期求值
6.
constexpr
修饰构造函数class Point { public:constexpr Point(int x, int y) : x_(x), y_(y) {}constexpr int getX() const { return x_; } private:int x_, y_; };
✔ 允许创建 编译期对象:
constexpr Point p(10, 20); constexpr int x = p.getX(); // ✅ 在编译期计算
7.
constexpr
vsconst
关键字 编译期求值 运行时求值 能否修改 常见用途 constexpr
✅ 必须编译期求值 ❌ 不能运行时求值 ❌ 不能修改 编译期常量、优化计算 const
⚠ 不一定编译期求值 ✅ 可能在运行时求值 ❌ 不能修改 保护变量不被修改
8. 什么时候用
constexpr
?
- 需要在编译期计算的情况(如数组大小、模板参数)
- 需要优化计算,提高运行时性能
- 需要保证某个值始终是编译期常量
- 定义编译期常量的构造函数(如
std::array
)
9.
constexpr
结合if constexpr
在 C++17 之后,可以结合
if constexpr
进行编译期分支优化:template <typename T> constexpr auto getTypeSize() {if constexpr (std::is_integral<T>::value) {return sizeof(T);} else {return 0;} }
✔
if constexpr
只会编译符合条件的分支,提高效率:constexpr int size = getTypeSize<int>(); // ✅ 计算出 4(假设 int 是 4 字节)
总结
✅
constexpr
适用于:
- 常量表达式(编译期求值)
constexpr
函数(可编译期计算)- 优化运行时性能
- 类的
constexpr
成员函数、构造函数- 结合
if constexpr
进行编译期分支优化你现在理解
constexpr
了吗?😃
为什么 C++11 引入
constexpr
?在 C++11 之前,C++ 主要依赖
const
和 预处理宏 (#define
) 来定义常量,但它们存在缺陷,无法保证编译期求值。为了解决这些问题并优化性能,C++11 引入了constexpr
,让变量和函数可以在编译期求值,减少运行时计算的开销。
C++11 引入
constexpr
的主要原因1. 让编译期计算更安全
在 C++98/03 中,
const
变量可能在运行时初始化,无法保证一定是编译期常量:const int getSize() { return 10; } // ❌ 可能在运行时计算 const int size = getSize(); // ❌ 不能用于数组大小 int arr[size]; // ❌ 编译失败
C++11
constexpr
解决了这个问题:constexpr int getSize() { return 10; } // ✅ 编译期计算 constexpr int size = getSize(); // ✅ 直接在编译期求值 int arr[size]; // ✅ 编译成功
✔ 优势:保证所有
constexpr
变量和函数必须在编译期求值,防止运行时计算,提高安全性。
2. 让
constexpr
变量可以用于模板参数在 C++98/03 中,模板参数需要一个编译期常量,但
const
变量不一定是编译期常量:const int N = 5; template<int T> class Array {}; Array<N> arr; // ❌ 错误,N 可能是运行时变量
C++11
constexpr
让N
必须是编译期常量,可以安全用于模板参数:constexpr int N = 5; Array<N> arr; // ✅ 正确,N 是编译期常量
✔ 优势:使模板更安全、更强大,避免因
const
变量可能是运行时计算导致的错误。
3. 允许编译期计算复杂函数
C++98/03 不能在编译期计算复杂数学公式:
double pow(double base, int exp) {double result = 1;while (exp--) result *= base;return result; } constexpr double area = 3.14 * pow(2.0, 2); // ❌ 错误,运行时计算
C++11
constexpr
让pow()
可以在编译期计算:constexpr double pow(double base, int exp) {return (exp == 0) ? 1 : base * pow(base, exp - 1); } constexpr double area = 3.14 * pow(2.0, 2); // ✅ 编译期计算
✔ 优势:支持递归计算、数学运算、编译期优化,提升运行时性能。
4. 让类对象支持编译期常量
C++98/03 不能创建编译期构造的对象:
struct Point {int x, y;Point(int a, int b) : x(a), y(b) {} }; constexpr Point p(10, 20); // ❌ 错误,构造函数在运行时调用
C++11
constexpr
允许构造函数在编译期执行:struct Point {int x, y;constexpr Point(int a, int b) : x(a), y(b) {} }; constexpr Point p(10, 20); // ✅ 编译期创建对象
✔ 优势:允许在编译期创建类对象,减少运行时构造的开销。
总结
C++11 引入
constexpr
,主要是为了解决 C++98/03 时代const
变量不能保证编译期计算的问题,提升代码安全性、性能、模板能力,同时支持编译期数学计算和编译期构造对象。✔ 核心优势
- 强制编译期求值,避免
const
变量在运行时初始化的隐患- 支持模板参数,让
constexpr
变量可安全用于模板- 支持编译期数学计算,减少运行时计算开销
- 支持编译期构造对象,优化类的初始化性能
💡 一句话总结:C++11 引入
constexpr
让编译期计算更安全、高效,使代码更优化、更强大、更符合现代 C++ 需求。