章节目录

常量(命名常量)

本节阅读量:

常量简介

在编程中,常量是指在程序执行期间不能被更改的值。

C++ 支持两种不同类型的常量:

  1. 命名常量(Named constants)是与某个标识符关联的常量值。它有时也被称为符号常量(symbolic constants),或简称为常量(constants)。
  2. 字面量常量(Literal constants)是不与任何标识符关联的常量值。

本节先来介绍命名常量。


命名常量的类型

在 C++ 中定义命名常量有三种方式:

  1. 将变量声明为不可更改,即常变量(Constant variables)(在本课介绍)。
  2. 带替换文本的类对象宏(在——预处理器简介——一节中介绍,本课会再做一些补充)。
  3. 枚举常量(在后续——枚举值——一节中介绍)。

常变量(Constant variables)是最常用的一种命名常量,因此我们就从它开始讲起。


常变量(Constant variables)

到目前为止,我们所见过的所有变量的值都可以随时修改(通常通过赋值新值来实现)。例如:

1
2
3
4
5
6
7
int main()
{
    int x { 4 }; // x 不是常变量
    x = 5; // 通过赋值将 x 设置为 5 

    return 0;
}

然而,在很多情况下,定义一个值不能被更改的变量是很有用的。例如,考虑地球表面附近的重力加速度:9.8 米/秒²。这个值在短期内不会有什么变化(如果变化了,那你面对的问题可能比学习 C++ 更严重)。把它定义成常量,可以确保这个值不会被意外修改。常量还有其他好处,我们会在后面的课程中逐步介绍。

虽然这个说法听起来有点自相矛盾,但值不能更改的变量就被称为常变量。


声明常变量

要声明一个常变量,我们只需在对象类型旁边加上 const 关键字(即“const 限定符”):

1
2
const double gravity { 9.8 };  // 更推荐把 const 放在类型之前
int const sidesInSquare { 4 }; // const 放在类型之后也可以,但不推荐

虽然 const 限定符放在类型前或类型后都可以,但放在类型之前更为常见,因为这更符合英语的习惯用法——修饰词一般放在被修饰的对象之前(比如“绿色的球”而不是“球绿色的”)。


常变量必须初始化

定义常变量时必须对其进行初始化,一旦初始化之后便不能通过赋值修改其值:

1
2
3
4
5
6
7
int main()
{
    const double gravity; // error: const 变量必须初始化
    gravity = 9.9;        // error: const 变量的值不能改变

    return 0;
}

需要注意的是,常变量可以由其他变量(包括非常变量)来初始化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

int main()
{ 
    std::cout << "Enter your age: ";
    int age{};
    std::cin >> age;

    const int constAge { age }; // 用非常量的值来初始化常变量

    age = 5;      // ok: age 不是 const, 所以可以改变值
    constAge = 6; // error: constAge 是 const, 不能改变值

    return 0;
}

在上面的示例中,我们使用非常变量 age 来初始化常变量 constAge。由于 age 仍然是非 const 的,因此我们可以修改它的值。但是,由于 constAge 是 const,所以初始化之后就不能再修改它的值。


命名约定

常变量有多种不同的命名约定。

从 C 转过来的程序员通常喜欢用全大写加下划线的方式命名常变量(例如 EARTH_GRAVITY)。在 C++ 中则更常用带“k”前缀的驼峰命名(例如 kEarthGravity)。

不过,由于常变量除了不能被赋值之外,其行为与普通变量没有什么区别,所以其实并没有特别的理由一定要使用特殊的命名约定。因此,我们更倾向于使用与普通变量相同的命名约定(例如 earthGravity)。


常量函数参数

函数参数可以通过 const 关键字声明为常量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>

void printInt(const int x)
{
    std::cout << x << '\n';
}

int main()
{
    printInt(5); // 用 5 来初始化 x
    printInt(6); // 用 6 来初始化 x

    return 0;
}

注意,我们并没有给常量参数 x 显式提供初始值——调用函数时传入的实参就会作为 x 的初始值。

