在字面量这节中中,我们介绍了C风格字符串字面量:
#include <iostream>
int main()
{
std::cout << "Hello, world!"; // "Hello world!"是一个C风格字符串字面量。
return 0;
}
虽然可以使用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类型,它位于
我们可以像创建其他对象一样创建std::string类型的对象:
#include <string> // 允许使用std::string
int main()
{
std::string name {}; // 空字符串
return 0;
}
就像普通变量一样,你可以像预期的那样初始化或给std::string对象赋值:
#include <string>
int main()
{
std::string name { "Alex" }; // 用字符串字面量"Alex"初始化name
name = "John"; // 将name改为"John"
return 0;
}
请注意,字符串也可以由数字字符组成:
std::string myID{ “45” }; // “45"不等于整数45!
在字符串形式中,数字被视为文本,而不是数字,因此它们不能作为数字进行操作(例如,你不能将它们相乘)。C++不会自动将字符串转换为整数或浮点值,反之亦然(尽管我们将在以后的课程中介绍一些方法)。
使用std::cout输出字符串
可以使用std::cout按预期输出std::string对象:
#include <iostream>
#include <string>
int main()
{
std::string name { "Alex" };
std::cout << "My name is: " << name << '\n';
return 0;
}
这将打印:
My name is: Alex
空字符串将什么也不打印:
#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" }; // 用字符串字面量"Alex"初始化name
std::cout << name << '\n';
name = "Jason"; // 将name改为一个更长的字符串
std::cout << name << '\n';
name = "Jay"; // 将name改为一个更短的字符串
std::cout << name << '\n';
return 0;
}
这将打印:
Alex Jason Jay
在上面的例子中,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; // 这不会按预期工作,因为std::cin会在空白处中断
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;
}
这是该程序的一个示例运行结果:
Enter your full name: John Doe Enter your favorite color: Your name is John and your favorite color is Doe
嗯,这不对!发生了什么?原来,当使用运算符»从std::cin中提取一个字符串时,运算符»只返回它遇到的第一个空白字符之前的字符。其他字符留在std::cin中,等待下一次提取。
因此,当我们使用运算符»将输入提取到变量name中时,只有"John"被提取,而"Doe"留在std::cin中。然后,当我们使用运算符»将输入提取到变量color中时,它提取了"Doe",而不是等待我们输入一个颜色。然后程序结束。
使用std::getline()输入文本
要将一整行输入读入一个字符串,最好使用std::getline()函数。std::getline()需要两个参数:第一个是std::cin,第二个是你的字符串变量。
以下是使用std::getline()的上述相同程序:
#include <iostream>
#include <string> // 用于std::string和std::getline
int main()
{
std::cout << "Enter your full name: ";
std::string name{};
std::getline(std::cin >> std::ws, name); // 将一整行文本读入name
std::cout << "Enter your favorite color: ";
std::string color{};
std::getline(std::cin >> std::ws, color); // 将一整行文本读入color
std::cout << "Your name is " << name << " and your favorite color is " << color << '\n';
return 0;
}
现在我们的程序按预期工作:
Enter your full name: John Doe Enter your favorite color: blue Your name is John Doe and your favorite color is blue
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); // 注意:这里没有std::ws
std::cout << "Hello, " << name << ", you picked " << choice << '\n';
return 0;
}
这是该程序的一些输出:
Pick 1 or 2: 2 Now enter your name: Hello, , you picked 2
这个程序首先要求你输入1或2,并等待你输入。到目前为止一切顺利。然后它会要求你输入你的名字。然而,它实际上并不会等待你输入你的名字!相反,它打印出"Hello"字符串,然后退出。
当你使用运算符»输入一个值时,std::cin不仅捕获该值,还捕获你按下回车键时出现的换行符(’\n’)。因此,当你输入2然后按下回车键时,std::cin将"2\n"作为输入捕获。然后它将值2提取到变量choice中,留下换行符供以后使用。然后,当std::getline()去提取文本到name时,它看到"\n"已经在std::cin中等待,于是认为我们之前输入了一个空字符串!这显然不是我们想要的。
我们可以通过修改上述程序,使用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); // 注意:这里添加了std::ws
std::cout << "Hello, " << name << ", you picked " << choice << '\n';
return 0;
}
现在这个程序将按预期工作。
Pick 1 or 2: 2 Now enter your name: Alex Hello, Alex, you picked 2
最佳实践
如果使用std::getline()读取字符串,请使用std::cin » std::ws输入操纵符来忽略前导空白。这需要在每个std::getline()调用中完成,因为std::ws不会在调用之间保留。
关键洞见
当提取到变量时,提取运算符(»)会忽略前导空白。它在遇到非前导空白时停止提取。
std::getline()不会忽略前导空白。如果你想让它忽略前导空白,请将std::cin » std::ws作为第一个参数传递。它在遇到换行符时停止提取。
std::string的长度
如果我们想知道一个std::string中有多少个字符,我们可以询问一个std::string对象它的长度。这样做的语法与你之前见过的不同,但相当简单:
#include <iostream>
#include <string>
int main()
{
std::string name{ "Alex" };
std::cout << name << " has " << name.length() << " characters\n";
return 0;
}
这将打印:
Alex has 4 characters
尽管std::string被要求以空字符终止(自C++11起),但返回的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,以避免关于有符号/无符号转换的编译器警告:
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(第2部分)中介绍)。
对于高级读者
在某些情况下,std::string也可以通过(const)引用返回,这是另一种避免创建副本的方法。我们在第12.12课——按引用返回和按地址返回以及第14.6课——访问函数中进一步讨论这个问题。
std::string的字面量
双引号字符串字面量(如"Hello, world!")默认是C风格字符串(因此,有一个奇怪的类型)。
我们可以通过在双引号字符串字面量后面加上一个s后缀来创建类型为std::string的字符串字面量。s必须是小写的。
#include <iostream>
#include <string> // 用于std::string
int main()
{
using namespace std::string_literals; // 方便使用s后缀
std::cout << "foo\n"; // 没有后缀是C风格字符串字面量
std::cout << "goo\n"s; // s后缀是std::string字面量
return 0;
}
提示
“s"后缀位于命名空间std::literals::string_literals中。
访问字面量后缀最简洁的方式是通过使用指令using namespace std::literals。然而,这会将标准库中的所有字面量导入到使用指令的作用域中,这会带来一大堆你可能不会使用的玩意儿。
我们建议使用命名空间std::string_literals,它只导入std::string的字面量。
我们在第7.13课——使用声明和使用指令中讨论使用指令。这是一个使用整个命名空间通常是可以接受的例外情况,因为其中定义的后缀不太可能与你的代码发生冲突。避免在头文件中函数外使用这样的使用指令。
你可能不需要经常使用std::string字面量(因为用C风格字符串字面量初始化std::string对象是可以的),但我们在以后的课程中会看到一些情况(涉及类型推导),使用std::string字面量而不是C风格字符串字面量会让事情变得更容易(参见第10.8课——使用auto关键字的对象类型推导中的一个例子)。
对于高级读者
“Hello"s解析为std::string { “Hello”, 5 },它创建了一个临时std::string,用C风格字符串字面量"Hello"初始化(它的长度为5,不包括隐式的空字符终止符)。
constexpr字符串
如果你尝试定义一个constexpr std::string,你的编译器可能会报错:
#include <iostream>
#include <string>
int main()
{
using namespace std::string_literals;
constexpr std::string name{ "Alex"s }; // 编译错误
std::cout << "My name is: " << name;
return 0;
}
这发生是因为在C++17及更早版本中根本不支持constexpr std::string,而在C++20/23中只有在非常有限的情况下才支持。如果你需要constexpr字符串,请使用std::string_view。
总结
std::string很复杂,利用了许多我们尚未涵盖的语言特性。幸运的是,你不需要理解这些复杂性就可以使用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。