Skip to content

22.3 - std-string 的长度和容量

Key Takeaway

一旦创建了字符串,知道它们的长度通常是很有用的。我们还将讨论将std::string转换回C风格字符串的各种方法,这样你就可以在将其用在期望char*类型字符串的函数中使用它们。

字符串的长度

字符串的长度非常简单——即字符串中字符的个数。有两个函数可以用于确定字符串的长度:

size_type string::length() const
size_type string::size() const

  • 这两个函数都可以返回字符串中字符的个数,包括结束符。

例子:

1
2
std::string s { "012345678" };
std::cout << s.length() << '\n';

输出:

1
9

尽管使用 length() 判断字符串中是否有字符也是可行的,但是最佳的方法是使用 empty() 函数:

bool string::empty() const

  • 如果字符串中没有字符,则返回true,否则返回false

例子:

1
2
3
4
std::string string1 { "Not Empty" };
std::cout << (string1.empty() ? "true" : "false") << '\n';
std::string string2; // empty
std::cout << (string2.empty() ? "true" : "false")  << '\n';

输出:

1
2
false
true

还有一个与大小相关的函数,虽然可能永远都用不到,但为了完整,我们决定还是将其列在这里:

size_type string::max_size() const

  • 返回字符串允许的最大字符数;
  • 根据操作系统和系统架构的不同,该值会有所不同。

例子:

1
2
std::string s { "MyString" };
std::cout << s.max_size() << '\n';

输出:

1
4294967294

字符串的容量

字符串的容量反映了该字符串分配多少内存来保存其内容。该值用字符串表示,不包括终止符。例如,容量为8的字符串可以容纳8个字符。

size_type string::capacity() const

  • 返回字符串在不重新分配的情况下可以容纳的字符数。

例子:

1
2
3
std::string s { "01234567" };
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

输出:

1
2
Length: 8
Capacity: 15

注意,该字符串的容量大于其长度!虽然我们的字符串的长度是8,但该字符串实际上分配了足够存放15个字符的内存!为什么要这样做?

这里要注意的是,如果用户希望在字符串中放入的字符多于字符串的容量,则字符串需要重新分配更大的容量。例如,如果字符串的长度和容量都为8,那么向字符串中添加任何字符都将强制重新分配。通过使容量大于实际字符串,这为用户提供了一些缓冲空间,以便在需要进行重新分配之前能够存放一些额外的字符。

事实证明,再分配有以下缺陷:

首先,重新分配字符串开销较大。首先,必须分配新的内存。然后必须将字符串中的每个字符复制到新的内存中。如果字符串很大,这可能需要很长时间。最后,旧的内存必须被释放。如果你正在进行多次重新分配,这个过程会极大地拖累程序性能。

其次,每当一个字符串被重新分配时,该字符串的内容将放置到一个新的内存地址。这意味着对字符串的所有引用、指针和迭代器都会无效!

注意,分配给字符串的容量并不总是大于长度。考虑下面的程序:

1
2
3
std::string s { "0123456789abcde" };
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

输出结果:

1
2
Length: 15
Capacity: 15

(实际结果根据编译器的不同也可能会不同)

接下来,在字符串中添加一个字符并观察其容量的变化:

1
2
3
4
5
6
7
8
std::string s("0123456789abcde");
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

// Now add a new character
s += "f";
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

输出结果:

1
2
3
4
Length: 15
Capacity: 15
Length: 16
Capacity: 31

void string::reserve()
void string::reserve(size_type unSize)

  • 此函数的第二种形式将字符串的容量设置为至少unSize(可以更大)。注意,这可能需要进行重新分配。
  • 如果调用函数的第一种形式,或者调用第二个味道时unSize小于当前容量,函数将尝试缩小容量以匹配长度。根据执行情况,可能会忽略这个缩小容量的要求。

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
std::string s { "01234567" };
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

s.reserve(200);
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

s.reserve();
std::cout << "Length: " << s.length() << '\n';
std::cout << "Capacity: " << s.capacity() << '\n';

输出:

1
2
3
4
5
6
Length: 8
Capacity: 15
Length: 8
Capacity: 207
Length: 8
Capacity: 207

这个例子展示了两个有趣的现象。首先,虽然我们要求的容量是200,但实际上我们得到的容量是207。容量总是保证至少与你的请求一样大,但也可能更大。然后我们请求容量更改以适应字符串。该请求被忽略,容量并没有改变。

如果你提前知道你将通过大量的字符串操作来构造一个很长字符串(这会增加字符串的大小),则应该在开始时就预留足够的容量来避免重新分配:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>
#include <cstdlib> // for rand() and srand()
#include <ctime> // for time()

int main()
{
    std::srand(std::time(nullptr)); // seed random number generator

    std::string s{}; // length 0
    s.reserve(64); // reserve 64 characters

    // Fill string up with random lower case characters
    for (int count{ 0 }; count < 64; ++count)
        s += 'a' + std::rand() % 26;

    std::cout << s;
}

这个程序的结果每次都会改变,其中一次的结果为:

1
wzpzujwuaokbakgijqdawvzjqlgcipiiuuxhyfkdppxpyycvytvyxwqsbtielxpy

为了避免重新分配,这里我们先设置一次容量,然后填充字符串。当通过连接构造长字符串时,其性能差异是非常明显的。