Horbynz hub

C/C++ 八股文:const

Word count: 3kReading time: 11 min
2022/03/20

Summary

前排提示,本来源于 C++ Primer,Section 2.4,const 的学习记录

一句话总结(看粗体即可,其他辅助理解):

  • 对常量的引用,形式是 const int &ri = ci; // ci 是 const int,细节有三点:
    • 不允许普通引用绑定常量,即 int &ri = ci; // 错误, ci 是 const int,因为 ri 是普通引用,理论上可以修改所绑定对象,但此处所绑定对象是常量,所以编译器不允许这种行为
    • 允许用任何表达式作为初始值,即 const int &ri = 10; // 正确,绑定字面量const int &ri = dval; // dval 是 double,正确,绑定其他类型的非常量,原因详见下文
    • 对常量的引用实际含义是,限定引用的操作而不限定引用对象本身,即 const int &r1 = i; r1 = 100; // i 是 int,错误,引用的修改操作不允许int &r2 = i; r2 = 100; // 正确,所引用的对象本身可以用其他方式实现修改,具体含义详见下文
  • 指向常量的指针(可对比上面对常量的引用类比理解),形式是 const int *pi = ci; // ci 是 const int,细节有四点:
    • 记忆方法是从右往左看,即第一个符号是 int * 意味着是一个指针,第二个符号是 const 意味着是常量,综合即一个指针指向常量,所以是指向常量的指针。由于底下的数据是常量,所以通过解引来修改底下数据是不允许的,但又由于指针本身是变量,可以改变指向。所以记忆方法大概是 const *,可以改变指向但不可解引
    • (与对常量的引用同)不允许普通指针指向常量,即 int *pi = ci; // 错误,ci 是 const int,原因是普通指针 pi 有修改常量的风险,编译器因此不允许
    • (与对常量的引用不同)初始值只能是相同类型的常量或非常量,即 const int *pi = 10; // 错误,指向字面量const int *pi = dval; // dval 是 const double,错误,指向其他类型
    • (与对常量的引用同)指向常量的指针实际含义是,限定指针的操作而不限定所指向的对象本身,即 const int *p1 = i; *p1 = 100; // i 是 int,错误,通过指针实现修改操作不被允许p1 = &j; // j 是 int,正确,改变指针指向允许,同时,和上面一样所指向的对象本身也可以以其他形式被改变如 int *p2 = &i; *p2 = 100; // 正确,所指向的对象本身可以用其他方式实现修改
  • 常量指针,形式是 int *const p = &i; // i 是 int,细节有三点:
    • 记忆方法也是从右往左看,即第一个符号是 const 意味着是一个常量,第二个符号是 int * 意味着是指针,综合即一个常量的指针,简称常量指针。含义是指针本身是常量,但底下的数据不关心。所以记忆起来大概是 *const,常量指针(不能修改指向)但可以解引
    • 只能初始化不能赋值,即 int *const cp = &i; // i 是 int,正确,这是初始化 但初始化后 cp = &ci; // ci 是 const int,错误,这是赋值,不允许常量指针重新赋值
  • 顶层/底层 const 看下图理解,有一点细节:
    • const int i = 100; const int *const pi = &i; 都属于底层 const
  • constexpr,有两点细节:
    • 含义是编译阶段就能确定,详见下文
    • constexpr 指针属于顶层 const,原因是 constexpr 仅对指针有效而对所指向对象无效


const 和初始化

用一个对象去初始化另一个对象,是不是 const 都没问题:

1
2
3
4
5
int i = 10;
const int ci = 20;

int j = ci; // 用 const 初始化普通变量
const cj = i; // 用普通变量初始化 const

对常量的引用(reference to const)

怎么理解这个概念?看下面两行代码:

1
2
const int ci = 1024;    // 这叫常量
const int &r1 = ci; // r 叫对常量的引用,含义是把引用绑定到常量上

但是,

1
2
3
4
int &r2 = ci;           // 这样就不行,因为 r2 可以修改所引用对象
// 但是 ci 是常量,不允许修改
// 这么写即程序员企图用 r2 修改常量
// 编译器不允许这种行为

这里还有一个地方值得一提

可能大家都喜欢将 “对常量的引用” 称为 “常量引用”,但严格来说并不对。原因是,引用不是对象,引用只是别名。而且,引用本来就是不可改变其绑定对象的,也即是引用本身就相当于常量

所以,常量引用这种说法可能更倾向于 int &r1, &r2; 这些形式,而对常量的引用则更倾向于 const int &r3 = ci; // ci 是 const int 这种形式

但是,实际上不会分得这么严格,大多时候程序员说的常量引用往往就是 const int &r = ci; 的意思。只不过,本文为了不产生歧义,统一将 const int &r = ci; 形式称为 “对常量的引用”

另外,还有一个特别重要的细节:对常量的引用实际上,只限制引用可参与的操作

可以这么理解,对常量的引用含义是将引用绑定到常量上,也即是说仅对引用可参与的操作做出限制(不允许修改引用),但对于要引用的对象是什么?是不是常量等方面不作出限定

所以,这个引用绑定的对象其实是有可能被修改的,比如:

1
2
3
4
5
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0; // 允许,通过普通引用修改所引用对象
//r2 = 0; // 不允许,禁止通过对常量的引用修改对象

初始化

只有一条法则:允许用任何表达式作为初始值,如:

允许对常量的引用绑定到非常量上

1
const int &r1 = 10;     // 正确

也允许对常量的引用绑定其他类型

1
2
3
4
5
double dval = 3.14;
const int &r2 = dval; // 也正确
// int &r3 = dval; // 错误,用一个普通引用绑定对常量的引用
// 也即程序员企图用普通引用修改某个常量
// 编译器不允许这种行为

