const 与 volatile 一起并称 CV 限定符,用于指定被声明对象或被命名类型的常量性或易变性。
const 全称 constant,其指定一个约束,告知编译器该变量无法被修改。对于那些明确不发生改变的变量,应尽可能使用 const,以获得编译器的帮助。
#const 普通变量
声明时,const 与 typename 顺序可以互换,并且可以直接初始化常量,之后就不能对常量进行修改了。
const int a;         // OK!
int const b = 0;     // OK!
int c = 1;
a = c;               // ERROR! 无法对 const 进行修改
c = a;               // OK!
int const nums[10];  // OK!
#const 指针
const 搭配指针使用时,也会出现不同的顺序:
- 
底层 const:
const位于*之前,是为常量指针,表明指针指向的变量为常量,无法通过指针修改指向的对象; - 
顶层 const:
const位于*之后,是为指针常量,表明指针为常量,无法修改指针变量本身; 
const int foo = 0;
int bar = 1;
int *p = &foo;             // ERROR! 常量只能被常量指针指向
int *q = (int*)&foo;       // OK!
const int* a = &foo;       // OK! 常量指针
int const* b = &bar;       // OK! 可以指向非常量,但无法通过 *b = ? 的方式修改变量 bar
int* const d = &bar;       // OK! 指针常量
const int* const d = &foo; // OK! 指向常量的常量指针
// *a = 1, d = &foo 这些操作都是编译报错的
上面这段代码其实存在漏洞,即将 foo 的地址强转为 int* 类型并赋值给了 int* q。这是不安全的,因为 p 并不是常量指针,可以凭借 p 修改指向的变量,如果加上下面这段代码,则会发现一些奇妙的事:
(*q)++;
std::cout << q << " " << *q << "\n"
          << &foo << " " << foo << " " << *(&foo);
// output:
// 0x78fe0c 1
// 0x78fe0c 0 1
不难发现,q 与 &foo 为同一个地址,但奇怪的是,*q 与 foo 值不同,并且 foo 竟然与 *(&foo) 的值也产生了差异!
Actually,函数中定义的常量放在内存的栈区,而栈内存是可以通过指针修改的,不过即使修改了这块内存,在程序中仍然看起来没有修改常量,这是因为 C++ 编译器对 const 做了优化——当编译器遇到 foo 的定义时,会在内存的某个位置开辟一张表,然后将 key-value 对 {foo, 0} 写入该表,这就相当于将常量 0 绑定在符号 foo 上,之后每次取数据 foo 时,虽然运行时栈会为 foo 分配内存,但不是取栈中地址对应的值,而会去查这张表,然后用 value: 0 直接代替。
所以尽管指针 p 对地址上的值进行了修改,打印 foo 时我们得到的并不是真正的地址 &foo 上的值。
但并不是所有的常量声明时都是如此,比如将上面的代码略作修改:
// case 1
int bar = 0;
const int foo = bar;
int *q = (int*)&foo;
(*q)++;
std::cout << q << " " << *q << "\n"
          << &foo << " " << foo << " " << *(&foo);
// output:
// 0x78fe0c 1
// 0x78fe0c 1 1
// case 2
constexpr int bar = 0;
const int foo = bar;
int *q = (int*)&foo;
(*q)++;
std::cout << q << " " << *q << "\n"
          << &foo << " " << foo << " " << *(&foo);
// output:
// 0x78fe0c 1
// 0x78fe0c 0 1
发现上面两个 case 唯一的区别在于变量 bar 是否为 constexpr,换句话说,就是用于初始化 foo 的变量值是否在编译时可知。如果只有到了运行时才能确定常量 foo 的值,那么编译器并不会写 key-value 进表,而是表现地像 C 语言一样,直接从栈上获取数据;反之,如果在编译时就能确定值(比如上面那个 const int foo = 0;),编译器就会跟我们上面讨论的一样运作。
所以有些时候改用
constexpr是更好的选择。
而如果对全局变量进行 const 约束,此时变量分配在静态区,那么无论怎样都无法修改。
const int a;
int main() {
  int *p = (int*)&a;
  (*p)++; // ERROR!
}
#const 引用
修饰引用时,const 只能位于 & 左侧,毕竟引用变量本身一经初始化就无法更改,自带 const 语义。这种情况下,const + 引用均视为常量引用,即引用的变量为常量,无法修改。
const int foo = 1;
int bar = 2;
const int& a = foo; // OK!
const int& b = bar; // OK!
int const& c = foo; // OK!
int& const d = foo; // ERROR!
// a = 1 编译报错
#const 函数
const 与函数搭配只有两种情况:
- 
修饰形参。此时函数体内无法修改 const 修饰的形参;
 - 
修饰函数返回值。
 
#const 类成员变量
类中定义常量主要有以下实现方式:
- 
枚举。此时枚举变量相当于静态变量,在编译时可知。
class A { public: enum test { foo, bar }; // static int nums1[test::foo]; int nums2[test::bar]; }; std::cout << A::test::foo; // output: 0 - 
const 修饰。仅用 const 修饰的变量为非静态变量,只有在运行时才能确定值。仅能在构造函数的初始化列表进行初始化,或者直接就地初始化。此后无法再修改。
class A { public: const int foo{0}; int bar[foo]; // ERROR! invalid use of non-static A::foo // A() { foo = 1; } // ERROR! A(): foo(1) {} void test() { foo++; } // ERROR! foo is const }; 
#const 类成员函数
除了普通函数的用法外,类成员函数还可以在函数体前加上 const 修饰符,表明该成员函数不会修改任何非 mutable 关键字修饰的成员变量。此时 this 隐式为 const *(更严谨地说应该为 const *const,因为 this 不可修改指向),表明在该函数体内,编译器将该对象视为 const 对象。对于 const 对象,只能调用 const 成员函数,因为非 const 函数无法保证不会修改成员变量。
class Foo {
 public:
  void show() const {
    std::cout << bar;
    bar++;               // ERROR!
    Foo* p = this;       // ERROR! this 为 const Foo*
    const Foo* q = this; // OK!
  }
 private:
  int bar{1};
};
此外,const 类成员函数亦可与不加 const 的同名成员函数产生不同重载版本。至于调用哪个重载版本,就看调用的对象是否为常量,如果是常量,则调用 const 版本,否则调用非 const 版本。
class Foo {
 public:
  // 两个重载版本
  void show() const { std::cout << "const\n"; }
  void show() { std::cout << "non-const\n"; }
};
int main() {
  Foo a;
  const Foo b;
  const Foo* c = &a;
  a.show();
  b.show();
  c->show();
}
// output:
// non-const
// const
// const
#与宏定义的区别
| 宏定义 #define | 常量 const | 
|---|---|
| 宏定义,相当于字符替换 | 常量声明 | 
| 预处理器处理 | 编译器处理 | 
| 无类型安全检查 | 有类型安全检查 | 
| 不分配内存 | 要分配内存 | 
| 存储在代码段 | 存储在数据段 | 
可通过 #undef 取消 | 
不可取消 |