std::string简介
本节阅读量:在之前的课程中,我们介绍了 C 风格字符串:
|
|
虽然 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> 头文件中。
我们可以像创建其他对象一样创建 std::string 类型的对象:
|
|
就像普通变量一样,你可以初始化 std::string 对象,也可以给它赋值:
|
|
需要注意的是,字符串也可以由数字字符组成:
|
|
在字符串形式中,数字被视为文本而不是数值,因此它们不能像数字一样被运算(例如不能相乘)。C++ 不会在字符串和整型或浮点数之间自动转换,反之亦然(当然,也有一些方法可以进行转换,我们会在后续课程中介绍)。
用 std::cout 输出字符串
使用 std::cout 可以像预期的那样输出 std::string 对象:
|
|
这将输出:
|
|
空字符串则不会输出任何内容:
|
|
输出为:
|
|
std::string 可以处理不同长度的字符串
大多数类型分配给它们的字节数是固定的。例如,如果系统上的 int 是 4 字节,那么每个 int 对象都会占用 4 字节的内存。如果你想保存一个需要超过 4 字节存储的整数值……那就只能使用其他类型。
std::string 最厉害的一点就是可以保存不同长度的字符串:
|
|
输出为:
|
|
在上面的示例中,name 最初用字符串 “Fly” 初始化,它包含 4 个字符(3 个显式字符加上一个 null 结尾符)。接下来我们把 name 改成了更长的字符串,然后再改成更短的字符串。std::string 处理这些情况毫无问题!
这也是 std::string 强大的原因之一。
使用 std::cin 读取字符串输入
将 std::string 与 std::cin 一起使用时可能会出现一些意外情况!来看下面这个例子:
|
|
下面是这个程序的一次运行结果:
|
|
发生了什么?原来,当使用 >> 运算符从 std::cin 中提取字符串时,>> 只会返回遇到的第一个空白字符之前的那部分内容。剩下的字符都留在 std::cin 中,等待下一次提取。
因此,当我们用 >> 将输入提取到变量 name 时,只取到了 “John”,而 “Doe” 则被留在了 std::cin 中。接着,当我们用 >> 向 color 提取输入时,它直接提取了 “Doe”,而不再等待我们输入新的内容。随后程序就结束了。
使用 std::getline() 读取整行文本
要将整行输入读入到字符串中,最好改用 std::getline() 函数。getline() 需要两个参数:第一个是 std::cin,第二个是字符串变量。
下面是使用 std::getline() 改写的上面的程序:
|
|
现在,我们的程序就能如预期般工作了:
|
|
什么是 std::ws?
在——浮点数相关——课程中,我们讨论过输出操纵器,它可以改变输出的呈现方式。在那节课中,我们使用了输出操纵器函数 std::setprecision() 来改变 std::cout 显示的精度位数。
C++ 也支持输入操纵器,它可以改变接收输入的方式。std::ws 输入操纵器告诉 std::cin 在提取之前忽略所有前导空白字符。前导空白是指出现在字符串开头的任何空白字符(空格、制表符、换行符)。
我们来看看为什么这有用。考虑下面这个程序:
|
|
下面是该程序的一次输出:
|
|
该程序首先要求你输入 1 或 2。到目前为止一切正常。然后它会提示你输入姓名。然而,它并不会真的等待你输入姓名!相反,它直接打印了 “Hello” 那一行,然后就退出了。
当使用 >> 运算符读取输入值时,std::cin 不仅会捕获该值,还会捕获按下回车时产生的换行符(’\n’)。因此,当我们输入 2 并按下回车时,std::cin 实际上捕获了字符串 “2\n”。然后它从中把整数 2 提取给 choice,把换行符留在缓冲区里。接下来,当 std::getline() 想把输入读到 name 中时,它发现 std::cin 中已经有一个 “\n” 在等待,于是认为我们输入了一个空字符串!
我们可以对上面的程序做一点修改,使用 std::ws 输入操纵器来告诉 std::getline() 忽略任何前导空白字符:
|
|
现在,这个程序就能按预期工作了:
|
|
最佳实践
如果使用 std::getline() 读取字符串,请使用 std::cin » std::ws 输入操纵器来忽略前导空白。每次调用 std::getline() 都需要重新添加,因为 std::ws 的设置不会在新的调用中保留。
关键点
对 std::cin 使用提取运算符(»)会忽略前导空白。std::getline() 则不会忽略前导空白,除非使用输入操纵器 std::ws。它在遇到换行符时会停止读取。
std::string 的长度
如果我们想知道一个 std::string 中有多少个字符,可以查询 std::string 对象的长度。它的语法与前面所见的稍有不同,但非常简单:
|
|
输出为:
|
|
虽然从 C++11 开始 std::string 要求以 null 结尾,但 std::string 返回的长度不包括隐式的 null 结尾符。
请注意,调用方式并不是 length(name),而是 name.length()。length() 并不是一个普通的独立函数——它是一种特殊的函数,嵌套在 std::string 内部,被称为成员函数。由于 length() 成员函数是在 std::string 内部声明的,因此在文档中有时被写作 std::string::length()。
后面我们会更详细地介绍成员函数,包括如何编写自己的成员函数。
还要注意,std::string::length() 返回的是无符号整数值(很可能是 size_t 类型)。如果要把长度赋值给 int 变量,应当使用 static_cast 进行转换,以避免编译器对有符号/无符号转换给出警告:
|
|
在 C++20 中,你还可以使用 std::ssize() 函数以有符号整数值的形式获取 std::string 的长度:
|
|
关键点
对于普通函数,我们的调用方式是 function(object)。而对于成员函数,我们的调用方式是 object.function()。
初始化 std::string 的开销很大
每次初始化 std::string 时,都会复制一份用于初始化它的字符串。复制字符串的代价是很大的,因此我们应当尽量减少复制的次数。
不要按值传递 std::string
当按值将 std::string 传给函数时,必须实例化 std::string 类型的函数参数,并用传入的实参来初始化它。这会带来昂贵的复制。我们将在——std::string_view 简介——中讨论如何避免这种复制(使用 std::string_view)。
最佳实践
不要按值传递 std::string,因为这样会生成昂贵的副本。
提示
在大多数情况下,请改用 std::string_view 参数(在——std::string_view 简介——中介绍)。
返回 std::string
当函数按值向调用方返回时,返回值通常会从函数复制回调用方。因此,你可能会认为不应按值返回 std::string,因为这样做会返回一份昂贵的 std::string 副本。
然而,作为经验法则,当 return 语句的表达式结果属于下列情况之一时,按值返回 std::string 是可以的:
- 类型为 std::string 的局部变量。
- 由函数调用或运算符按值返回的 std::string。
- 作为 return 语句的一部分所创建的 std::string。
除此之外的大多数场景下,不要按值返回 std::string,因为这样会产生昂贵的副本。
进阶读者
std::string 支持一种叫做“移动语义”的特性,它允许那些本来会在函数结束时被销毁的对象按值返回而不产生复制。移动语义的具体原理超出了本文的讨论范围,我们会在后续介绍。
提示
如果返回的是 C 风格字符串字面值,请改用 std::string_view 作为返回类型。
进阶读者
std::string 也可以通过(const)引用返回,这是另一种避免复制的方式。我们会在后续进行介绍。
std::string 的字面值
双引号括起来的字符串(例如 “Hello, world!")默认是 C 风格字符串(因此具有那种奇怪的类型)。
我们可以在双引号字符串后面加上 s 后缀,来创建类型为 std::string 的字符串字面值。
|
|
你可能不会经常用到 std::string 字面值(因为用 C 风格字符串初始化 std::string 对象是完全没问题的),但我们会在后面的课程中看到一些场景(涉及类型推导),在这些场景下使用 std::string 字面值而非 C 风格字符串,会让事情更简单。
提示
“s” 后缀位于命名空间 std::literals::string_literals 中。
启用字面值后缀最简洁的方式是使用 using namespace std::literals,但这会把所有标准库字面值后缀都引入进来,其中包含了很多你可能用不到的东西。
我们建议使用 using namespace std::string_literals,它只会引入 std::string 相关的字面值后缀。
我们会在后续课程中讨论 using 指令。这是少数几种可以使用整个命名空间的例外场景,因为这些被定义的字面值后缀不太可能与你自己的代码发生冲突。但在头文件中,应避免在函数外部使用 using 指令。
进阶读者
“Hello"s 会被解析为 std::string{“Hello”, 6},它用 C 风格字符串 “Hello”(长度为 6,包含隐式 null 结尾符)初始化了一个临时的 std::string。
constexpr 字符串
如果尝试定义 constexpr std::string,编译器可能会报错:
|
|
这是因为 C++17 及更早的版本根本不支持 constexpr std::string,而在 C++20/23 中也仅在非常有限的场景下才支持。如果需要 constexpr 字符串,请改用 std::string_view。
总结
std::string 的实现相当复杂,用到了许多我们目前还未介绍的语言特性。幸运的是,你不需要了解这些复杂细节,就可以将 std::string 用于像基本的字符串输入输出这样的简单任务。我们鼓励你现在就开始尝试字符串,后续我们还会介绍更多字符串相关的功能。