const 和 宏
const 常量与 #define 宏定义区别在于 const 常量具有类型的,可以进行类型检查,而#define宏定义只是字符串替换,不能进行类型检查。
#include <iostream>
using namespace std;
int main() {
const int const_value = 100;
int* ptr = (int*)&const_value;
*ptr = 200;
cout << const_value << endl;
return 0;
}
答案是 100。这是因为编译器会对程序进行优化,编译器发现 const_value是常量,在遇到使用 const_value 的地方时,直接采用类似于宏替换的方法,将 const_value 换为之前的常数(例如此代码中用 100 替换)。
但是如果查看内存会发现 const_value 处的数值确实被修改为 100 了。即 *ptr 的值为 100。
const 是如何保证不被修改的?
答案是:分2种变量。一种是在函数内部定义的 const ,一种是定义在全局的 const 。
如果是函数内部定义的const,编译器来检查你有没有修改 const 。比如你如果在函数内部写
const int const_value = 100;
const_value = 200;
编译器会报错。但是我们可以骗编译器,比如
const int const_value = 100;
int * ptr = (int *)&const_value;
*ptr = 200;
嗯,然后你可以修改所谓的const代码了。但是如果说是在全局变量里定义的,比如下面这个例子。
#include <stdio.h>
#include <stdlib.h>
static const char const_data[16] = "I'm Const Data";
int main(int args,char ** argv)
{
char * pc = (char *)const_data;
*pc = 'X';
return 0;
}
可以过编译,但是会出现运行时runtime memory violation。为什么呢?
答案是,在生成ELF的过程中,代码的各部分变量或者数据不是全部无脑放在一起的。简单说一下重点分区,其中 .rodata 分区会存放全局常量(也就是我们这个例子中的 const_data ),.text 分区存放源码编译的机器指令。你可以查到.rodata分区会和.text分区会加载到一个段中,并且可操作权限只有 R + E,而没有 W ,所以你 write 的时候会报错——执行的时候发现你往一块没有写权限的内存写东西了。
所以本质上,为什么2个const一个能改一个不能,就是因为变量所在的存储区不同。函数级变量是在函数的帧里的,程序拥有对这个存储区写的权限。而全局性的const变量是放在另一个存储区里的,程序默认不拥有写权限。
const 变量的作用域
const 修饰的全局变量默认是内部链接(只在当前源文件有效 不能直接用于其他源文件)如果必须用在其他源文件 使用只读的全局变量 必须加extern将它转换成外部链接
C++ 对 const 优化
c++ 中用 const 定义了一个常量后,不会分配一个空间给它,而是将其写入符号表(symbol table),符号表需要在编译期间用到,记录符号的具体信息,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
通过指针的方式修改常量对应的内存空间中的值时,这种修改不会影响到常量本身的值,因为用到该常量(local)的时候,编译器根本不会去进行内存空间的读取。这就是c++的常量折叠(constant folding)。
将 const 常量放在符号表中,而并不给其分配内存,编译器直接进行替换优化。除非需要用到 local 的存储空间的时候,编译器迫不得已才会分配一个空间给 local ,(在 release 条件下,是监听不到 const 变量地址的)但之后 local 的值仍旧从符号表中读取,不管 local 的存储空间中的值如何变化,都不会对常量local产生影响。
在C中却不是这样,C没有 constant folding 的概念,用 const 定义一个常量的时候,编译器会直接开辟一个内存空间存放该常量,不会进行优化,所以从内存空间对其进行了修改,内存空间的值变化了,常量本身的值也就变化了(跟变量一样,只不过常量不能直接用常量名二次赋值或初始化)。
const 引用
先看普通引用以及普通引用不允许的行为,普通引用的右边可以是左值。
int i = 2;
double &j = i; // 错误:引用类型与对象类型不一致
int &i = 2; // 错误:不允许用右值初始化
int &j = a * 2 // 错误:不允许用表达式初始化
但是 const 引用会有不一样
int i = 2;
const double &j = i; // 正确:j是常量引用
const int &i = 2; // 正确:i是常量引用
const int &j = a * 2 // 正确:j是常量引用
上面三种写法 const 引用将会额外创建一个临时变量,并绑定上去。C++支持这种做法的目的在于,既然不能通过 const 引用修改对象值,那么额外创建一个常量和直接绑定对象并没有什么区别,所以干脆让 const 引用支持这种非常规做法。
顶层 const 和底层 const
int i = 0;
int *const j = &i; // 指针j指向i,const修饰指针j本身,所以j的地址值不允许修改,但是可以通过j修改i的值
const int *k = &i; // 指针k指向i,const修饰k指向的i,所以k的地址值可以修改,但是不可以通过k修改i的值
修饰指针j本身的const称为顶层const,修饰k所指向变量i的const成为底层const。底层const与顶层const是两个互相独立的修饰符,互不影响。
底层const只能保证不可以通过这个变量来修改对象的值。
cosntexper
对于修饰Object来说,const并未区分出编译期常量和运行期常量constexpr限定在了编译期常量
constexpr修饰的函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。不过,我们不必因此而写两个版本,所以如果函数体适用于constexpr函数的条件,可以尽量加上constexpr。
constexpr int foo(int i)
{
return i + 5;
}
constexpr修饰的函数还会自动加上inline,任何数字的字面量都是常量表达式,常量表达式和常量表达式进行的算术运算都是常量表达式。
const 函数
void fcn(const int i) { /* ... */ }
这个函数中,变量i为值传递形参,根据值传递的初始化规则,形参i是否为const与传入的实参是否为const是完全无关的。这里的const仅表示i在函数体中不允许修改。
因为值传递的const形参在调用上与非const形参没有区别,所以仅仅使用const无法区分参数类别,所以无法实现函数重载,如下的重载是错误的:
void fcn1(const int i) { /* ... */ }
void fcn1(int i) { /* ... */ } // 错误:重复定义函数,不能实现重载
下面来看指针参数,对于顶层const的指针,与上一个一样,顶层const仅表示指针/引用本身在函数体中不允许修改。由于底层const描述实参性质,可以在调用时区分const,所以使用底层const的指针/引用可以实现函数重载:
void fcn3(int &x) { /* ... */ }
void fcn3(const int &x) { /* ... */ } // 新函数,作用于const的引用
所以可以分别调用两个函数:
int i = 0;
fcn3(i); // 正确:调用第一个函数
const int j = 0;
fcn3(j); // 正确:调用第二个函数
小结:定义了底层const的形式参数,它们可以接受const或非const对象。因为底层是对实参的表述所以可以对接受的参数进行重载
const 和 typedef
typedef int* pint;
const pint mycpint = 0;
如果简单地将 pint 替换成 int*
const int* mycpint; // mycpint 指向 const int,是一个底层 const
typedef int* pint;
int main() {
int i = 1;
const pint mycpint = &i;
*mycpint = 2;
return 0;
}
实际上上面这行代码会正常运行,说明并不是简单地替换。想要理解 const 和作用范围只要加上括号即可
typedef IPTR int*;
const int* == (const int)* // 指向const int
int* const == (int*) const
const int* const == (const int*) const
const IPTR == IPTR const == (int*) const == int* const
const IPTR const == const (int*) const //两个const意义重复,依旧等同于int* const
cosnt 和类函数
class Student {
std::string name;
std::string getName const() {
return name;
}
}
类函数名后面加一个 const 代表这个函数不会修改这个类的数据
class Student {
std::string name;
mutable int age;
std::string getName const() {
age++
return name;
}
}
const意思是“这个函数不修改对象内部状态”。为了保证这一点,编译器也会主动替你检查,确保你没有修改对象成员变量—否则内部状态就变了。mutable意思是“这个成员变量不算对象内部状态”。
比如,你搞了个变量,用来统计某个对象的访问次数(比如供debug用)。它变成什么显然并不影响对象功用,但编译器并不知道:它仍然会阻止一个声明为const的函数修改这个变量。把这个计数变量声明为mutable,编译器就明白了:这个变量不算对象内部状态,修改它并不影响const语义,所以就不需要禁止const函数修改它了。