章节目录

字面值常量

本节阅读量:

字面值是指直接写在代码中的值。例如:

1
2
3
4
return 5;                     // 5 是整数字面值
bool myNameIsAlex { true };   // true 是 bool 字面值
double d { 3.4 };             // 3.4 是 double 字面值
std::cout << "Hello, world!"; // "Hello, world!" 是 C 语言风格的字符串字面值

字面值有时也被称为字面值常量,因为它们的值无法被重新定义(5 永远都表示整数值 5)。


字面值的类型

就像对象有类型一样,所有字面值也都有类型。字面值的类型是从字面值本身的值推导出来的。例如,一个整数(如 5)的字面值会被推断为 int 类型。

在默认情况下:

字面值 样例 类型
整数 5, 0, -3 int
bool true, false bool
浮点数 1.2, 0.0, 3.4 double (而非 float!)
字符 ‘a’, ‘\n’ char
C 语言风格的字符串 “Hello, world!” const char[14]

字面值后缀

如果字面值的默认类型无法满足需求,可以通过添加后缀来更改其类型:

类别 后缀 类型
integral u or U unsigned int
integral l or L long
integral ul, uL, Ul, UL, lu, lU, Lu, LU unsigned long
integral ll or LL long long
integral ull, uLL, Ull, ULL, llu, llU, LLu, LLU unsigned long long
integral z or Z The signed version of std::size_t (C++23)
integral uz, uZ, Uz, UZ, zu, zU, Zu, ZU std::size_t (C++23)
floating point f or F float
floating point l or L long double
string s std::string
string sv std::string_view

大多数后缀是不区分大小写的。由于小写字母 L 在某些字体中看起来很像数字 1,因此一些开发者更偏好使用大写形式的后缀字面值。


整型字面值

整型字面值通常不需要加后缀,但以下是一些示例:

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

int main()
{
    std::cout << 5 << '\n';  // 5 (无后缀) 是 int 类型 (默认情况)
    std::cout << 5L << '\n'; // 5L 是 long 类型
    std::cout << 5u << '\n'; // 5u 是 unsigned int 类型

    return 0;
}

在大多数情况下,即使初始化的是非 int 的整型变量,也可以直接使用不带后缀的 int 字面值:

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

int main()
{
    int a { 5 };          // ok: 类型匹配
    unsigned int b { 6 }; // ok: 编译器会将数字转换为 unsigned int
    long c { 7 };         // ok: 编译器会将数字转换为 long

    return 0;
}

在这种情况下,编译器会把 int 转换为合适的类型。


浮点字面值

默认情况下,浮点字面值的类型是 double。要让它成为 float 字面值,应加上 F(或 f)后缀:

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

int main()
{
    std::cout << 5.0 << '\n';  // 5.0 (无后缀) 是 double 类型 (默认情况)
    std::cout << 5.0f << '\n'; // 5.0f 是 float 类型

    return 0;
}

新手程序员常常对下面这条编译器警告感到困惑:

1
float f { 4.1 }; // warning: 4.1 is a double literal, not a float literal

这是因为 4.1 没有后缀,所以字面值的类型是 double,而不是 float。当编译器判断字面值的类型时,它并不关心你后续会如何使用它(比如这里用它来初始化一个 float 变量)。由于字面值(double)的类型与要初始化的变量(float)的类型不一致,所以字面值必须先被转换为 float,然后才能用来初始化变量 f。把值从 double 转换为 float 可能会损失精度,因此编译器会发出警告。

解决办法是采用以下方式之一:

1
2
float f { 4.1f }; // 使用 'f' 后缀让字面值是 float 类型
double d { 4.1 }; // 将变量类型改为 double

浮点字面值的科学计数法

声明浮点字面值有两种不同的方式:

1
2
double pi { 3.14159 }; // 3.14159 是标准写法
double avogadro { 6.02e23 }; // 6.02 x 10^23 是科学计数法写法

在第二种形式中,指数后面的数字也可以是负数:

1
double electronCharge { 1.6e-19 }; // 电子的电荷量为 1.6 x 10^-19

字符串字面值

在编程中,字符串是由一系列连续的字符组成的集合,用来表示文本(如名字、单词和句子)。

你写的第一个 C++ 程序可能就长这样:

