字面值常量
本节阅读量:字面值是指直接写在代码中的值。例如:
|
|
字面值有时也被称为字面值常量,因为它们的值无法被重新定义(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,因此一些开发者更偏好使用大写形式的后缀字面值。
最佳实践
优先使用大写 L 作为字面值后缀,而不是小写 l。
整型字面值
整型字面值通常不需要加后缀,但以下是一些示例:
|
|
在大多数情况下,即使初始化的是非 int 的整型变量,也可以直接使用不带后缀的 int 字面值:
|
|
在这种情况下,编译器会把 int 转换为合适的类型。
浮点字面值
默认情况下,浮点字面值的类型是 double。要让它成为 float 字面值,应加上 F(或 f)后缀:
|
|
新手程序员常常对下面这条编译器警告感到困惑:
|
|
这是因为 4.1 没有后缀,所以字面值的类型是 double,而不是 float。当编译器判断字面值的类型时,它并不关心你后续会如何使用它(比如这里用它来初始化一个 float 变量)。由于字面值(double)的类型与要初始化的变量(float)的类型不一致,所以字面值必须先被转换为 float,然后才能用来初始化变量 f。把值从 double 转换为 float 可能会损失精度,因此编译器会发出警告。
解决办法是采用以下方式之一:
|
|
浮点字面值的科学计数法
声明浮点字面值有两种不同的方式:
|
|
在第二种形式中,指数后面的数字也可以是负数:
|
|
字符串字面值
在编程中,字符串是由一系列连续的字符组成的集合,用来表示文本(如名字、单词和句子)。
你写的第一个 C++ 程序可能就长这样:
|
|
“Hello, world!” 就是一个字符串字面值。字符串字面值使用双引号括起来,以便将其识别为字符串(而字符字面值则使用单引号括起来)。
由于程序中常常需要使用字符串,因此大多数现代编程语言都内置了基本的字符串数据类型。但由于历史原因,字符串在 C++ 中并不是一个基本类型。相反,它们是一种奇怪、复杂、难以使用的类型(我们会在后面的课程中详细介绍)。这样的字符串通常被称为 C 字符串或 C 风格字符串,因为它们继承自 C 语言。
关于 C 风格字符串字面值,有两点值得了解:
-
所有的 C 字符串都有一个隐式的 null 结尾符。例如 “hello” 看起来只有 5 个字符,但实际上是 6 个:‘h’、’e’、’l’、’l’、‘o’ 和 ‘\0’(ASCII 码 0)。结尾的 ‘\0’ 是一个特殊字符,也就是我们所说的 null 结尾符,用来标记字符串的结束位置。
-
与大多数其他字面值(它们只是值,而非对象)不同,C 字符串是一个 const 对象,在程序开始执行之前就存在,并一直存在到程序结束。这一点在介绍 std::string_view 时会详细讨论。
与 C 风格字符串字面值不同,std::string 和 std::string_view 字面值会创建临时对象。这些临时对象在创建之后立即被使用,并会在创建它们的完整表达式结束时被销毁。
进阶读者
这就是为什么字符串 “Hello, world!” 的类型是 const char[14] 而不是 const char[13]——隐藏的 null 结尾符也算作一个字符。
关键点
C 风格字符串字面值是在程序启动时创建的常量对象,并保证在整个程序运行期间都存在。
相关内容
我们在——std::string 简介——和——std::string_view 简介——中分别介绍了 std::string 和 std::string_view 字面值。
魔数(Magic numbers)
魔数是指那些单看其字面值含义不明确、并且未来可能需要修改的字面值(通常是数字)。
下面是两条包含魔数的示例语句:
|
|
在这些上下文中,字面值 30 究竟是什么意思?在前一种情况下,你可能会猜它是每个班级的学生人数,但这并不显而易见。而在后者中,谁也说不清。我们只能去查看该函数的定义,才能知道它的作用。
在复杂的程序中,如果没有注释加以说明,就很难推断某个字面值到底代表什么。
使用魔数通常被认为是一种不好的做法,因为单看其值就难以判断它的用途,并且如果这个值需要被修改时还会带来问题。假设学校购置了新课桌,将班级规模从 30 人扩大到 35 人,程序就需要相应地进行修改。
为此,我们需要把一个或多个 30 更新为 35。但究竟是哪些字面值呢?maxStudentsPerSchool 初始值中的 30 看起来很明确。但作为 setMax() 参数的 30 呢?它是否与前一个 30 表达相同的含义?如果是,那就应该一起修改;如果不是,那就应该单独处理,否则可能会破坏程序的逻辑。如果进行全局搜索替换,就可能会在 setMax() 的参数不应被修改时意外地把它也改掉。因此,你必须检查代码中所有出现字面值 30 的地方(可能有几百处),再逐一确认是否需要修改。这会非常耗时(也容易出错)。
幸运的是,使用命名常量可以很容易地解决上下文不清晰和修改相关的问题:
|
|
常量的名字本身就提供了上下文,而且我们只需要在一处修改它的值,就可以让整个程序中的值同步更新。
需要注意的是,魔数不一定总是数字——它们也可以是其他类型的字面值(例如字符串)。
在那些明显不太可能被修改的上下文中使用的字面值,通常不被视为魔数。-1、0、0.0 和 1 这些值经常会出现在下面这样的场景中:
|
|
其他一些数字在其上下文中也可能含义非常明显(因此也不被视为魔数):
|
|
最佳实践
避免在代码中使用魔数(改用 constexpr 变量)。