5-7 std::string 介绍
在第5.2节——字面量中,我们介绍了C风格的字符串字面量:
#include <iostream>
int main()
{
std::cout << "Hello, world!"; // "Hello world!" is a C-style string literal.
return 0;
}
虽然C风格字符串常量可以使用,但C风格字符串变量行为怪异、难以操作(例如无法通过赋值操作为C风格字符串变量赋新值),且存在安全隐患(例如将较长的C风格字符串复制到为较短字符串分配的空间中,将导致未定义行为)。在现代C++中,应尽量避免使用C风格字符串变量。
值得庆幸的是,C++引入了两种更易用且安全的字符串类型:std::string和std::string_view(C++17)。与先前介绍的类型不同,std::string 和 std::string_view 并非基础类型(它们属于类类型,我们将在后续章节详述)。但两者的基本用法足够直观且实用,因此在此先行介绍。
介绍 std::string
在 C++ 中处理字符串和字符串对象最简单的方式是通过 std::string 类型,该类型定义在
我们可以像创建其他对象一样创建 std::string 类型的对象:
#include <string> // allows use of std::string
int main()
{
std::string name {}; // empty string
return 0;
}
与普通变量类似,你可以像预期那样初始化或为 std::string 对象赋值:
#include <string>
int main()
{
std::string name { "Alex" }; // initialize name with string literal "Alex"
name = "John"; // change name to "John"
return 0;
}
请注意,字符串也可以由数字字符组成:
std::string myID{ "45" }; // "45" is not the same as integer 45!
在字符串形式下,数字会被视为文本而非数字,因此无法进行数字运算(例如无法进行乘法运算)。C++不会自动将字符串转换为整数或浮点数,反之亦然(不过存在实现方法,我们将在后续课程中讲解)。
使用 std::cout 输出字符串
std::string 对象可通过 std::cout 按预期输出:
#include <iostream>
#include <string>
int main()
{
std::string name { "Alex" };
std::cout << "My name is: " << name << '\n';
return 0;
}
这将输出:

空字符串将不输出任何内容:
#include <iostream>
#include <string>
int main()
{
std::string empty{ };
std::cout << '[' << empty << ']';
return 0;
}
打印什么:

std::string 能够处理不同长度的字符串
std::string 最酷的功能之一就是能够存储不同长度的字符串:
#include <iostream>
#include <string>
int main()
{
std::string name { "Alex" }; // initialize name with string literal "Alex"
std::cout << name << '\n';
name = "Jason"; // change name to a longer string
std::cout << name << '\n';
name = "Jay"; // change name to a shorter string
std::cout << name << '\n';
return 0;
}
这将输出:

在上例中,name 变量初始化为字符串 “Alex”,该字符串包含五个字符(四个显式字符和一个空终止符)。随后我们先将 name 赋值为更长的字符串,再赋值为更短的字符串。std::string 完全能够胜任这种操作!你甚至可以将超长字符串存储在 std::string 中。
这正是 std::string 如此强大的原因之一。
关键要点
当 std::string 内存不足以存储字符串时,它会通过动态内存分配机制在运行时请求额外空间。这种动态扩展能力赋予了 std::string 极高的灵活性,但也导致其运行效率相对较低。
动态内存分配机制将在后续章节中详细探讨。
使用 std::cin 进行字符串输入
使用 std::string 与 std::cin 配合时可能会产生意外结果!请看以下示例:
#include <iostream>
#include <string>
int main()
{
std::cout << "Enter your full name: ";
std::string name{};
std::cin >> name; // this won't work as expected since std::cin breaks on whitespace
std::cout << "Enter your favorite color: ";
std::string color{};
std::cin >> color;
std::cout << "Your name is " << name << " and your favorite color is " << color << '\n';
return 0;
}
以下是该程序的示例运行结果:

嗯,不对劲!怎么回事?原来使用operator>>运算符从std::cin提取字符串时,它只会返回遇到第一个空格前的字符。其余字符仍留在std::cin中,等待下次提取。
因此当我们用operator>>运算符将输入提取到变量name时,仅提取了“John”,而“Doe”仍留在std::cin中。随后再次使用operator>>运算符提取变量color的输入时,程序直接提取了“Doe”而非等待用户输入颜色,导致程序终止。
使用 std::getline() 输入文本
若要将整行输入读取到字符串中,建议改用 std::getline() 函数。该函数需要两个参数:第一个是 std::cin,第二个是你的字符串变量。
以下是使用 std::getline() 实现的同上程序:
#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到底是什么?
在第4.8节——浮点数中,我们讨论了输出操作符,它允许我们改变输出的显示方式。在那节课中,我们使用输出操作符函数std::setprecision()来改变std::cout显示的数字精度位数。
C++同样支持输入操作符,用于改变输入的接收方式。std::ws输入操作符指示std::cin在提取数据前忽略所有前导空格。所谓前导空格,是指字符串开头出现的任何空格字符(包括空格、制表符和换行符)。
让我们探究其用途。请看以下程序:
#include <iostream>
#include <string>
int main()
{
std::cout << "Pick 1 or 2: ";
int choice{};
std::cin >> choice;
std::cout << "Now enter your name: ";
std::string name{};
std::getline(std::cin, name); // note: no std::ws here
std::cout << "Hello, " << name << ", you picked " << choice << '\n';
return 0;
}
以下是该程序的输出结果:

该程序首先要求你输入1或2,并等待你输入。到目前为止一切正常。随后它会要求你输入姓名。然而,它实际上并不会等待你输入姓名!相反,它会打印出“Hello”字符串,然后退出。
当使用operator>>运算符输入值时,std::cin不仅捕获数值,还会捕获按下回车键时产生的换行符'\n'。因此当输入2后按回车,std::cin会捕获字符串“2\n”作为输入。它提取数值2赋值给变量choice,而将换行符保留待后续处理。此时,当 std::getline() 试图将文本提取到 name 时,发现 std::cin 中已存在 “\n”,误判为先前输入了空字符串!这显然与预期效果相悖。
我们可通过修改程序使用 std::ws 输入操作符,指示 std::getline() 忽略所有前导空格字符:
#include <iostream>
#include <string>
int main()
{
std::cout << "Pick 1 or 2: ";
int choice{};
std::cin >> choice;
std::cout << "Now enter your name: ";
std::string name{};
std::getline(std::cin >> std::ws, name); // note: added std::ws here
std::cout << "Hello, " << name << ", you picked " << choice << '\n';
return 0;
}
现在这个程序将按预期运行。

最佳实践
若使用 std::getline() 读取字符串,请配合 std::cin >> std::ws 输入操作符忽略前导空格。每次调用 std::getline() 时均需执行此操作,因为 std::ws 在不同调用间不会被保留。
关键要点
向变量提取数据时,提取运算符(>>)会忽略前导空格,并在遇到非前导空格时停止提取。
std::getline()不会忽略前导空格。若需忽略前导空格,请将std::cin >> std::ws作为第一个参数传递。该函数在遇到换行符时停止提取。
std::string的长度
若要获取std::string对象的字符数量,可直接查询其长度属性。该操作的语法虽与先前所见不同,但逻辑十分清晰:
#include <iostream>
#include <string>
int main()
{
std::string name{ "Alex" };
std::cout << name << " has " << name.length() << " characters\n";
return 0;
}
这将输出:

尽管 C++11 起要求 std::string 必须为空终止字符串,但 std::string 返回的长度并不包含隐含的空终止符。
请注意,我们不应将字符串长度表示为 length(name),而应使用 name.length()。length() 函数并非普通独立函数——它是嵌套在 std::string 类中的特殊函数类型,称为成员函数。由于 length() 成员函数声明于 std::string 内部,文档中有时会写成 std::string::length()。
我们将在后续章节详细讲解成员函数,包括如何编写自定义成员函数。
关键要点
普通函数调用格式为 function(object),成员函数则采用 object.function() 形式。
另需注意:std::string::length() 返回无符号整数值(通常为 size_t 类型)。若需将长度赋值给 int 变量,应使用 static_cast 进行类型转换以避免编译器关于有符号/无符号转换的警告:
int length { static_cast<int>(name.length()) };
对于高级读者
在 C++20 中,您还可以使用 std::ssize() 函数获取 std::string 的长度,该长度以大型有符号整数类型(通常为 std::ptrdiff_t)表示:
#include <iostream> #include <string> int main() { std::string name{ "Alex" }; std::cout << name << " has " << std::ssize(name) << " characters\n"; return 0; }由于 ptrdiff_t 可能大于 int,若需将 std::ssize() 的结果存储在 int 变量中,应使用 static_cast 将结果转换为 int:
int len { static_cast<int>(std::ssize(name)) };
初始化 std::string 的开销较大
每次初始化 std::string 时,都会创建用于初始化的字符串副本。复制字符串的开销较大,因此应谨慎操作以尽量减少复制次数。
不要按值传递 std::string
当按值将 std::string 传递给函数时,函数形参 std::string 必须实例化并用实参初始化。这会导致代价高昂的复制操作。我们将在第 5.8 课——std::string_view 介绍中讨论替代方案(使用 std::string_view)。
最佳实践
避免按值传递 std::string,因其会产生高成本的复制操作。
提示
多数情况下应改用 std::string_view 作为参数(详见第 5.8 课——std::string_view 介绍)。
返回 std::string
当函数通过值返回给调用方时,返回值通常会被从函数复制回调用方。因此你可能会认为不应通过值返回 std::string,因为这样会返回一个耗费资源的 std::string 副本。
但经验法则表明,当返回语句的表达式符合以下任一情况时,按值返回 std::string 是可行的:
- 类型为 std::string 的局部变量
- 由其他函数调用或运算符按值返回的 std::string
- 作为返回语句组成部分创建的 std::string 临时对象
对于进阶读者
std::string支持名为“移动语义”的功能,允许在函数结束时被销毁的对象通过值返回而不进行复制。移动语义的具体实现超出本文范围,将在第16.5课“返回std::vector与移动语义介绍”中详述。
在多数其他场景中,应避免按值返回 std::string,此操作将产生高成本的复制。
提示
若需返回 C 风格字符串常量,请改用 std::string_view 作为返回类型(详见第 5.9 课——std::string_view(第二部分))。
对于进阶读者
特定情况下,std::string 也可通过(const)引用返回,这是另一种避免复制的方法。相关细节将在第12.12课——引用返回与地址返回,以及第14.6课——访问函数中深入探讨。
std::string 的字面量
双引号字符串字面量(如“Hello, world!”)默认是 C 风格字符串(因此具有奇怪的类型)。
我们可以在双引号字符串字面量后添加 s 后缀来创建类型为 std::string 的字符串字面量。s 必须为小写。
#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;
}

