章节目录

std::string简介

本节阅读量:

在之前的课程中,我们介绍了 C 风格字符串:

1
2
3
4
5
6
7
#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 类型,它位于 <string> 头文件中。

我们可以像创建其他对象一样创建 std::string 类型的对象:

1
2
3
4
5
6
7
8
#include <string> // 引入 std::string

int main()
{
    std::string name {}; // 空字符串

    return 0;
}

就像普通变量一样,你可以初始化 std::string 对象,也可以给它赋值:

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

int main()
{
    std::string name { "Fly" }; // 初始化变量 name,值为 "Fly"
    name = "John";               // 给变量 name 赋值 "John"

    return 0;
}

需要注意的是,字符串也可以由数字字符组成:

1
std::string myID{ "45" }; // "45" 和数字 45 是不同的东西!

在字符串形式中,数字被视为文本而不是数值,因此它们不能像数字一样被运算(例如不能相乘)。C++ 不会在字符串和整型或浮点数之间自动转换,反之亦然(当然,也有一些方法可以进行转换,我们会在后续课程中介绍)。


用 std::cout 输出字符串

使用 std::cout 可以像预期的那样输出 std::string 对象:

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

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

    return 0;
}

这将输出:

1
My name is: Fly

空字符串则不会输出任何内容:

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

int main()
{
    std::string empty{ };
    std::cout << '[' << empty << ']';

    return 0;
}

输出为:

1
[]

std::string 可以处理不同长度的字符串

大多数类型分配给它们的字节数是固定的。例如,如果系统上的 int 是 4 字节,那么每个 int 对象都会占用 4 字节的内存。如果你想保存一个需要超过 4 字节存储的整数值……那就只能使用其他类型。

std::string 最厉害的一点就是可以保存不同长度的字符串:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <string>

int main()
{
    std::string name { "Fly" }; // 用 "Fly" 初始化变量 name
    std::cout << name << '\n';

    name = "Jason";              // 将 name 改成一个更长的字符串
    std::cout << name << '\n';

    name = "Jay";                // 再把 name 改成一个更短的字符串
    std::cout << name << '\n';

    return 0;
}

输出为:

1
2
3
Fly
Jason
Jay

在上面的示例中,name 最初用字符串 “Fly” 初始化,它包含 4 个字符(3 个显式字符加上一个 null 结尾符)。接下来我们把 name 改成了更长的字符串,然后再改成更短的字符串。std::string 处理这些情况毫无问题!

这也是 std::string 强大的原因之一。


使用 std::cin 读取字符串输入

将 std::string 与 std::cin 一起使用时可能会出现一些意外情况!来看下面这个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#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;
}

下面是这个程序的一次运行结果:

1
2
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() 函数。getline() 需要两个参数:第一个是 std::cin,第二个是字符串变量。

下面是使用 std::getline() 改写的上面的程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#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;
}

现在,我们的程序就能如预期般工作了:

1
2
3
Enter your full name: John Doe
Enter your favorite color: blue
Your name is John Doe and your favorite color is blue

什么是 std::ws?

在——浮点数相关——课程中,我们讨论过输出操纵器,它可以改变输出的呈现方式。在那节课中,我们使用了输出操纵器函数 std::setprecision() 来改变 std::cout 显示的精度位数。

C++ 也支持输入操纵器,它可以改变接收输入的方式。std::ws 输入操纵器告诉 std::cin 在提取之前忽略所有前导空白字符。前导空白是指出现在字符串开头的任何空白字符(空格、制表符、换行符)。

我们来看看为什么这有用。考虑下面这个程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#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;
}

下面是该程序的一次输出:

1
2
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 中时,它发现 std::cin 中已经有一个 “\n” 在等待,于是认为我们输入了一个空字符串!

我们可以对上面的程序做一点修改,使用 std::ws 输入操纵器来告诉 std::getline() 忽略任何前导空白字符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#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;
}

现在,这个程序就能按预期工作了:

1
2
3
Pick 1 or 2: 2
Now enter your name: Alex
Hello, Alex, you picked 2

std::string 的长度

如果我们想知道一个 std::string 中有多少个字符,可以查询 std::string 对象的长度。它的语法与前面所见的稍有不同,但非常简单:

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

int main()
{
    std::string name{ "Alex" };
    std::cout << name << " has " << name.length() << " characters\n";

    return 0;
}

输出为:

1
Alex has 4 characters

虽然从 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 进行转换,以避免编译器对有符号/无符号转换给出警告:

1
int length { static_cast<int>(name.length()) };

在 C++20 中,你还可以使用 std::ssize() 函数以有符号整数值的形式获取 std::string 的长度:

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

int main()
{
    std::string name{ "Alex" };
    std::cout << name << " has " << std::ssize(name) << " characters\n";

    return 0;
}

初始化 std::string 的开销很大

每次初始化 std::string 时,都会复制一份用于初始化它的字符串。复制字符串的代价是很大的,因此我们应当尽量减少复制的次数。


不要按值传递 std::string

当按值将 std::string 传给函数时,必须实例化 std::string 类型的函数参数,并用传入的实参来初始化它。这会带来昂贵的复制。我们将在——std::string_view 简介——中讨论如何避免这种复制(使用 std::string_view)。


返回 std::string

当函数按值向调用方返回时,返回值通常会从函数复制回调用方。因此,你可能会认为不应按值返回 std::string,因为这样做会返回一份昂贵的 std::string 副本。

然而,作为经验法则,当 return 语句的表达式结果属于下列情况之一时,按值返回 std::string 是可以的:

  1. 类型为 std::string 的局部变量。
  2. 由函数调用或运算符按值返回的 std::string。
  3. 作为 return 语句的一部分所创建的 std::string。

除此之外的大多数场景下,不要按值返回 std::string,因为这样会产生昂贵的副本。


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; // 方便地使用 s 后缀

    std::cout << "foo\n";   // C 风格字符串字面值
    std::cout << "goo\n"s;  // s 后缀表示这是 std::string 字面值

    return 0;
}

你可能不会经常用到 std::string 字面值(因为用 C 风格字符串初始化 std::string 对象是完全没问题的),但我们会在后面的课程中看到一些场景(涉及类型推导),在这些场景下使用 std::string 字面值而非 C 风格字符串,会让事情更简单。


constexpr 字符串

如果尝试定义 constexpr std::string,编译器可能会报错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#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 用于像基本的字符串输入输出这样的简单任务。我们鼓励你现在就开始尝试字符串,后续我们还会介绍更多字符串相关的功能。


5.7 Constexpr和consteval函数

上一节

5.9 std::string_view简介

下一节