1
2
3
4
5
6
7
#include <iostream>
 
int main()
{
    std::cout << "Hello, world!";
    return 0;
}

“Hello, world!” 就是一个字符串字面值。字符串字面值使用双引号括起来,以便将其识别为字符串(而字符字面值则使用单引号括起来)。

由于程序中常常需要使用字符串,因此大多数现代编程语言都内置了基本的字符串数据类型。但由于历史原因,字符串在 C++ 中并不是一个基本类型。相反,它们是一种奇怪、复杂、难以使用的类型(我们会在后面的课程中详细介绍)。这样的字符串通常被称为 C 字符串或 C 风格字符串,因为它们继承自 C 语言。

关于 C 风格字符串字面值,有两点值得了解:

  1. 所有的 C 字符串都有一个隐式的 null 结尾符。例如 “hello” 看起来只有 5 个字符,但实际上是 6 个:‘h’、’e’、’l’、’l’、‘o’ 和 ‘\0’(ASCII 码 0)。结尾的 ‘\0’ 是一个特殊字符,也就是我们所说的 null 结尾符,用来标记字符串的结束位置。

  2. 与大多数其他字面值(它们只是值,而非对象)不同,C 字符串是一个 const 对象,在程序开始执行之前就存在,并一直存在到程序结束。这一点在介绍 std::string_view 时会详细讨论。

与 C 风格字符串字面值不同,std::string 和 std::string_view 字面值会创建临时对象。这些临时对象在创建之后立即被使用,并会在创建它们的完整表达式结束时被销毁。


魔数(Magic numbers)

魔数是指那些单看其字面值含义不明确、并且未来可能需要修改的字面值(通常是数字)。

下面是两条包含魔数的示例语句:

1
2
constexpr int maxStudentsPerSchool{ numClassrooms * 30 };
setMax(30);

在这些上下文中,字面值 30 究竟是什么意思?在前一种情况下,你可能会猜它是每个班级的学生人数,但这并不显而易见。而在后者中,谁也说不清。我们只能去查看该函数的定义,才能知道它的作用。

在复杂的程序中,如果没有注释加以说明,就很难推断某个字面值到底代表什么。

使用魔数通常被认为是一种不好的做法,因为单看其值就难以判断它的用途,并且如果这个值需要被修改时还会带来问题。假设学校购置了新课桌,将班级规模从 30 人扩大到 35 人,程序就需要相应地进行修改。

为此,我们需要把一个或多个 30 更新为 35。但究竟是哪些字面值呢?maxStudentsPerSchool 初始值中的 30 看起来很明确。但作为 setMax() 参数的 30 呢?它是否与前一个 30 表达相同的含义?如果是,那就应该一起修改;如果不是,那就应该单独处理,否则可能会破坏程序的逻辑。如果进行全局搜索替换,就可能会在 setMax() 的参数不应被修改时意外地把它也改掉。因此,你必须检查代码中所有出现字面值 30 的地方(可能有几百处),再逐一确认是否需要修改。这会非常耗时(也容易出错)。

幸运的是,使用命名常量可以很容易地解决上下文不清晰和修改相关的问题:

1
2
3
4
5
constexpr int maxStudentsPerClass { 30 };
constexpr int totalStudents{ numClassrooms * maxStudentsPerClass }; // 现在 30 的含义就很明确了

constexpr int maxNameLength{ 30 };
setMax(maxNameLength); // 这里的 30 显然是另一个含义

常量的名字本身就提供了上下文,而且我们只需要在一处修改它的值,就可以让整个程序中的值同步更新。

需要注意的是,魔数不一定总是数字——它们也可以是其他类型的字面值(例如字符串)。

在那些明显不太可能被修改的上下文中使用的字面值,通常不被视为魔数。-1、0、0.0 和 1 这些值经常会出现在下面这样的场景中:

1
2
int idGenerator { 0 };         // fine: 我们从 0 开始生成 id
idGenerator = idGenerator + 1; // fine: id + 1

其他一些数字在其上下文中也可能含义非常明显(因此也不被视为魔数):

1
2
3
4
int kmtoM(int km)
{
    return km * 1000; // fine: 显然 1000 是一个转换系数
}

5.2 Constexpr

上一节

5.4 数字系统(十进制、二进制、十六进制和八进制)

下一节