const 的作用

使用类型修饰符 const 说明的类型成为常类型,常类型的变量或对象的值是不能被更新的。所以在编程过程中存在保持不变的值时应当用 const 限定。


const 的基本使用

注意:因为 const 对象一旦创建后其值就不能再改变,所以 const 对象必须要初始化。

初始化 bufsize 的值:

1
2
3
const int bufSize = 512;
std::cout << "bufSize = " << bufSize;
// 输出:bufSize = 512

默认状态下,const 对象仅在文件内有效

注意:如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字。

解决 const 对象仅在文件内有效的方法:对于 const 变量不管是声明还是定义都添加 extern 关键字,这样只需定义一次就行。

1
2
3
4
5
6
7
8
9
10
11
// file1.cpp
extern const int ext = 10;

// file2.cpp
#include<iostream>
extern const int ext;
int main()
{
std::cout << ext << std::endl;
}
// 输出:12

const 的引用

“对 const 的引用”简称为“常量引用”。

把引用绑定到 const 对象上,就像绑定到其他对象上一样,称为对常量的引用。与普通引用不同,对常量的引用不能被用作修改它所绑定的对象:

1
2
3
4
5
const int ci = 1024;

const int& r1 = ci; // 正确:引用和它对应的对象都是常量
r1 = 42; // 错误:r1 是对常量的引用,r1 是不可修改的左值
int& r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象,将 "int&" 类型的引用绑定到 "const int" 类型的初始值设定项时,限定符被丢弃

注意:引用的类型必须与其所引用的对象类型一致。但是存在两种例外。

在初始化常量引用允许用任意表达式作为初始量,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面量、甚至是个一般表达式:

1
2
3
4
5
int i = 42;
const int& r1 = i; // 允许将 const int& 绑定到一个普通 int 对象上
const int& r2 = 42; // 正确:r2 是一个常量引用
const int& r3 = r1 * 2; // 正确:r3 是一个常量引用
int& r4 = r1 * 2; // 错误:r4 是一个普通的非常量引用,非常量引用的初始值必须为左值(可修改的值)

常量引用(对 const 的引用)可能引用一个并非 const 的对象

常量引用仅对引用可参与的操作做出了限定,对引用的对象本身是不做限定的。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:

1
2
3
4
5
int i = 42;
int& r1 = i; // 引用 ri 绑定对象 i
const int& r2 = i; // r2 也绑定对象 i,但是不允许通过 r2 修改 i 的值
r1 = 0; // r1 并非常量引用,i 的值修改为 0
r2 = 0; // 错误:r2 是一个常量引用

为什么常量引用可以绑定一个与其类型不一样的对象呢?

1
2
3
double i = 42.5;
const int& r1 = i; // 正确:输出 42
int& r2 = i; // 错误:无法用 "double" 类型的值初始化 "int&" 类型的引用(非常量限定)

因为要保证 r1 能够绑定一个整型,会先把 i 赋值给一个整型的临时量,再让 r1 绑定这个临时量。由于 r1 绑定的是临时量而非 i,这就让引用失去了它原本的含义,所以 C++ 也把这种行为归为非法。如果是常量引用,这个引用无法被修改,自然就不会破坏引用的意义。

拓展:

  • 左值是可寻址的变量;
  • 右值一般是不可寻址的常量;
  • 左值一般可以被修改,而右值不能。

指针和 const

与 const 相关的指针:

  1. const int * p 或者 int const * p:指向 const 对象的指针或者说是指向常量的指针。
  2. int * const p:常量指针、const 指针。
  3. const int * const p:指向常量对象的常量指针。

从上面我们可以看出,当 * 比 const 离变量名更近时说明它先是一个指针,称为指向常量的指针;当 const 比 * 离变量名更近时说明它先是一个常量,称为常量指针。

指向常量的指针

指向常量的指针不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。

1
2
3
4
5
const double PI = 3.14; // PI 是一个常量,它的值不能改变
double* ptr = &PI; // 错误:ptr 是一个普通指针,"const double *" 类型的值不能用于初始化 "double *" 类型的实体
const double* p = 10.0; // 错误:"double" 类型的值不能用于初始化 "const double *" 类型的实体
const double* cptr = &PI; // 正确:cptr 可以指向一个双精度常量
*cptr = 42; // 错误:不能给 *cptr 赋值,表达式必须是可修改的左值

引用一样,指针的类型必须与其所引用的对象类型一致,但是允许一个指向常量的指针指向一个非常量对象

1
2
3
4
const double PI = 3.14; // PI 是一个常量,它的值不能改变
double dval = 3.14; // dval 是一个双精度浮点数,它的值可以改变
const double* cptr = &PI; // 正确:cptr 可以指向一个双精度常量
cptr = &dval; // 正确:但是不能通过 cptr 改变 dval 的值

注意:虽然不能给 *cptr 赋值,但是我们可以让 cptr 重新指向新的双精度常量。

常量引用一样,所谓指向常量的指针仅仅只是不允许通过该指针去修改对象的值,并没有限制用其他途径去修改对象的值。

const 指针

指针是对象而引用不是,因此允许把指针本身定义为常量常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把 * 放在 const 关键字之前说明指针是一个常量,即不变的是指针本身,而非指针指向的值。

