一、二者区别
const 表示“只读”的语义,constexpr 表示“常量”的语义 。
constexpr(常量表达式):是指值不会改变并且在编译过程就能得到计算结果的表达式。
常量表达式的优点是将计算过程转移到编译时期,那么运行期就不再需要计算了,程序性能也就提升了。
复杂系统中很难分辨⼀个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是⼀个常量表达式。
1.修饰变量
①普通变量
constexpr 只能定义编译期常量,⽽ const 可以定义编译期常量,也可以定义运⾏期常量。
二者修饰的变量在定义时必须初始化。
编译期常量:在编译器就能得到值。
运行期常量:在运行时才能得到值。
const int a = 5; //编译期常量
constexpr int aa = 5; //编译期常量,不会报错
string s = "hello";
constexpr int aaa = s.length(); //运行期常量,编译错误
②成员变量
二者修饰的类成员只能通过构造函数的初始化列表初始化。
2.修饰指针
首先了解两个概念:顶层const, 底层const。
顶层const代表指针变量自身无法修改;底层const代表指针所指对象无法修改。
用const修饰指针:左定值(底层),右定向(顶层)
int a = 3;
int b = 4;
const int *p = &a; //左定值,不能修改指向变量的值,但是能修改指向的变量
*p = 33; //编译错误,无法修改*p的值
p = &b; //可以运行,p可以指向其他变量
int *const p = &a; //右定向,可以修改指向变量的值,但是不能再指向其他变量。
*p = 33; //可以运行,*p的值可以被修改
p = &b; //编译错误,p不能再指向其他变量
用constexpr修饰指针:constexpr只对指针有效,与指针所指对象无关。
这句话的意思是constexpr修饰的指针是一个指向变量的常对象,指针的方向不能改变,指向的对象可以改变。
// a的定义必须放在函数体外
int a = 5;
// 函数体内
constexpr int *p1 = &a;
cout << *p1 << endl; // 5
*p1 = 7;
cout << a << endl; // 7
p1 = nullptr; // 错误,constexpr指针无法修改
3.修饰普通函数
constexpr函数是指能⽤于常量表达式的函数。
constexpr int new() {return 42;}
函数的返回类型和所有形参类型都是字⾯值类型,函数体有且只有⼀条return语句。 为了可以在编译过程展开,constexpr函数被隐式转换成了内联函数。 constexpr和内联函数可以在程序中多次定义,⼀般定义在头⽂件。
从C++11开始,constexpr函数不仅可以返回常量,还可以进行递归操作。
从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句。
4.修饰成员函数
const修饰成员函数,通常称为const函数,表示该函数不会修改类的状态(即不会通过任何方式修改类数据成员)。另外,const类对象,只能调用const函数,确保不会修改类的数据成员。
constexpr无法修饰成员函数,只能作为函数返回值类型,表明该函数返回的是一个编译期可确定的常量。
constexpr构造函数必须有⼀个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调⽤的成员函数必须使⽤ constexpr 修饰
class ss {
public:
int m_a;
int m_size;
public:
constexpr ss(int a, int b) : m_a(a), m_size(b) {}
public:
int size() const {
m_size = 10; // 编译错误,不能写任何数据成员
return m_size;
}
const int size() {
m_size = 20; // 函数体可以修改数据成员,但返回类型是const,也就是调用者无法修改
return m_size;
}
constexpr int getMaxSize() { return INT_MAX; } // 不能返回非常量值
int getMaxSize() constexpr { return INT_MAX; } //编译错误,constexpr无法修饰成员函数,只能修饰返回值
};
二、修改const修饰的变量
1.修改全局变量
全局const变量是存放于rodata(只读)区的,无论如何都不能修改。
2.修改局部变量
若要修改const修饰的局部变量的值,需要加上关键字volatile。
const int w = 2;
volatile const int m = 3;
int* p = (int*) & w;
*p = 4;
cout << &w << endl; //000000088D9FF734
cout << p << endl; //000000088D9FF734
cout << *p << endl; // 4
cout << w << endl; // 2
int* pp = (int*) & m;
*pp = 4;
cout << (void*) & m << endl;//0000000E291BFA14
cout << pp << endl; //0000000E291BFA14
cout << *pp << endl; // 4
cout << m << endl; // 4
对于w来说,虽然没有报错,修改成功,但是w的值仍然是2。
可是指针的地址和w的地址是一样的啊,这是为什么呢?难道一个地址可以存放两个不同的值吗?这显然是不符合常理的。
其实,这是C++中的常量折叠现象:const变量(即常量)值放在编译器的符号表中,计算时编译器直接从表中取值,省去了访问内存的时间,这是编译器进行的优化。w是const变量,编译器对w在预处理的时候就进行了替换。编译器只对const变量的值读取一次。所以打印的是2。w实际存储的值被指针p所改变。简而言之就是读取const类型的变量不会取内存中读取,而是在编译器符号表中读取。
而对于m来说,m是volatile const类型变量,告诉编译器该变量属于易变的,不要对此句进行优化,每次计算时要去内存中读取该变量的值,进而避免出现常量折叠的问题。
volatile
定义: [与const绝对对⽴的,是类型修饰符]
影响编译器编译的结果,⽤该关键字声明的变量表示该变量随时可能发⽣变 化,与该变量有关的运算,不要进⾏编译优化;会从内存中重新装载内容,⽽不是直接从寄存器拷⻉内容。
作⽤: 指令关键字,确保本条指令不会因编译器的优化⽽省略,且要求每次直接读值,保证对特殊地址的稳定访问
使⽤场合: 在中断服务程序和cpu相关寄存器的定义
举例说明: 空循环
for(volatile int i=0; i<100000; i++); // 它会执⾏,不会被优化掉
至于为什么在输出m的地址时需要用void*强转,请移步为什么对于volatile int *p,cout << p输出会是这样? - 知乎 (zhihu.com)
三、使用const的好处
1、可以定义const常量
这样可以避免由于无意间修改数据而导致的编程错误。
2、便于进行类型检查
const常量有数据类型,而宏常量(define)没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误。
3、为函数重载提供了一个参考
const修饰的函数可以看作是对同名函数的重载。
4、可以节省空间,避免不必要的内存分配
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象宏一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而宏定义的常量在内存中有若干个拷贝。
5、提高了效率
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期的常量,没有了存储与读内存的操作,使得它的效率也很高。