把函数参数设为 const 需要编译器的帮助,以确保在函数内部不会修改参数的值。然而,在现代 C++ 中,当以值传递给函数时,我们通常不会把参数声明为 const,因为我们一般并不关心函数是否修改参数的值(反正它只是一个副本,函数结束时就会被销毁)。而且 const 关键字还会在函数原型中引入一些不必要的视觉噪音。

在本系列教程的后面,我们会介绍向函数传递参数的另外两种方式:按引用传递和按地址传递。在使用这两种方式时,正确地使用 const 就非常重要了。


常量返回值

函数的返回值也可以声明为常量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

const int getValue()
{
    return 5;
}

int main()
{
    std::cout << getValue() << '\n';

    return 0;
}

对于基本类型,返回类型上的 const 限定符会被直接忽略(编译器可能会给出警告)。

对于其他类型(后面会介绍),按值返回一个 const 对象通常意义不大,因为它只是一个临时副本,无论如何都会被销毁。此外,按值返回 const 还会阻碍某些类型的编译器优化(涉及移动语义),可能导致性能下降。


带替换文本的类对象宏

在——预处理器简介——一节中,我们曾讨论过带替换文本的类对象宏。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>

#define MY_NAME "Fly"

int main()
{
    std::cout << "My name is: " << MY_NAME << '\n';

    return 0;
}

当预处理器处理包含这段代码的文件时,它会用 “Fly” 替换 MY_NAME(第 7 行)。注意 MY_NAME 是一个名字,被替换的文本是一个常量值,因此带替换文本的类对象宏也属于命名常量。


相比于预处理器宏,更推荐使用常变量

为什么不推荐使用预处理器宏来定义命名常量呢?(至少)有三个主要原因。

最大的问题在于:宏不遵循正常的 C++ 作用域规则。一旦定义了一个宏,当前文件中后续出现的所有同名标识符都会被替换。如果相同的名字在别处被使用,就会在不该替换的地方发生替换,极易引发奇怪的编译错误。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

void someFcn()
{
// 尽管 gravity 是在这个函数中定义的
// 但预处理器会替换文件剩余部分中所有出现的 gravity
#define gravity 9.8
}

void printGravity(double gravity) // 替换到这里时,会导致编译错误
{
    std::cout << "gravity: " << gravity << '\n';
}

int main()
{
    printGravity(3.71);

    return 0;
}

编译时,GCC 会给出如下这条令人困惑的错误:

1
2
3
4
prog.cc:5:17: error: expected ',' or '...' before numeric constant
    5 | #define gravity 9.8
      |                 ^~~
prog.cc:9:26: note: in expansion of macro 'gravity'

其次,宏定义相关代码的调试通常相对困难。虽然源代码中能看到宏的名称,但编译器和调试器其实从来看不到这个宏,因为它在此之前就已经被展开替换了。许多调试器都无法查看宏的值,而且对宏的支持通常也很有限。

第三,宏替换的行为与 C++ 中其他所有操作的行为都不一样,因此很容易无意中写出错误的代码。

而常变量则不存在这些问题:它们遵循正常的作用域规则,对编译器和调试器都是可见的,并且行为保持一致。


在多文件程序中使用常变量

在许多应用程序中,某个命名常量需要在整个代码库(而不仅仅是单个文件)中使用。例如那些不会改变的物理或数学常量(比如 π 或阿伏伽德罗常数),或者特定于应用的“调参”值(比如摩擦系数或重力系数)。与其在每次使用时都重新定义一遍,不如在一个特定的位置统一声明一次,然后在需要的地方直接使用。这样一来,如果需要修改,只需要改这一个地方就可以了。

在 C++ 中有多种方式可以实现这一点——我们会在后续章节——在多个文件中共享全局常量(使用内联变量)——中进行详细介绍。


类型限定符

类型限定符(有时简称为限定符)是用来修改类型行为的关键字。用于声明常变量的 const 被称为 const 类型限定符(简称 const 限定符)。

截至 C++23,C++ 只有两个类型限定符:const 和 volatile。


4.12 第4章总结

上一节

5.1 常量表达式、编译时常量和运行时常量

下一节