5-1 常量变量(命名常量)

常量介绍

在编程中,常量constant是指程序执行过程中不可改变的值。

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

  • 命名常量Named constants是与标识符关联的常量值,有时也称为符号常量symbolic constants
  • 字面常量Literal constants是不与标识符关联的常量值。

我们将从命名常量开始讲解常量,随后在后续课程5.2(字面量)中介绍字面常量。


命名常量的类型

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

常量变量是最常见的命名常量类型,因此我们将从这里开始讲解。


常量变量

迄今为止,我们所见的所有变量都是非常量变量——也就是说,它们的值可以在任何时候被改变(通常通过赋予新值)。例如:

int main()
{
    int x { 4 }; // x is a non-constant variable
    x = 5; // change value of x to 5 using assignment operator

    return 0;
}

然而,在许多情况下,定义不可更改值的变量非常有用。例如,考虑地球重力(近地表处):9.8米/秒²。这个数值短期内不太可能改变(如果真改变了,你面临的麻烦恐怕远不止学习C++那么简单)。将此值定义为常量有助于确保该值不会被意外修改。常量还具有其他优势,我们将在后续课程中探讨。

尽管这看似矛盾,但初始化后值不可更改的变量仍被称为常量变量constant variable


声明常量变量

要声明常量变量,需在对象类型旁添加 const 关键字(称为“const 修饰符qualifier”):

const double gravity { 9.8 };  // preferred use of const before type
int const sidesInSquare { 4 }; // "east const" style, okay but not preferred

尽管C++允许将const限定符置于类型前后,但更常见的做法是将其置于类型之前,因为这更符合标准英语语法惯例——修饰语应置于被修饰对象之前(例如“a green ball”而非“a ball green”)。

顺带一提……
由于编译器解析更复杂声明的方式,部分开发者更倾向于将 const 置于类型之后(因其略显一致)。这种风格被称为“east const”。尽管该风格拥有支持者(且存在合理依据),但并未广泛流行。

最佳实践
将 const 置于类型之前(因为这样更符合惯例)。

关键洞察
对象的类型包含const限定符,因此当我们定义const double gravity { 9.8 };时,gravity的类型即为const double。


常量变量必须初始化

常量变量必须在定义时初始化,且该值不能通过赋值改变:

int main()
{
    const double gravity; // error: const variables must be initialized
    gravity = 9.9;        // error: const variables can not be changed

    return 0;
}

请注意,const 变量可以从其他变量(包括非 const 变量)进行初始化:

#include <iostream>

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

    const int constAge { age }; // initialize const variable using non-const value

    age = 5;      // ok: age is non-const, so we can change its value
    constAge = 6; // error: constAge is const, so we cannot change its value

    return 0;
}

image

在上例中,我们使用非const变量age初始化const变量constAge。由于age仍是非const变量,其值可被修改。但constAge作为const变量,初始化后其值便不可更改。

关键要点
常量变量的初始化表达式可以是非常量值。


常量变量的命名

常量变量存在多种不同的命名规范。

从C语言转型的程序员通常更倾向于使用下划线加大写字母的命名方式(例如EARTH_GRAVITY)。而在C++中更常见的是使用首字母大写且带'k'前缀的命名(例如kEarthGravity)。

然而,由于常量变量本质上与普通变量行为一致(仅不能被赋值),其实并无必要采用特殊命名规则。因此我们建议沿用非常量变量的命名规范(如earthGravity)。


常量函数形参

函数形参可通过 const 关键字定义为常量:

#include <iostream>

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

int main()
{
    printInt(5); // 5 will be used as the initializer for x
    printInt(6); // 6 will be used as the initializer for x

    return 0;
}

请注意,我们并未为常量形参 x 显式提供初始化器——函数调用中实参的值将被用作 x 的初始化值。

将函数形参设为常量可借助编译器确保形参值在函数内部不变。但在现代C++中,我们通常不将值参数设为const,因为函数修改形参值的行为通常无关紧要(毕竟参数只是副本,函数结束时会被销毁)。const关键字还会给函数原型添加些许冗余。

最佳实践
值形参不应使用 const。

本教程后续将介绍另外两种函数实参传递方式:按引用传递和按地址传递。采用这两种方式时,const 的正确使用至关重要。


常量返回值

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

#include <iostream>

const int getValue()
{
    return 5;
}

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

    return 0;
}

image

(需要暂时取消"将警告视为错误", 只需要取消-Werror就可以了,这里偷懒采用clang++ main.cpp -o main)
image

对于基本类型,返回类型的 const 修饰符会被直接忽略(编译器可能会生成警告)。

对于其他类型(我们稍后会讨论),按值返回 const 对象通常意义不大,因为这些对象只是临时副本,最终仍会被销毁。返回 const 值还会阻碍某些编译器优化(涉及移动语义),导致性能下降。

