const 和 宏

const 常量与 #define 宏定义区别在于 const 常量具有类型的,可以进行类型检查,而#define宏定义只是字符串替换,不能进行类型检查。

  1. #include <iostream>
  2. using namespace std;
  3. int main() {
  4. const int const_value = 100;
  5. int* ptr = (int*)&const_value;
  6. *ptr = 200;
  7. cout << const_value << endl;
  8. return 0;
  9. }

答案是 100。这是因为编译器会对程序进行优化,编译器发现 const_value是常量,在遇到使用 const_value 的地方时,直接采用类似于宏替换的方法,将 const_value 换为之前的常数(例如此代码中用 100 替换)。

但是如果查看内存会发现 const_value 处的数值确实被修改为 100 了。即 *ptr 的值为 100。

const 是如何保证不被修改的?

答案是:分2种变量。一种是在函数内部定义的 const ,一种是定义在全局的 const 。

如果是函数内部定义的const,编译器来检查你有没有修改 const 。比如你如果在函数内部写

  1. const int const_value = 100;
  2. const_value = 200;

编译器会报错。但是我们可以骗编译器,比如

  1. const int const_value = 100;
  2. int * ptr = (int *)&const_value;
  3. *ptr = 200;

嗯,然后你可以修改所谓的const代码了。但是如果说是在全局变量里定义的,比如下面这个例子。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. static const char const_data[16] = "I'm Const Data";
  4. int main(int args,char ** argv)
  5. {
  6. char * pc = (char *)const_data;
  7. *pc = 'X';
  8. return 0;
  9. }

可以过编译,但是会出现运行时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 引用

先看普通引用以及普通引用不允许的行为,普通引用的右边可以是左值。

  1. int i = 2;
  2. double &j = i; // 错误:引用类型与对象类型不一致
  3. int &i = 2; // 错误:不允许用右值初始化
  4. int &j = a * 2 // 错误:不允许用表达式初始化

但是 const 引用会有不一样

  1. int i = 2;
  2. const double &j = i; // 正确:j是常量引用
  3. const int &i = 2; // 正确:i是常量引用
  4. const int &j = a * 2 // 正确:j是常量引用

上面三种写法 const 引用将会额外创建一个临时变量,并绑定上去。C++支持这种做法的目的在于,既然不能通过 const 引用修改对象值,那么额外创建一个常量和直接绑定对象并没有什么区别,所以干脆让 const 引用支持这种非常规做法。

顶层 const 和底层 const

  1. int i = 0;
  2. int *const j = &i; // 指针j指向i,const修饰指针j本身,所以j的地址值不允许修改,但是可以通过j修改i的值
  3. 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。

  1. constexpr int foo(int i)
  2. {
  3. return i + 5;
  4. }

constexpr修饰的函数还会自动加上inline,任何数字的字面量都是常量表达式,常量表达式和常量表达式进行的算术运算都是常量表达式。

const 函数

  1. void fcn(const int i) { /* ... */ }

这个函数中,变量i为值传递形参,根据值传递的初始化规则,形参i是否为const与传入的实参是否为const是完全无关的。这里的const仅表示i在函数体中不允许修改。

因为值传递的const形参在调用上与非const形参没有区别,所以仅仅使用const无法区分参数类别,所以无法实现函数重载,如下的重载是错误的:

  1. void fcn1(const int i) { /* ... */ }
  2. void fcn1(int i) { /* ... */ } // 错误:重复定义函数,不能实现重载

下面来看指针参数,对于顶层const的指针,与上一个一样,顶层const仅表示指针/引用本身在函数体中不允许修改。由于底层const描述实参性质,可以在调用时区分const,所以使用底层const的指针/引用可以实现函数重载

  1. void fcn3(int &x) { /* ... */ }
  2. void fcn3(const int &x) { /* ... */ } // 新函数,作用于const的引用
  3. 所以可以分别调用两个函数:
  4. int i = 0;
  5. fcn3(i); // 正确:调用第一个函数
  6. const int j = 0;
  7. fcn3(j); // 正确:调用第二个函数

小结:定义了底层const的形式参数,它们可以接受const或非const对象。因为底层是对实参的表述所以可以对接受的参数进行重载

const 和 typedef

  1. typedef int* pint;
  2. const pint mycpint = 0;

如果简单地将 pint 替换成 int*

  1. const int* mycpint; // mycpint 指向 const int,是一个底层 const
  1. typedef int* pint;
  2. int main() {
  3. int i = 1;
  4. const pint mycpint = &i;
  5. *mycpint = 2;
  6. return 0;
  7. }

实际上上面这行代码会正常运行,说明并不是简单地替换。想要理解 const 和作用范围只要加上括号即可

  1. typedef IPTR int*;
  2. const int* == (const int)* // 指向const int
  3. int* const == (int*) const
  4. const int* const == (const int*) const
  5. const IPTR == IPTR const == (int*) const == int* const
  6. const IPTR const == const (int*) const //两个const意义重复,依旧等同于int* const

cosnt 和类函数

  1. class Student {
  2. std::string name;
  3. std::string getName const() {
  4. return name;
  5. }
  6. }

类函数名后面加一个 const 代表这个函数不会修改这个类的数据

  1. class Student {
  2. std::string name;
  3. mutable int age;
  4. std::string getName const() {
  5. age++
  6. return name;
  7. }
  8. }

const意思是“这个函数不修改对象内部状态”。为了保证这一点,编译器也会主动替你检查,确保你没有修改对象成员变量—否则内部状态就变了。mutable意思是“这个成员变量不算对象内部状态”。

比如,你搞了个变量,用来统计某个对象的访问次数(比如供debug用)。它变成什么显然并不影响对象功用,但编译器并不知道:它仍然会阻止一个声明为const的函数修改这个变量。把这个计数变量声明为mutable,编译器就明白了:这个变量不算对象内部状态,修改它并不影响const语义,所以就不需要禁止const函数修改它了。