要理解上面这个法则,首先要了解对常量的引用初始化这个过程发生过了什么:

这就是,初始化分为两步(以上面 const int &r2 = dval; 为例说明)

1
2
const int temp = dval;  // 编译器使用临时变量完成了类型转换
const int &r2 = temp;

指向常量的指针(pointer to const)

语法上和上面的对常量的引用很像,但是由于引用不是对象而指针是对象,所以总体来说还是有些内容有区别

1
2
const int &ri = i;      // 对常量的引用
const int *pi = ci; // 指向常量的指针

先来看类似的部分

首先,也是不允许普通指针指向常量

1
2
3
4
int *p = ci;            // 这样就不行,因为 p 可以修改所指向对象
// 但是 ci 是常量,不允许修改
// 这么写即程序员企图用 p 修改常量
// 编译器不允许这种行为

另外,也是一个特别重要的细节:指向常量的指针实际上,只限制指针可参与的操作

也是一样理解,对常量的引用绑定到常量上,即不允许修改所引用的对象。这里也只对指针可参与的操作作出限定(不允许指针修改对象),但对于指向的对象是不是常量不限制

所以,这个指向的对象也是有可能被修改的,比如:

1
2
3
4
5
int i = 42;
int *p1 = &i;
const int *p2 = &i;
*p1 = 0; // 允许,通过普通指针修改所指向对象
//*p2 = 0; // 不允许,禁止通过指向常量的指针修改对象

初始化

这里和对常量的引用,可以用任意表达式初始化不同。指向常量的指针只能是相同类型的,常量或非常量才可以用来做初始化

1
2
3
4
5
6
7
8
9
const int &r1 = 10;     // 对常量的引用可以使用字面量
//const int *p1 = 10; // 不可以

const double d = 3.14;
const int &r2 = d; // 对常量的引用可以使用其他类型
//const int *p2 = &d; // 不可以

const int *p3 = &i; // i 是 int,指向常量的指针允许这种初始化
const int *p4 = &ci; // ci 是 const int,也允许

const 指针(常量指针)

1
int *const cp = &ci;    // cp 是一个常量指针

字面意思,指针本身是常量,所以指针不可以改变(也即指针不能改变指向),所以常量指针必须初始化

初始化

既可以用常量对象,也可以用普通对象初始化

1
2
3
4
int i = 10;
int *const cp1 = &i; // 用普通对象初始化
const int ci = 120;
int *const cp2 = &ci; // 也可以用常量对象初始化

八股文重灾区之如何记忆

答:从右往左读

1
const int *p1;

从右往左第一个符号 int * 是一个指针,第二个符号 const 是常量,合起来就是 “一个指针” “常量”,也就是 “一个指针指向常量”(其中,中间加上 “指向” 两个字来连接)

1
int *const p2 = &ci;    // ci 是 const int

从右往左第一个符号 const 是常量,第二个符号 int * 是指针,合起来就是 “常量指针”


顶层/底层 const(top-level/low-level const)

直接上图说明指针和 const 的情况

但是,情况换成引用和 const,以下理解是错的

要注意但凡引用加上 const,只能是底层 const(形式上是 const int &ri = ci; // ci 是 const int)。因为引用这个概念本来就不允许修改,你见过重新赋值的引用吗?所以从这一点来看引用就相当于常量

但是要考虑到引用本身不是对象,它是绑定到其他对象身上作为别名而存在的概念。而指针本身就是个对象,这才有顶层的概念,引用不是对象,去哪里找这个 “顶层”?所以引用加上 const 只能是底层 const

值得一提的是,以下两种形式也属于底层 const

1
2
const int ci = 1000;
const int *const cp = &ci;

因为底层 const 突出的概念是 “底层数据不可修改”,第一个常量不可修改吧,所以算底层 const;第二个虽然看左半部分是顶层 const,但右半部分的含义也是不可修改底层数据,所以实际上也是算底层 const


constexpr

这个限定符就只有一个含义:编译阶段就能确定表达式的值

怎么理解呢?看下面两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sum1(int a, int b) {
return a + b;
}
constexpr int sum2(int a, int b) {
return a + b;
}

int main() {
// 此处打上断点,称为断点 1
int i1 = sum1(10, 20);
// 此处也打上断点,称为断点 2
constexpr int i2 = sum2(100, 20000);
return 0;
}

单步调试模式执行上面的代码,你会发现,断点 1 会跳入函数体内,但断点 2 不会跳入函数体内

这是因为,i2 的值早在编译阶段就算出来了,所以执行的时候就不需要进入函数

但要注意,constexpr 的表达式一定要是能够确定才行,如果编译阶段发现表达式确定不下来,那就变成普通函数了

constexpr 和指针

1
2
// 这是一个 constexpr 指针
constexpr int *p = nullptr;

对于指针,限定符只限定指针,而与指针所指对象无关

这是说,constexpr 只对指针可参与的操作作出限定(不允许指针修改对象),但对于指向的对象是不是常量不限制————这其实就是 const int *p; 指向常量的指针的性质

也就是说,constexpr 指针是一个顶层 const

CATALOG
  1. 1. Summary
  2. 2. const 和初始化
  3. 3. 对常量的引用(reference to const)
    1. 3.1. 初始化
  4. 4. 指向常量的指针(pointer to const)
    1. 4.1. 初始化
  5. 5. const 指针(常量指针)
    1. 5.1. 初始化
  6. 6. 八股文重灾区之如何记忆
  7. 7. 顶层/底层 const(top-level/low-level const)
  8. 8. constexpr
    1. 8.1. constexpr 和指针