Cpp学习7 第 5 章 常量与字符串

铁名_IronName Lv5

5.1 — 常量变量(命名常量)

在编程中, 常量是指在程序执行期间不能改变的值。
命名常量 Named constants是与标识符关联的常量值。它们有时也称为符号常量 symbolic constants 。
字面常量 Literal constants是指不与标识符关联的常量值。

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

Constant variables  常量变量

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

常量变量 必须 在定义时进行初始化,之后不能通过赋值来改变其值, const 量可以从其他变量(包括非常量变量)初始化:

由于常量变量的行为与普通变量类似(只是不能赋值),因此没有必要采用特殊的命名规则。正因如此,我们更倾向于使用与非常量变量相同的命名规则(例如 earthGravity )。

不要将 const 用于函数形式参数(没啥用,冗余)。
不要将 const 用于函数返回类型(没啥用,冗余)。

如果一个变量可以设为常量,通常应该将其设为常量。

带有替换文本的对象式宏

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

#define MY_NAME "Alex"

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

return 0;
}

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

最佳实践

优先使用常量变量,而不是带有替换文本的对象型宏。

在多文件程序中使用常量变量,见7.10课

命名规则:类型限定符 type qualifiers
类型限定符 (有时简称限定符 )是一个关键字,它应用于类型,用于修改该类型的行为。用于声明常量变量的 const 称为 const 类型限定符 (或简称 const 限定符 )。
从 C++23 开始,C++ 只有两个类型限定符: const 和 volatile 。
volatile 限定符用于告诉编译器,一个对象的值可能随时被更改。这个很少使用的限定符会禁用某些类型的优化。

5.2 — 字面量 Literals

字面量是直接插入到代码中的值。

如果字面量的默认类型不符合预期,可以通过添加后缀来更改字面量的类型。

默认情况下,浮点字面量的类型为 double 。要将其改为 float 字面量,应使用 f (或 F )后缀。浮点字面量还可以用科学计数法表示。

字符串字面量

所有 C 风格的字符串字面量都隐含一个空终止符。例如,考虑字符串 "hello" 。虽然这个 C 风格字符串看起来只有五个字符,但实际上它有六个: 'h' 、 'e' 、 'l ‘、 'l' 、 'o' 和 '\0' (ASCII 码为 0 的字符)。末尾的 ‘\0’ 字符是一个特殊字符,称为空终止符 ,用于指示字符串的结尾。以空终止符结尾的字符串称为空终止字符串 。

与其他大多数字面量(它们是值,而不是对象)不同,C 风格的字符串字面量是常量对象,它们在程序开始时创建,并保证在整个程序运行期间都存在。这一点在几节课中讨论 std::string_view 时会变得很重要。

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

魔法数字

魔法数字是指含义不明确或可能需要在以后更改的字面值(通常是一个数字)。
在复杂的程序中,如果没有注释进行解释,就很难推断字面量代表什么。

避免在代码中使用魔法数字(改用 constexpr 变量,参见第 5.6 课 — constexpr 变量 )。

5.3 — 数制(十进制、二进制、十六进制和八进制)

要使用八进制字面量,请在字面量前面加上 0。例如int x{012}
要使用十六进制字面量,请在字面量前加上 0x 前缀。例如int x{0xF}
从 C++14 开始,我们可以使用 0b 前缀来使用二进制字面量。

由于过长的字面量难以阅读,C++14 还增加了使用引号 (‘) 作为数字分隔符的功能。

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

