🔥 本文定位 :本文围绕
std::string的"使用层"展开,从 C 风格字符串的痛点出发,系统梳理构造、访问、遍历、容量管理、追加、查找、截取、输入输出,以及字符串算法题中最常见的双指针、计数和模拟加法。💡 学习目标 :不仅会调用接口,还要真正理解
size与capacity的区别、reserve与resize为什么不能混用、范围for何时必须加引用、npos为什么要用size_type接收、getline为什么会读到空行,以及修改字符串后迭代器为什么可能失效。📌 阅读说明:本文以 C++11 及之后的主流写法为背景,并标注 C++17、C++20 中容易混淆的变化。不同标准库可以采用不同的容量增长策略和小字符串优化,文章不会把某个编译器版本的对象布局当成语言规则。

文章目录
- [一、为什么有了字符数组还要学习 string](#一、为什么有了字符数组还要学习 string)
- [二、先认识 std::string 的基本模型](#二、先认识 std::string 的基本模型)
- [三、auto 与范围 for:遍历 string 的基础工具](#三、auto 与范围 for:遍历 string 的基础工具)
- [四、string 的构造、赋值与字符访问](#四、string 的构造、赋值与字符访问)
- [五、size、capacity、reserve 与 resize](#五、size、capacity、reserve 与 resize)
- 六、四种常见遍历方式
- 七、追加、插入、删除与替换
- [八、find、rfind、substr 与 npos](#八、find、rfind、substr 与 npos)
- [九、输入输出:cin、getline 与缓冲区陷阱](#九、输入输出:cin、getline 与缓冲区陷阱)
- 十、五类经典字符串算法
- 十一、性能、失效规则与实现边界
- 十二、高频面试题与排查清单
- 总结
一、为什么有了字符数组还要学习 string
1.1 C 风格字符串是什么
C 风格字符串通常由一段连续字符和结尾的空字符 '\0' 组成:
cpp
char text[] = "hello";
数组中实际包含 6 个字符:
text
'h' 'e' 'l' 'l' 'o' '\0'
<cstring> 提供了 strlen、strcpy、strcat、strcmp 等函数,但"字符数据"和"管理数据的操作"彼此分离。调用者还要自己处理:
- 目标缓冲区是否足够;
- 是否为结尾的
'\0'留出空间; - 源地址和目标地址是否重叠;
- 动态空间何时扩容、由谁释放;
- 空指针、越界和异常路径如何处理。
这些问题并非不能解决,但很容易让业务逻辑被内存管理细节淹没。
1.2 std::string 带来了什么
std::string 把字符序列和资源管理封装在一个对象中:
cpp
#include <iostream>
#include <string>
int main() {
std::string name = "Alice";
name += " Chen";
std::cout << name << '\n';
std::cout << name.size() << '\n';
}
它负责维护有效字符数、可用空间和对象生命周期,并提供迭代器、比较、查找、截取等统一接口。

需要注意,std::string 并没有让所有错误自动消失。下标越界、错误保存迭代器、误解编码、反复触发扩容等问题仍需要程序员负责。
1.3 std::string 不是"只能装普通文本"
std::string 本质上是字符序列,可以包含位于中间的 '\0':
cpp
std::string data{"A\0B", 3};
std::cout << data.size() << '\n'; // 3
如果把 data.c_str() 交给只认 '\0' 结尾的旧式 C 接口,对方可能只看到 "A"。因此:
- 在 C++ 对象内部,长度由
size()记录; - 在传统 C 字符串接口中,
'\0'常被当作终点; - 文本、字节序列和编码是三个相关但不同的问题。
二、先认识 std::string 的基本模型
2.1 类型与头文件
使用字符串类需要包含:
cpp
#include <string>
std::string 是 std::basic_string<char> 的别名。日常代码中直接使用 std::string 即可:
cpp
std::string message = "hello";
示例代码为了简洁有时会写 using namespace std;,但大型项目和头文件中更建议保留 std::,避免名字冲突。
2.2 一个稳定的心智模型
可以先把一个字符串对象抽象成三项信息:
text
string 对象
├─ 字符序列:当前保存的有效字符
├─ size:有效字符数量
└─ capacity:无需重新分配即可容纳的字符数量
并始终满足:
text
0 <= size() <= capacity()
标准库会保证用于 C 字符串访问的末尾终止字符,因此:
cpp
std::string s = "hello";
const char* p = s.c_str();
// p[0] ... p[s.size() - 1] 是有效字符
// p[s.size()] 是 '\0'
但不要把这个抽象误读成固定的对象内存布局。标准没有规定 std::string 对象必须包含"一个指针、一个长度、一个容量",也没有规定对象必须占多少字节。
2.3 小字符串优化不是语言保证
主流实现常采用 Small String Optimization,简称 SSO:短字符串直接放在对象内部,较长字符串才申请动态空间。它能减少短文本的堆分配,但属于实现策略:
- 阈值可能因标准库、位数、编译选项而不同;
sizeof(std::string)不能推导出统一的 SSO 长度;- 代码不能依赖某个版本"长度小于 16 一定不分配";
- 性能结论应通过目标平台上的基准和分析工具验证。
三、auto 与范围 for:遍历 string 的基础工具
3.1 auto 会推导类型,不会让 C++ 变成动态类型
auto 要求编译器在编译期从初始化表达式推导类型:
cpp
int number = 10;
auto copy = number; // int
auto pointer = &number; // int*
auto& alias = number; // int&
几个关键规则:
- 普通
auto通常会丢掉顶层const和引用; - 需要引用语义时要显式写
auto&或const auto&; - 变量通常必须有初始化器;
- 一个声明中的多个声明符必须得到相容的推导结果;
auto不是运行时"万能类型"。
cpp
const int value = 42;
auto a = value; // int
const auto b = value; // const int
const auto& c = value; // const int&
早期教程常说"auto 不能作为函数参数"。这对 C++11 到 C++17 的普通函数参数成立;从 C++20 开始,void print(auto value) 是合法的简写函数模板。
3.2 范围 for 的值、引用与只读引用
遍历字符串时,循环变量的写法决定了是复制、修改还是只读访问:
cpp
std::string text = "hello";
for (char ch : text) {
// ch 是副本,修改 ch 不影响 text
}
for (char& ch : text) {
// ch 引用原字符,可以修改 text
ch = static_cast<char>(std::toupper(
static_cast<unsigned char>(ch)
));
}
for (const char& ch : text) {
// 只读引用,不复制元素
}
对 char 而言复制成本很低,for (char ch : text) 完全合理;当元素类型较大,或需要保留引用语义时,再使用 auto& / const auto&。
3.3 为什么 toupper 前常转换成 unsigned char
<cctype> 中的 std::toupper、std::tolower、std::isalnum 等函数只接受 EOF 或能表示为 unsigned char 的值。若普通 char 是有符号类型,直接传入负值可能产生未定义行为。
更稳妥的写法是:
cpp
char upper_ascii(char ch) {
const auto byte = static_cast<unsigned char>(ch);
return static_cast<char>(std::toupper(byte));
}
这仍然只是按当前 C locale 处理单字节字符,不能把 UTF-8 中文字符当成一个 char 来做大小写或字符分类。
四、string 的构造、赋值与字符访问
4.1 常见构造方式
cpp
std::string s1; // 空字符串
std::string s2 = "hello"; // 从 C 字符串构造
std::string s3("hello", 3); // "hel"
std::string s4(5, '*'); // "*****"
std::string s5 = s2; // 拷贝构造
std::string s6 = std::move(s5); // 移动构造
从 const char* 构造时,传入的指针必须满足该重载的前置条件。不要把空指针当成空字符串:
cpp
const char* p = nullptr;
// std::string bad(p); // 错误:不是合法的 C 字符串
std::string safe = p ? p : "";
4.2 operator\[\] 与 at
operator[] 适合已经证明下标合法的热路径,at 会检查边界:
cpp
std::string word = "cat";
word[0] = 'C';
std::cout << word.at(1) << '\n';
访问有效字符时,应把合法下标理解为:
text
0 到 size() - 1
不要用"末尾存在 '\0'"作为越界访问普通字符的理由。需要边界检查时使用 at,越界会抛出 std::out_of_range。
4.3 front、back、data 与 c_str
cpp
std::string path = "demo.txt";
char first = path.front();
char last = path.back();
const char* c_text = path.c_str();
对空字符串调用 front() 或 back() 不满足前置条件,调用前先判断 empty()。
从 C++17 开始,非 const std::string 的 data() 可以返回可修改的字符指针,但不能借此破坏字符串的不变量,也不能写到 size() 之外:
cpp
std::string text = "abc";
char* raw = text.data();
raw[0] = 'A'; // C++17 起可行
任何可能导致重新分配的操作之后,都要重新取得 data() / c_str() 指针。
五、size、capacity、reserve 与 resize
5.1 size 与 length
size() 和 length() 都返回有效字符数量:
cpp
std::string s = "hello";
std::cout << s.size() << '\n'; // 5
std::cout << s.length() << '\n'; // 5
容器代码更常使用 size(),因为它与 vector、list 等接口一致。
5.2 clear 只清空内容,不负责"归还容量"
cpp
std::string s = "a long text";
const auto old_capacity = s.capacity();
s.clear();
std::cout << s.size() << '\n'; // 0
std::cout << s.empty() << '\n'; // true
clear() 将有效字符数变为 0,但不要依赖它缩小 capacity()。如果确实希望请求收缩,可以调用 shrink_to_fit(),但它本身也是非强制请求。
5.3 reserve 只调整容量预期
当大致知道最终长度时,可以提前预留:
cpp
std::string result;
result.reserve(1024);
for (int i = 0; i < 1000; ++i) {
result += 'x';
}
reserve 不会凭空增加有效字符:
cpp
std::string s = "abc";
s.reserve(100);
// s 仍然是 "abc"
// s.size() 仍然是 3
// s.capacity() 至少能够满足请求
5.4 resize 会改变有效字符数量
resize 直接改变 size():
cpp
std::string s = "abc";
s.resize(5, 'x'); // "abcxx"
s.resize(2); // "ab"
扩展时:
resize(n, ch)用ch填充新增字符;resize(n)对新增字符进行值初始化,char得到'\0';- 若容量不足,可能重新分配。
缩小时,后面的字符被移除,但容量通常不会因此自动缩小。

5.5 reserve 与 resize 的一句话区别
text
reserve:我以后可能要放这么多,先准备空间
resize :我现在就有这么多个有效字符
如果只是为了减少扩容次数,不要用 resize 代替 reserve,否则会真的向字符串中加入字符。
六、四种常见遍历方式
6.1 下标遍历
cpp
for (std::string::size_type i = 0; i < s.size(); ++i) {
std::cout << s[i] << ' ';
}
适合需要位置、相邻元素或双指针的场景。
6.2 正向迭代器
cpp
for (auto it = s.begin(); it != s.end(); ++it) {
std::cout << *it << ' ';
}
迭代器是容器与算法之间的通用接口。很多标准算法只需要一对迭代器:
cpp
#include <algorithm>
std::reverse(s.begin(), s.end());
6.3 反向迭代器
cpp
for (auto it = s.rbegin(); it != s.rend(); ++it) {
std::cout << *it << ' ';
}
6.4 范围 for
cpp
for (char ch : s) {
std::cout << ch << ' ';
}
它最简洁,但默认不暴露下标。需要位置时可以使用下标循环,或在 C++20 中结合 std::views 设计更高层的遍历逻辑。

6.5 size_type 与"倒序循环"陷阱
size() 返回无符号的 size_type。下面的倒序循环会出错:
cpp
// 错误示范:i 永远不会小于 0
for (std::size_t i = s.size() - 1; i >= 0; --i) {
}
可以改成:
cpp
for (std::size_t i = s.size(); i-- > 0;) {
std::cout << s[i];
}
或者直接使用反向迭代器。
七、追加、插入、删除与替换
7.1 尾部追加
cpp
std::string s = "hello";
s.push_back('!');
s.append(" C++");
s += " string";
常见选择:
- 追加一个字符:
push_back(ch)或+= ch; - 追加整个字符串:
+= other; - 追加区间、重复字符或部分字符串:
append的重载更丰富。
7.2 insert、erase 与 replace
cpp
std::string s = "I C++";
s.insert(2, "love "); // "I love C++"
s.erase(2, 5); // "I C++"
s.replace(2, 3, "like C++"); // "I like C++"
这些函数有很多重载。阅读代码时要特别确认参数到底表示:
- 位置和数量;
- 迭代器区间;
- C 字符串;
- 另一个
string的子区间。
7.3 为什么不必机械地排斥 operator+
早期教程常说"operator+ 一定产生深拷贝,所以尽量不用"。现代 C++ 有移动语义、返回值优化和实现优化,不能用一句固定结论概括所有表达式。
真正值得警惕的是循环中的链式累加:
cpp
std::string result;
for (const auto& part : parts) {
result = result + part; // 可能反复创建临时对象
}
更直接的写法是:
cpp
std::string result;
result.reserve(total_size);
for (const auto& part : parts) {
result += part;
}
先写清楚,再通过性能分析确认是否值得优化。
八、find、rfind、substr 与 npos
8.1 find 从前向后查找
cpp
std::string path = "/usr/local/bin";
const auto pos = path.find("local");
if (pos != std::string::npos) {
std::cout << pos << '\n';
}
若没有找到,返回 std::string::npos。
8.2 为什么不要用 int 接收 find
find 的返回类型是 std::string::size_type。推荐使用 auto 或明确的 size_type:
cpp
std::string::size_type pos = text.find(':');
npos 通常是 size_type 能表示的最大值。把它粗暴地放入 int 可能截断,也会混淆"合法位置"和"未找到"。
8.3 rfind 从后向前查找
cpp
std::string filename = "archive.tar.gz";
const auto dot = filename.rfind('.');
if (dot != std::string::npos) {
std::cout << filename.substr(dot + 1) << '\n'; // gz
}
8.4 substr 截取子串
cpp
std::string s = "hello world";
std::string first = s.substr(0, 5); // hello
std::string rest = s.substr(6); // world
参数含义是 substr(pos, count),第二个参数是"数量",不是结束下标。

8.5 最后一个单词长度的边界
若输入保证末尾没有空格,可以写:
cpp
const auto pos = line.rfind(' ');
const auto length = line.size() - pos - 1;
这里 npos 恰好会通过无符号回绕得到正确结果,但这种技巧可读性不高,而且遇到尾随空格就失败。更稳健的写法是主动跳过尾部空格:
cpp
std::size_t end = line.size();
while (end > 0 && line[end - 1] == ' ') {
--end;
}
std::size_t begin = end;
while (begin > 0 && line[begin - 1] != ' ') {
--begin;
}
std::cout << end - begin << '\n';
九、输入输出:cin、getline 与缓冲区陷阱
9.1 operator>> 读取一个"单词"
cpp
std::string word;
std::cin >> word;
默认情况下,它会跳过前导空白,并在下一个空白字符处停止。
9.2 getline 读取整行
cpp
std::string line;
std::getline(std::cin, line);
getline 会读取到分隔符,默认分隔符为换行符,并把分隔符从输入流中取走,但不放进字符串。
9.3 cin >> 后紧接 getline 为什么常读到空行
cpp
int age = 0;
std::string name;
std::cin >> age;
std::getline(std::cin, name); // 可能立即读到上一行留下的 '\n'
一种常见解决方式:
cpp
#include <limits>
std::cin >> age;
std::cin.ignore(
std::numeric_limits<std::streamsize>::max(),
'\n'
);
std::getline(std::cin, name);
如果输入格式允许,也可以统一使用 getline,再对整行做解析。
十、五类经典字符串算法

10.1 只反转字母:相向双指针
目标是只交换字母,其他字符保持原位置:
cpp
#include <cctype>
#include <string>
#include <utility>
bool is_letter(char ch) {
return std::isalpha(
static_cast<unsigned char>(ch)
) != 0;
}
std::string reverse_only_letters(std::string s) {
if (s.empty()) {
return s;
}
std::size_t left = 0;
std::size_t right = s.size() - 1;
while (left < right) {
while (left < right && !is_letter(s[left])) {
++left;
}
while (left < right && !is_letter(s[right])) {
--right;
}
if (left < right) {
std::swap(s[left], s[right]);
++left;
--right;
}
}
return s;
}
核心不是记代码,而是识别模式:
text
左指针找可交换元素
右指针找可交换元素
交换
继续向中间收缩
10.2 第一个只出现一次的字符:计数两遍
如果题目明确字符范围是 ASCII,可以使用固定数组:
cpp
#include <array>
#include <string>
int first_unique_char(const std::string& s) {
std::array<int, 256> count{};
for (unsigned char ch : s) {
++count[ch];
}
for (std::size_t i = 0; i < s.size(); ++i) {
const auto ch = static_cast<unsigned char>(s[i]);
if (count[ch] == 1) {
return static_cast<int>(i);
}
}
return -1;
}
第一遍统计,第二遍按原顺序找答案。若字符集不受限,应改用适合题意的映射结构;若输入是 UTF-8,"一个用户可见字符"也不等于一个字节。
10.3 验证回文:过滤规则 + 双指针
cpp
#include <cctype>
#include <string>
bool is_palindrome(const std::string& s) {
std::size_t left = 0;
std::size_t right = s.size();
while (left < right) {
while (left < right &&
!std::isalnum(static_cast<unsigned char>(s[left]))) {
++left;
}
while (left < right &&
!std::isalnum(static_cast<unsigned char>(s[right - 1]))) {
--right;
}
if (left < right) {
const auto a = std::tolower(
static_cast<unsigned char>(s[left])
);
const auto b = std::tolower(
static_cast<unsigned char>(s[right - 1])
);
if (a != b) {
return false;
}
++left;
--right;
}
}
return true;
}
这里把 right 设计成"开区间终点",空字符串也不需要先计算 size() - 1,可以避免无符号下溢。
10.4 大整数相加:从低位向高位模拟
cpp
#include <algorithm>
#include <string>
std::string add_strings(
const std::string& lhs,
const std::string& rhs
) {
std::size_t i = lhs.size();
std::size_t j = rhs.size();
int carry = 0;
std::string result;
result.reserve(std::max(lhs.size(), rhs.size()) + 1);
while (i > 0 || j > 0 || carry != 0) {
int sum = carry;
if (i > 0) {
sum += lhs[--i] - '0';
}
if (j > 0) {
sum += rhs[--j] - '0';
}
result.push_back(static_cast<char>('0' + sum % 10));
carry = sum / 10;
}
std::reverse(result.begin(), result.end());
return result;
}
为什么先尾插再反转?
push_back在尾部追加,通常更符合连续容器的使用方式;- 每次在开头
insert都要搬移已有字符; - 预留最大长度加 1,可以减少扩容。
10.5 翻转单词:先明确"空格语义"
"翻转句子中的单词"至少有三种不同题意:
- 单词顺序不变,只翻转每个单词内部字符;
- 单词内部不变,只反转单词顺序;
- 同时规范多余空格。
写代码前应先确认:
- 分隔符是否只有普通空格;
- 是否存在连续空格、前导空格和尾随空格;
- 是否要原地修改;
- 是否要支持 Unicode 空白字符。
算法错误经常不是循环写错,而是没有先定义输入模型。
十一、性能、失效规则与实现边界
11.1 容量增长策略是实现细节
连续追加时,标准库通常按某种增长策略扩容,以获得摊销效率。但增长倍率、SSO 阈值和对象大小都不由 C++ 标准固定。
可移植代码应依赖接口契约:
size()是有效字符数;capacity()是当前无需重新分配可容纳的数量;reserve可以提前提出容量需求;- 不要依赖某次追加后的精确
capacity()。
11.2 重新分配会让旧地址失效
cpp
std::string s = "hello";
const char* old = s.c_str();
s += std::string(1000, 'x');
// old 可能已经失效,必须重新获取
const char* current = s.c_str();
同理,保存的指针、引用和迭代器也可能失效。稳妥原则是:
在执行可能修改字符串结构的操作后,不要继续使用先前取得的元素地址或迭代器,除非对应接口明确保证仍然有效。
11.3 时间复杂度只是第一层
常见操作的直觉成本:
| 操作 | 常见复杂度直觉 | 备注 |
|---|---|---|
size()、empty() |
常数时间 | 长度由对象维护 |
operator[] |
常数时间 | 不做常规边界检查 |
尾部 push_back |
摊销常数时间 | 扩容时需要搬移字符 |
中间 insert/erase |
线性时间 | 后续字符通常需要移动 |
find |
与文本和模式长度相关 | 不承诺某个固定高级算法 |
substr |
与结果长度相关 | 会构造新字符串 |
优化时还要考虑分配次数、缓存局部性、临时对象和真实数据规模。
11.4 编码边界
std::string 管理的是 char 序列,不自动理解"字符"的语言学含义。UTF-8 中一个汉字通常占多个字节,因此:
cpp
std::string text = "中国";
std::cout << text.size() << '\n';
输出往往不是 2。按字节 reverse 还可能破坏编码。处理 Unicode 码点、字素簇、规范化和本地化比较时,应使用明确的 Unicode 方案,而不是把 char 当作用户可见字符。
十二、高频面试题与排查清单
12.1 常见误区
- 认为
reserve(100)会让size()变成 100; - 认为
resize(100)只是准备空间,不会加入字符; - 用
int接收find返回值; - 直接判断
find(...) >= 0; - 对空字符串调用
front()/back(); - 在修改字符串后继续使用旧的
c_str()指针; - 倒序循环使用无符号变量并判断
i >= 0; - 把
clear()当作释放全部容量; - 把某个编译器的 SSO 阈值当作标准保证;
- 使用
cin >> s读取带空格的整行; - 直接把可能为负的
char传给std::tolower; - 把字节数量
size()当成 Unicode 字符数量。
12.2 高频面试题
问题 1:std::string 与 C 字符串的核心区别是什么?
std::string 是负责资源生命周期和字符序列操作的对象;C 字符串通常依赖 '\0' 作为结束标记,容量和生命周期由调用者额外管理。二者可以互操作,但长度模型和接口约定不同。
问题 2:size 与 capacity 有什么区别?
size 是当前有效字符数,capacity 是当前无需重新分配可以容纳的字符数。始终有 size() <= capacity()。
问题 3:reserve 与 resize 有什么区别?
reserve 面向容量,不改变有效字符数;resize 直接改变有效字符数,扩展时会加入字符,容量不足时还可能触发重新分配。
问题 4:clear 会释放字符串内存吗?
clear 保证清空有效字符,不保证缩小容量。shrink_to_fit 也只是收缩请求,不能作为强制释放容量的可移植保证。
问题 5:为什么 find 的结果要与 npos 比较?
查找失败时返回 std::string::npos,它属于 size_type。使用 auto 接收并与 npos 比较,可以避免有符号/无符号和截断问题。
问题 6:operator\[\] 与 at 有何区别?
operator[] 面向已证明下标合法的访问;at 会做边界检查,并在越界时抛出 std::out_of_range。
问题 7:c_str 返回的指针能保存多久?
它与字符串对象当前内部存储关联。对象销毁后失效;许多非 const 修改操作也可能使它失效。修改后应重新调用 c_str()。
问题 8:std::string 一定在堆上保存字符吗?
不一定。主流实现常对短字符串使用 SSO,直接把字符放进对象内部。具体布局和阈值属于实现细节。
问题 9:现代 std::string 采用写时拷贝吗?
不能这样概括。历史实现曾采用 Copy-on-Write,但现代 C++ 的迭代器、并发和复杂度要求使主流标准库采用独立值语义配合 SSO、移动语义等策略。代码不应依赖写时拷贝。
问题 10:范围 for 中 auto、auto&、const auto& 如何选择?
按值用于只需要副本且元素便宜的情况;auto& 用于修改原元素;const auto& 用于只读且避免复制。对 char 而言按值遍历很常见。
问题 11:为什么大整数相加适合先 push_back 再 reverse?
计算从最低位开始,自然产生逆序结果。尾插比反复头插更适合连续存储,最后一次线性反转即可。
问题 12:std::string::size() 是字符数吗?
它是 char 元素数量,也就是代码单元/字节序列长度。对 UTF-8 文本,它通常不等于 Unicode 码点数,更不等于用户看到的字素数量。
12.3 排查清单
遇到字符串问题时,可以按下面的顺序检查:
text
输入是单词、整行还是任意字节序列?
↓
下标是否始终落在 [0, size())?
↓
是否发生 size() - 1 的无符号下溢?
↓
find 失败是否与 npos 正确比较?
↓
reserve 与 resize 是否用反?
↓
修改后是否还在使用旧指针、引用或迭代器?
↓
循环中是否反复创建临时字符串或触发扩容?
↓
字符分类函数是否安全接收 unsigned char?
↓
题目说的"字符"究竟是字节、码点还是字素?
12.4 一组可运行的最小练习
cpp
#include <algorithm>
#include <iostream>
#include <string>
int main() {
std::string text;
std::getline(std::cin, text);
std::cout << "size = " << text.size() << '\n';
std::cout << "capacity = " << text.capacity() << '\n';
text.reserve(text.size() + 32);
text += " [C++]";
const auto pos = text.find("C++");
if (pos != std::string::npos) {
std::cout << text.substr(pos, 3) << '\n';
}
std::reverse(text.begin(), text.end());
std::cout << text << '\n';
}
建议依次尝试:
- 把
reserve改成resize,观察内容和size的变化; - 保存
c_str()后追加一个很长的字符串,比较修改前后的地址; - 用
cin >> text替换getline,输入带空格的句子; - 对空字符串故意计算
size() - 1,观察无符号结果; - 在 ASCII 和 UTF-8 中文文本上分别测试
size()和reverse。
12.5 参考资料
- C++ working draft:basic.string
- cppreference:std::basic_string
- cppreference:std::basic_string::reserve
- cppreference:std::basic_string::find
- C++ Core Guidelines:Standard-library interfaces
总结
std::string 的使用主线可以压缩为:
text
C 字符串需要手工维护缓冲区与终止字符
↓
std::string 封装字符序列和资源生命周期
↓
size 描述有效字符,capacity 描述可用空间
↓
reserve 预留容量,resize 改变有效长度
↓
下标、迭代器和范围 for 完成遍历
↓
find/rfind 定位,substr 提取,npos 表示失败
↓
双指针、计数、扫描和进位模拟解决经典题型
↓
修改后重新审视地址、迭代器、复杂度与编码边界
请牢记:
std::string是值类型容器,不是一个自动安全的裸字符指针;size与capacity分工不同,reserve与resize不能互换;- 范围
for是否加引用,取决于要复制、修改还是只读访问; find失败返回npos,应使用匹配的size_type或auto;getline读取整行,operator>>默认按空白切分;- 重新分配后,旧的
data/c_str指针、引用和迭代器可能失效; - 容量增长、对象大小和 SSO 阈值都是实现细节;
- 字符串算法先定义输入模型,再选择双指针、计数或模拟流程;
char序列不等于 Unicode 用户字符序列;- 能正确使用标准
string,是下一篇模拟实现资源管理语义的前提。
如果本文对你有帮助,欢迎点赞、收藏。真正掌握
std::string的标志,不是背下几十个成员函数,而是能在看到一段字符串代码时,立即判断它修改了内容还是容量、会不会重新分配、边界是否安全,以及题目中的"字符"究竟指什么。