最佳实践
不要在按值返回时使用 const。


为何应将变量设为常量

若变量可被设为常量,通常应将其设为常量。此举至关重要,原因如下:

  • 降低错误概率。通过将变量设为常量,可确保其值不会被意外修改。
  • 为编译器提供更多优化空间。当编译器能确定变量值不变时,可运用更多优化技术,从而生成更小更快的编译程序。本章后续将对此展开讨论。
  • 最重要的是,它能降低程序的整体复杂度。当需要分析代码段功能或调试问题时,我们明确知道常量变量的值不会改变,因此无需担心其值是否实际发生变化、变化为何值以及新值是否正确。

关键洞察
系统中的每个活动部件都会增加复杂性,并提高缺陷或故障的风险。非恒定变量属于活动部件,而恒定变量则不属于。

最佳实践
尽可能将变量设为常量。例外情况包括按值传递的函数参数和按值返回的类型,这些通常不应设为常量。


带替换文本的对象式宏

第2.10节——预处理器介绍中,我们讨论了带替换文本的对象式宏。例如:

#include <iostream>

#define MY_NAME "Alex"

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

    return 0;
}

image

当预处理器处理包含此代码的文件时,它将把第7行中的MY_NAME替换为“Alex”。请注意,MY_NAME是一个名称,而替换文本是一个常量值,因此具有替换文本的对象类宏也是命名的常量。


优先使用常量变量而非预处理器宏

那么为何不使用预处理器宏来定义命名常量?主要存在(至少)三个问题。

最大的问题在于宏不遵循常规的C++作用域规则。一旦宏被#define定义,当前文件中该宏名称的所有后续出现位置都会被替换。若该名称在其他位置被使用,就会在非预期处发生宏替换,极易导致奇怪的编译错误。例如:

#include <iostream>

void someFcn()
{
// Even though gravity is defined inside this function
// the preprocessor will replace all subsequent occurrences of gravity in the rest of the file
#define gravity 9.8
}

void printGravity(double gravity) // including this one, causing a compilation error
{
    std::cout << "gravity: " << gravity << '\n';
}

int main()
{
    printGravity(3.71);

    return 0;
}

编译时,GCC 产生了这个令人困惑的错误:

prog.cc:7:17: error: expected ',' or '...' before numeric constant
    5 | #define gravity 9.8
      |                 ^~~
prog.cc:10:26: note: in expansion of macro 'gravity'

(clang编译的结果如下:)
image

其次,使用宏调试代码往往更困难。尽管源代码中会保留宏名称,但编译器和调试器永远无法看到宏本身——因为它们在运行前已被替换。许多调试器无法检查宏的值,且在处理宏时常受限于功能。

第三,宏替换的行为与C++中其他元素截然不同,这容易导致无意中的错误。

常量变量则完全不存在这些问题:它们遵循常规作用域规则,可被编译器和调试器识别,且行为始终如一。

最佳实践
优先使用常量变量,而非包含替换文本的对象式宏。


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

在许多应用程序中,某个命名的常量需要在整个代码中使用(而不仅限于单个文件)。这些常量可能包括物理或数学中不变的常数(如π或阿伏伽德罗常数),或是应用程序特有的“调优”参数(如摩擦系数或重力系数)。与其每次使用时重新定义,不如在中心位置声明一次,并在需要时调用。这样,若需修改常量,仅需在单一位置进行变更。

C++中有多种实现方式——我们在第7.10节---跨文件共享全局常量(使用内联变量)中对此进行了详尽阐述。


命名法:类型限定符

类型限定符type qualifier(有时简称为限定符qualifier)是一种应用于类型的关键字,用于修改该类型的行为。用于声明常量变量的 const 被称为 const 类型限定符const type qualifier(或简称为 const 限定符const qualifier)。

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

可选阅读

volatile修饰符用于告知编译器某个对象的值可能随时发生变化。这种罕用的修饰符会禁用某些类型的优化。

在技术文档中,const和volatile修饰符常被统称为cv修饰符cv-qualifiers。C++标准中还使用以下术语:

  • cv未限定类型cv-unqualified是指不带任何类型修饰符的类型(例如int)。
  • cv限定类型cv-qualified是指应用了一个或多个类型限定符的类型(例如 const int)。
  • 可能带cv限定类型possibly cv-qualified是指可能为cv未限定类型或cv限定类型。

这些术语在技术文档之外使用较少,此处仅供参考,无需刻意记忆。
但至少现在你能理解JF Bastien的这个笑话

问:如何判断C++开发者是否合格?
答:看看他们的CV。(备注:编程双关语,可指Curriculum Vitae简历,也可指代cv-qualifiers, 而作C++最基本的限定符,不知道这个的话就意味着不合格)

image

posted @ 2026-02-15 05:23  游翔  阅读(2)  评论(0)    收藏  举报