1
2
3
4
int errNum = 0;
int* const curErr = &errNum; // curErr 将一直指向 errNum
const double PI = 3.14;
const double* const pip = &PI; // pip 是一个指向常量对象的常量指针

注意:常量指针并不意味着不能用指针修改所指向对象的值。

如果我们用常量指针去指向一个常量,也会出现错误,因为常量指针是可以修改指向对象的值的:

1
2
const int num = 0;
int* const p = &num; // 错误:"const int*" 类型的值不能用于初始化 "int *const" 类型的实体

所以我们可以总结出:

  1. 如果想要用指针指向常量,我们应该用指向常量的指针
  2. 如果只想无法通过指针去修改指向对象的值,我们应该用常量指针,前提是指向的对象是一个非常量。

顶层 const 和底层 const

top-level 顶层 const: 指针本身是个常量(常量指针)。

low-level 底层 const: 指针所指向的对象是个常量(指向常量的指针)。

引用就是一个常量指针,不能更改绑定的对象地址;若是常量引用(即 const& ),能绑定常量,并且不能用常量引用修改对象的值,但是如果对象不是常量,则可以用其他方式可以修改。

1
2
3
4
5
6
int i = 0;
int* const p1 = &i; // 不能改变 p1 的值,只是一个顶层 const(常量指针)
const int c1 = 42; // 不能改变 c1 的值,只是一个顶层 const(常量)
const int* p2 = &c1; // 允许改变 p2 的值,只是一个低层 const(指向常量的指针)
const int* const p3 = p2; // 靠右的 const 是顶层 const,靠左的是底层 const(指向常量对象的常量指针)
const int& r = c1; // 用于声明引用的 const 都是底层 const(常量引用)

在拷贝对象时,顶层 const底层 const 的限制有所不同:

  1. 顶层 const 对拷贝对象时,无影响;因为拷贝对象时,并不会修改被拷贝对象的值,所以无论是拷入还是拷出的对象是否为常量都没有什么影响。
  2. 底层 const 对拷贝对象时,有影响;拷入和拷出必须具有相同的底层 const 资格,或者两个对象的数据类型必须能转换。一般来说,非常量可以转换成常量,反之不行。
1
2
3
4
5
6
7
8
9
10
11
12
13
int i = 0;
const int c1 = 42; // 不能改变 c1 的值,只是一个顶层 const(常量)
const int* p2 = &c1; // 允许改变 p2 的值,只是一个低层 const(指向常量的指针)
const int* const p3 = p2; // 靠右的 const 是顶层 const,靠左的是底层 const(指向常量对象的常量指针)
// 顶层 const
i = c1; // 正确:拷贝 c1 的值,c1 是一个顶层 const,对此无影响
p2 = p3; // 正确:p2 和 p3 所指向的对象类型相同,p3 顶层 const 的部分不受影响
// 底层 const
int* p = p3; // 错误:p3 包含底层 const 的定义,而 p 没有
p2 = p3; // 正确:p2 和 p3 都是底层 const
p2 = &i; // 正确:int* 能转换成 const int*
int& r = c1; // 错误:普通的 int& 不能绑定到 int 常量上
const int& r2 = i; // 正确:const int& 可以绑定到一个普通的 int 上

总之就是要考虑在拷贝后会不会影响原来对象的原有限制。


总结

  1. 在编程过程中存在保持不变的值时应当用 const 限定。
  2. 因为 const 对象一旦创建后其值就不能再改变,所以 const 对象必须要初始化
  3. 如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字。
  4. 常量引用绑定对象,无法通过常量引用修改对象的值;但如果对象是个非常量的话,可以通过其他途径修改对象的值。
  5. 在初始化常量引用时允许用任意表达式作为初始量,只要该表达式的结果能转换成引用的类型即可。
  6. const 和指针:
    • const char * p 或者 char const * p:指向常量的指针,不允许通过该指针去修改对象的值。
    • char * const p:常量指针,初始化后存放在指针中的那个地址不能再改变。
    • 要想存放常量对象的地址,只能使用指向常量的指针,允许一个指向常量的指针指向一个非常量对象
    • 虽然不能给指向常量的指针赋值,但是我们可以让指向常量的指针重新指向新的双精度常量。
    • 常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。
    • 常量指针并不意味着不能用指针修改所指向对象的值。
    • 如果想要用指针指向常量,我们应该用指向常量的指针
    • 如果只想无法通过指针去修改指向对象的值,我们应该用常量指针,前提是指向的对象是一个非常量。
  7. top-level 顶层 const:指针本身是个常量(常量指针)。
  8. low-level 底层 const: 指针所指向的对象是个常量(指向常量的指针)。
  9. 在拷贝对象时,顶层 const 无影响,而底层 const 有影响;拷入和拷出必须具有相同的底层 const 资格,或者两个对象的数据类型必须能转换。一般来说,非常量可以转换成常量,反之不行。


📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!

🔗参考文献:

📖 C++ Primer中文版 第5版 [(美)李普曼,(美)拉乔伊,(美)默著][电子工业出版社][2013.08][838页]