C++----const关键字与constexpr关键字

server/2025/3/6 20:47:21/

目录

一、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 = &pi; //错误:ptr是一个普通指针const double* ptr = &pi; //正确: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 = &pi; //正确: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 通常通过 RDIRSI 传递。
    • 在某些情况下(如参数较多时),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 = &pi; //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 的一个别名,但由于 refconst,它禁止通过 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 变成 10ref 读取 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

类别示例作用
顶层 constconst int a = 10;a 本身 不能修改
int* const p = &x;p 本身 不能修改(指向不变),但 *p 可变
底层 constconst 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
  • 前一个 constconst int*)是底层 const,限制 *p 不能改
  • 后一个 constconst 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 指针的初始值必须是:

  1. nullptr0
  2. 存储于某个固定地址中的对象

这里的“存储于某个固定地址中的对象”,指的是在编译期就能确定地址的变量,例如:

  • 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 vs const

关键字编译期求值运行时求值能否修改常见用途
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 constexprN 必须是编译期常量,可以安全用于模板参数

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 constexprpow() 可以在编译期计算

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 变量不能保证编译期计算的问题,提升代码安全性、性能、模板能力,同时支持编译期数学计算和编译期构造对象

核心优势

  1. 强制编译期求值,避免 const 变量在运行时初始化的隐患
  2. 支持模板参数,让 constexpr 变量可安全用于模板
  3. 支持编译期数学计算,减少运行时计算开销
  4. 支持编译期构造对象,优化类的初始化性能

💡 一句话总结:C++11 引入 constexpr 让编译期计算更安全、高效,使代码更优化、更强大、更符合现代 C++ 需求


http://www.ppmy.cn/server/172965.html

相关文章

常用的分布式 ID 设计方案

文章目录 1.UUID2.数据库自增 ID3.雪花算法4.Redis 生成 ID5.美团 Leaf 1.UUID 原理&#xff1a;UUID 是由数字和字母组成的 128 位标识符&#xff0c;通过特定算法随机生成&#xff0c;包括时间戳、计算机网卡地址等信息。常见的版本有版本 1&#xff08;基于时间戳和 MAC 地…

深入理解设计模式中的工厂模式(Factory Pattern)

各类资料学习下载合集 ​​https://pan.quark.cn/s/8c91ccb5a474​​ 工厂模式是创建对象的一种设计模式,属于创建型设计模式。它提供了一种方法来创建对象,而无需在代码中直接指定对象的具体类。工厂模式通过将对象的创建过程封装起来,使得代码更加灵活、可维护…

windows 上删除 node_modules

在 Windows 11 上&#xff0c;你可以通过命令行来删除 node_modules 文件夹并清除 npm 缓存。以下是具体步骤&#xff1a; 删除 node_modules 打开命令提示符&#xff08;Command Prompt&#xff09;或终端&#xff08;PowerShell&#xff09;。 导航到项目目录。你可以使用 …

芯科科技通过全新并发多协议SoC重新定义智能家居连接

MG26系列SoC现已全面供货&#xff0c;为开发人员提供最高性能和人工智能/机器学习功能 致力于以安全、智能无线连接技术&#xff0c;建立更互联世界的全球领导厂商Silicon Labs&#xff08;亦称“芯科科技”&#xff0c;NASDAQ&#xff1a;SLAB&#xff09;&#xff0c;日前宣…

Spring Boot 常用注解全解析:从核心到进阶的实践指南

目录 引言&#xff1a;为什么注解是Spring Boot开发者的“战略武器”&#xff1f; 一、核心启动注解 1.1 应用启动三剑客 二、Web开发注解 2.1 控制器层注解 三、依赖注入注解 3.1 依赖管理矩阵 四、数据访问注解 4.1 JPA核心注解 五、配置管理注解 5.1 配置绑定注解…

【javaEE】计算机是如何工作的(基础常识)

1.❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; Hello, Hello~ 亲爱的朋友们&#x1f44b;&#x1f44b;&#xff0c;这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章&#xff0c;请别吝啬你的点赞❤️❤️和收藏&#x1f4d6;&#x1f4d6;。如果你对我的…

【2025.3最新版】从零开始的CSS网页开发学习笔记 1(包含CSS简介 CSS基础选择器 CSS字体属性 CSS文本属性 CSS引入方式)

文章目录 CSS简介CSS基本介绍CSS基本语法规范CSS代码风格 CSS基础选择器选择器的作用和分类标签选择器类选择器id选择器通配符选择器 CSS字体属性font-family 字体类型设置font-size 字体大小设置font-weight 字体粗细设置font-style 字体样式设置font 字体复合属性设置 CSS文本…

Checkpoint 模型与Stable Diffusion XL(SDXL)模型的区别

Checkpoint 模型与 Stable Diffusion XL&#xff08;SDXL&#xff09;模型 在功能、架构和应用场景上有显著区别&#xff0c;以下是主要差异的总结&#xff1a; 1. 基础架构与定位 Checkpoint 模型 是基于 Stable Diffusion 官方基础模型&#xff08;如 SD 1.4/1.5&#xff09;…