int main()
{
int bin { 0b1011'0010 }; // assign binary 1011 0010 to the variable
long value { 2'132'673'462 }; // much easier to read than 2132673462

return 0;
}

另请注意,分隔符不能出现在值的第一位数字之前。数字分隔符纯粹是视觉上的,不会以任何方式影响字面值。

默认情况下,C++ 以十进制输出值。但是,您可以使用 std::dec 、 std::oct 和 std::hex I/O 操作符来更改输出格式。这种修改是 sticky 的。

以二进制形式输出值稍微复杂一些,因为 std::cout 本身并不具备这种功能。幸运的是,C++ 标准库中包含一个名为 std::bitset 类型,它可以帮我们实现这个功能(在头文件中)。略。

5.4 — as-if 规则和编译时优化

在编程中, 优化是指修改软件以使其更高效地运行(例如,运行速度更快或资源占用更少)的过程。优化可以对应用程序的整体性能水平产生巨大影响。
某些类型的优化通常需要手动完成。可以使用名为性能分析器 profiler 的程序来查看程序各个部分的运行时间,以及哪些部分影响了整体性能。然后,程序员可以寻找方法来缓解这些性能问题。由于手动优化速度较慢,程序员通常专注于进行能够产生显著影响的高级改进(例如选择性能更高的算法、优化数据存储和访问、降低资源占用、并行化任务等)。
其他类型的优化可以自动执行,使用 优化器 optimizer。

 “as-if 规则” 规定,只要不影响程序的“可观察行为”,编译器可以随意修改程序以生成更优化的代码。

现代 C++ 编译器能够在编译时(而不是运行时)对某些表达式进行全部或部分求值。当编译器在编译时对表达式进行全部或部分求值时,这被称为编译时求值 。
常量折叠常量传播死代码消除….

使用常量变量可以帮助编译器更有效地进行优化。

5.5 — 常量表达式

C++ 语言提供了一些方法,允许我们在编译时明确指定要执行的代码部分。使用能够实现编译时求值的语言特性被称为编译时编程 。

区分
  • 编译时优化(被动)编译器说了算。 你甚至不需要写任何特殊代码,只要打开优化开关(如 -O2),编译器就会自己分析代码,把能提前算的算出来。它想优化就优化,不想优化就不优化,你管不着。

  • 编译时编程(主动)你(程序员)说了算。 你必须主动使用 constexprconsteval 等关键词。这等于给编译器下了一道死命令:“这段你必须编译时给我算出来,算不出来就报错给我看,别编译通过!”

 常量表达式也是一个由字面量、常量变量、运算符和函数调用组成的非空序列,其中所有元素都必须在编译时可求值。

略。

5.6 — Constexpr 变量

在变量声明中使用 constexpr 关键字(它是“常量表达式”的简写)而不是 const 变量,确保在需要的地方获得编译时常量变量。constexpr 变量必须使用常量表达式进行初始化,否则会导致编译错误。

const 和 constexpr

任何初始化器为常量表达式的常量变量都应该声明为 constexpr 。
任何初始化器不是常量表达式的常量变量(使其成为运行时常量)都应该声明为 const 。
const不一定会编译时求值,而constexpr一定会。

constexpr 函数是一种可以在常量表达式中调用的函数。当 constexpr 函数所属的常量表达式必须在编译时求值时(例如在 constexpr 变量的初始化器中),constexpr 函数必须在编译时求值。否则,constexpr 函数可以在编译时(如果符合条件)或运行时求值。要符合编译时执行的条件,所有参数都必须是常量表达式。可以用来写复杂的预处理函数,真的是数学上的函数那种。

“数据在编译时能确定,且它后面用于定大小、选分支、做断言,用 constexpr!”

5.7 — std::string 简介

虽然可以使用 C 风格的字符串字面量,但 C 风格的字符串变量行为怪异,难以操作(例如,不能使用赋值语句给 C 风格的字符串变量赋新值),而且存在风险(例如,如果将一个较长的 C 风格字符串复制到分配给较短 C 风格字符串的空间中,会导致未定义行为)。在现代 C++ 中,最好避免使用 C 风格的字符串变量。
C++ 引入了两种更易于使用且更安全的字符串类型: std::string 和 std::string_view (C++17)。与我们之前介绍的类型不同, std::string 和 std::string_view 并非基本类型(它们是类,我们将在后续章节中介绍)。

std::string

在 C++ 中,处理字符串和字符串对象的最简单方法是通过 std::string 类型,该类型位于头文件<string>中。

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

int main()
{
std::string name { "Alex" }; // initialize name with string literal "Alex"
name = "John"; // change name to "John"

return 0;
}

std::string 最巧妙的功能之一就是可以存储不同长度的字符串。如果 std::string 没有足够的内存来存储字符串,它会在运行时使用一种称为 动态内存分配 的内存分配方式来请求额外的内存。这种获取额外内存的能力是 std::string 如此灵活的原因之一,但也使其速度相对较慢。

要将整行输入读取到字符串中,最好使用 std::getline() 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string> // For std::string and std::getline

int main()
{
std::cout << "Enter your full name: ";
std::string name{};
std::getline(std::cin >> std::ws, name); // read a full line of text into name

std::cout << "Enter your favorite color: ";
std::string color{};
std::getline(std::cin >> std::ws, color); // read a full line of text into color

std::cout << "Your name is " << name << " and your favorite color is " << color << '\n';

return 0;
}

std::ws 是 输入操纵符,指示 std::cin 在提取数据之前忽略任何前导空白字符。前导空白字符是指字符串开头出现的任何空白字符(空格、制表符、换行符)。

最佳实践

如果使用 std::getline() 读取字符串,请使用 std::cin >> std::ws 输入操纵符来忽略前导空格。每次调用 std::getline() 时都需要这样做,因为 std::ws` 在调用之间不会保留。

用 name.length() 查询长度。
另请注意, std::string::length() 返回的是一个无符号整数值(很可能是 size_t 类型)。如果要将长度赋值给一个 int 变量,则应该使用 static_cast 其转换为 int 类型,以避免编译器发出关于有符号/无符号转换的警告。

每次初始化 std::string 时,都会复制用于初始化的字符串。复制字符串的开销很大,因此应尽量减少复制次数。不要按值传递 std::string ,因为它会进行代价高昂的复制。
大多数情况下,请使用 std::string_view 参数

std::string 的字面量

双引号字符串字面量(如“Hello, world!”)默认是 C 风格的字符串。
通过在双引号括起来的字符串字面量后添加后缀 s 来创建类型为 std::string 字符串字面量。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string> // for std::string

int main()
{
using namespace std::string_literals; // easy access to the s suffix

std::cout << "foo\n"; // no suffix is a C-style string literal
std::cout << "goo\n"s; // s suffix is a std::string literal

return 0;
}

访问字面量后缀的最简洁方法是通过 using 指令 using namespace std::literals 。但是,这会将 所有 标准库字面量导入到 using 指令的作用域中,从而引入很多你可能用不到的东西。
我们建议 using namespace std::string_literals ,它只导入 std::string 的字面量。

可能不需要经常使用 std::string 字面量(因为用 C 风格的字符串字面量初始化 std::string 对象是可以的),但我们将在以后的课程中看到一些情况(涉及类型推导),在这些情况下,使用 std::string 字面量而不是 C 风格的字符串字面量会使事情变得更容易。

constexpr std::string 在 C++17 或更早版本中完全不受支持,在 C++20/23 中也仅在非常有限的情况下有效。如果您需要 constexpr 字符串,请改用 std::string_view

quiz

1
2
3
4
void printResult(std::string name, int age) {
int name_length{ static_cast<int>(name.length()) };
std::cout << "Your age + length of name is: " << name_length + age;
}

5.8 — std::string_view 简介

为了解决 std::string 初始化(或复制)开销过大的问题,C++17 引入了 std::string_view (位于头文件中<string_view>)。std std::string_view 允许对 现有 字符串(C 风格字符串、 std::string 或其他 std::string_view )进行只读访问,而无需创建副本。 只读意味着我们可以访问和使用被查看的值,但不能对其进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string_view> // C++17

// str provides read-only access to whatever argument is passed in
void printSV(std::string_view str) // now a std::string_view
{
std::cout << str << '\n';
}

int main()
{
std::string_view s{ "Hello, world!" }; // now a std::string_view
printSV(s);

return 0;
}

std::string_view 的字面量,略。

与 std::string 不同, std::string_view 完全支持 constexpr:

5.9 — std::string_view(第 2 部分)

std::string 是(唯一)所有者。字符串复制成本很高,为什么要为不需要的副本付费呢?
std::string_view 是一个查看器。

view 依赖于被查看的对象。如果在 view 仍在运行时被查看的对象被修改或销毁,则会导致意外或未定义的行为。 a dangling view

std::string_view 最好用作只读函数参数。

std::string_view or const std::string& function parameters ?

大多数情况下,最好使用 std::string_view 。

不要使用std::string 初始化 std::string_view,因为这会导致 std::string_view 悬空。 a C-style string literal or a std::string_view literal 是可以的。

view 修改功能

注意不要编写任何假定 std::string_view 以空字符结尾的代码。

关于 std::string 需要记住的事项:

  • 初始化和复制 std::string 开销很大,所以应尽可能避免这样做。
  • 避免按值传递 std::string ,因为这会创建一个副本。
  • 尽量避免创建生命周期很短的 std::string 对象。
  • 修改 std::string 将使对该字符串的所有视图失效。
  • 按值返回本地 std::string 是可以的。

关于 std::string_view 需要记住的事项:

  • std::string_view 通常用于传递字符串函数参数并返回字符串字面量。
  • 因为整个程序都存在 C 风格的字符串字面量,所以始终可以将 std::string_view 设置为 C 风格的字符串字面量。
  • 当一个字符串被销毁时,所有指向该字符串的视图都会失效。
  • 使用已失效的视图(除了使用赋值重新验证视图之外)会导致未定义行为。
  • std::string_view 可能以空字符结尾,也可能不以空字符结尾。

5.x 测验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <string>
#include <string_view>

std::string enterName(int idx)
{
std::cout << "Enter the name of person #" << idx << ": ";
std::string name{};
std::getline(std::cin >> std::ws, name);
return name;
}

int enterAge(std::string_view name)
{
std::cout << "Enter the age of "<<name<<": ";
int x;
std::cin >> x;
return x;
}

void printResult(std::string_view name1, int age1, std::string_view name2, int age2) {
if (age1 > age2)
std::cout << "" << name1 << " (age " << age1 << ") is older than " << name2 << " (age " << age2 << ").";
if (age1 < age2)
std::cout << "" << name2 << " (age " << age2 << ") is older than " << name1 << " (age " << age1 << ").";
}

int main()
{
std::string name1{ enterName(1) };
int age1{ enterAge(name1) };
std::string name2{ enterName(2) };
int age2{ enterAge(name2) };

printResult(name1, age1, name2, age2);
return 0;
}
  • 标题: Cpp学习7 第 5 章 常量与字符串
  • 作者: 铁名_IronName
  • 创建于 : 2026-08-08 09:43:32
  • 更新于 : 2026-08-08 13:38:56
  • 链接: https://blog.ironname.top/2026/Cpp/Cpp学习7/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论