提示
字符串后缀“s”位于命名空间std::literals::string_literals中。
访问字面量后缀最简洁的方式是通过using-directive使用命名空间std::literals。但这会将标准库中所有字面量导入using-directive的作用域,引入大量您可能用不到的内容。
我们建议使用 namespace std::string_literals,该指令仅导入 std::string 的字面量。
第 7.13 课《using 声明与 using 指令》将详细讨论 using 指令。此处属于例外情况,通常可以导入整个命名空间,因为其中定义的后缀极少与用户代码发生冲突。请避免在头文件的函数外部使用此类 using 指令。
你可能不会经常使用 std::string 字面量(因为用 C 风格字符串字面量初始化 std::string 对象完全没问题),但后续课程中我们会看到几个案例(涉及类型推导),此时使用 std::string 字面量而非 C 风格字符串字面量会更便捷(参见 10.8 节 ——使用 auto 关键字的对象类型推导)。
对于进阶读者
“Hello” 将解析为 std::string { “Hello”, 5 },该表达式会创建一个临时 std::string 对象,其初始化值为 C 风格字符串字面量“Hello”(长度为 5,不包含隐含的空终止符)。
常量字符串
若尝试定义常量字符串 std::string,编译器很可能会报错:
#include <iostream>
#include <string>
int main()
{
using namespace std::string_literals;
constexpr std::string name{ "Alex"s }; // compile error
std::cout << "My name is: " << name;
return 0;
}

这是因为在C++17及更早版本中完全不支持constexpr std::string,而在C++20/23中也仅在极少数情况下有效。若需使用constexpr字符串,请改用std::string_view(详见第5.8课——std::string_view介绍)。
结论
std::string 结构复杂,涉及许多尚未讲解的语言特性。所幸在处理基础字符串输入输出等简单任务时,您无需理解这些复杂机制。我们建议您立即开始尝试使用字符串,后续章节将深入探讨更多字符串功能。
测验时间
问题 #1
编写一个程序,要求用户输入全名和年龄。输出结果应为用户年龄与姓名字符数之和(使用 std::string::length() 成员函数获取字符串长度)。为简化处理,姓名中的空格也计为一个字符。
示例输出:
Enter your full name: John Doe
Enter your age: 32
Your age + length of name is: 40
提醒:我们需要注意避免将有符号值与无符号值混用。std::string::length() 返回的是无符号值。若您的项目支持 C++20,请使用 std::ssize() 获取有符号长度值。否则,请将 std::string::length() 的返回值通过 static_cast 转换为 int 类型。

显示解决方案
#include <iostream>
#include <string>
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 age: ";
int age{}; // age needs to be an integer, not a string, so we can do math with it
std::cin >> age;
// age is signed, and name.length() is unsigned -- we shouldn't mix these
// We'll convert name.length() to a signed value
int nameLen { static_cast<int>(name.length()) }; // get number of chars in name (including spaces)
std::cout << "Your age + length of name is: " << age + nameLen << '\n';
return 0;
}

浙公网安备 33010602011771号