常量(命名常量)
本节阅读量:常量简介
在编程中,常量是指在程序执行期间不能被更改的值。
C++ 支持两种不同类型的常量:
- 命名常量(Named constants)是与某个标识符关联的常量值。它有时也被称为符号常量(symbolic constants),或简称为常量(constants)。
- 字面量常量(Literal constants)是不与任何标识符关联的常量值。
本节先来介绍命名常量。
命名常量的类型
在 C++ 中定义命名常量有三种方式:
- 将变量声明为不可更改,即常变量(Constant variables)(在本课介绍)。
- 带替换文本的类对象宏(在——预处理器简介——一节中介绍,本课会再做一些补充)。
- 枚举常量(在后续——枚举值——一节中介绍)。
常变量(Constant variables)是最常用的一种命名常量,因此我们就从它开始讲起。
常变量(Constant variables)
到目前为止,我们所见过的所有变量的值都可以随时修改(通常通过赋值新值来实现)。例如:
|
|
然而,在很多情况下,定义一个值不能被更改的变量是很有用的。例如,考虑地球表面附近的重力加速度:9.8 米/秒²。这个值在短期内不会有什么变化(如果变化了,那你面对的问题可能比学习 C++ 更严重)。把它定义成常量,可以确保这个值不会被意外修改。常量还有其他好处,我们会在后面的课程中逐步介绍。
虽然这个说法听起来有点自相矛盾,但值不能更改的变量就被称为常变量。
声明常变量
要声明一个常变量,我们只需在对象类型旁边加上 const 关键字(即“const 限定符”):
|
|
虽然 const 限定符放在类型前或类型后都可以,但放在类型之前更为常见,因为这更符合英语的习惯用法——修饰词一般放在被修饰的对象之前(比如“绿色的球”而不是“球绿色的”)。
题外话
由于编译器解析复杂声明的方式,一些开发者更喜欢将 const 放在类型之后(因为这样略微更一致)。这种风格被称为“east const”。虽然它有一些支持者(并且提出了不少合理的观点),但并没有得到广泛流行。
最佳实践
将 const 放在类型之前(因为这更符合传统写法)。
常变量必须初始化
定义常变量时必须对其进行初始化,一旦初始化之后便不能通过赋值修改其值:
|
|
需要注意的是,常变量可以由其他变量(包括非常变量)来初始化:
|
|
在上面的示例中,我们使用非常变量 age 来初始化常变量 constAge。由于 age 仍然是非 const 的,因此我们可以修改它的值。但是,由于 constAge 是 const,所以初始化之后就不能再修改它的值。
命名约定
常变量有多种不同的命名约定。
从 C 转过来的程序员通常喜欢用全大写加下划线的方式命名常变量(例如 EARTH_GRAVITY)。在 C++ 中则更常用带“k”前缀的驼峰命名(例如 kEarthGravity)。
不过,由于常变量除了不能被赋值之外,其行为与普通变量没有什么区别,所以其实并没有特别的理由一定要使用特殊的命名约定。因此,我们更倾向于使用与普通变量相同的命名约定(例如 earthGravity)。
常量函数参数
函数参数可以通过 const 关键字声明为常量:
|
|
注意,我们并没有给常量参数 x 显式提供初始值——调用函数时传入的实参就会作为 x 的初始值。
把函数参数设为 const 需要编译器的帮助,以确保在函数内部不会修改参数的值。然而,在现代 C++ 中,当以值传递给函数时,我们通常不会把参数声明为 const,因为我们一般并不关心函数是否修改参数的值(反正它只是一个副本,函数结束时就会被销毁)。而且 const 关键字还会在函数原型中引入一些不必要的视觉噪音。
在本系列教程的后面,我们会介绍向函数传递参数的另外两种方式:按引用传递和按地址传递。在使用这两种方式时,正确地使用 const 就非常重要了。
最佳实践
按值传递参数时不要使用 const。
常量返回值
函数的返回值也可以声明为常量:
|
|
对于基本类型,返回类型上的 const 限定符会被直接忽略(编译器可能会给出警告)。
对于其他类型(后面会介绍),按值返回一个 const 对象通常意义不大,因为它只是一个临时副本,无论如何都会被销毁。此外,按值返回 const 还会阻碍某些类型的编译器优化(涉及移动语义),可能导致性能下降。
最佳实践
按值返回时不要使用 const。
带替换文本的类对象宏
在——预处理器简介——一节中,我们曾讨论过带替换文本的类对象宏。例如:
|
|
当预处理器处理包含这段代码的文件时,它会用 “Fly” 替换 MY_NAME(第 7 行)。注意 MY_NAME 是一个名字,被替换的文本是一个常量值,因此带替换文本的类对象宏也属于命名常量。
相比于预处理器宏,更推荐使用常变量
为什么不推荐使用预处理器宏来定义命名常量呢?(至少)有三个主要原因。
最大的问题在于:宏不遵循正常的 C++ 作用域规则。一旦定义了一个宏,当前文件中后续出现的所有同名标识符都会被替换。如果相同的名字在别处被使用,就会在不该替换的地方发生替换,极易引发奇怪的编译错误。例如:
|
|
编译时,GCC 会给出如下这条令人困惑的错误:
|
|
其次,宏定义相关代码的调试通常相对困难。虽然源代码中能看到宏的名称,但编译器和调试器其实从来看不到这个宏,因为它在此之前就已经被展开替换了。许多调试器都无法查看宏的值,而且对宏的支持通常也很有限。
第三,宏替换的行为与 C++ 中其他所有操作的行为都不一样,因此很容易无意中写出错误的代码。
而常变量则不存在这些问题:它们遵循正常的作用域规则,对编译器和调试器都是可见的,并且行为保持一致。
最佳实践
定义常量时,不推荐使用类对象宏。
在多文件程序中使用常变量
在许多应用程序中,某个命名常量需要在整个代码库(而不仅仅是单个文件)中使用。例如那些不会改变的物理或数学常量(比如 π 或阿伏伽德罗常数),或者特定于应用的“调参”值(比如摩擦系数或重力系数)。与其在每次使用时都重新定义一遍,不如在一个特定的位置统一声明一次,然后在需要的地方直接使用。这样一来,如果需要修改,只需要改这一个地方就可以了。
在 C++ 中有多种方式可以实现这一点——我们会在后续章节——在多个文件中共享全局常量(使用内联变量)——中进行详细介绍。
类型限定符
类型限定符(有时简称为限定符)是用来修改类型行为的关键字。用于声明常变量的 const 被称为 const 类型限定符(简称 const 限定符)。
截至 C++23,C++ 只有两个类型限定符:const 和 volatile。
可选阅读
volatile 限定符用于告诉编译器对象的值可能随时发生变化。这个很少使用的限定符会禁用某些类型的优化。
在技术文档中,const 和 volatile 限定符通常被合称为 cv 限定符。C++ 标准中也使用了下列术语:
- 非 cv 限定类型(cv-unqualified):没有任何类型限定符的类型(例如 int)。
- cv 限定类型(cv-qualified):带有一个或多个类型限定符的类型(例如 const int)。
- 可能 cv 限定类型(possibly cv-qualified):可以是 cv 限定也可以不是 cv 限定的类型。
这些术语在技术文档之外几乎不会用到,这里列出只是为了参考,不需要刻意去记。