std::string完全指南:C++字符串类型详解

在字面量这节中中,我们介绍了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。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"