string学习总结
string 学习总结
std::string 是 C++ 标准库中最常用的字符串类,本文从零到熟练梳理它的所有核心用法。
1. string 是什么
- 存储连续的字符序列,支持随机访问(按下标 O(1) 访问)。
- 底层是动态字符数组:在堆上分配一块连续内存,字符紧密排列,末尾自动加
\0(但\0不是 string 的一部分)。 - 自动扩容:字符增多时自动重新分配更大内存(O(n) 均摊插入)。
- 属于"字符串类",头文件
<string>。
#include <string>
string s; // 空 string
对比记忆:string 是"字符序列容器",内存连续、访问快、支持丰富字符串操作;vector<char> 是"字符动态数组",功能更底层但缺少字符串专属接口(如 find、substr);C 风格字符串 char[] 是"字符数组",无自动管理,容易越界。
2. 定义与初始化
#include <string>
using namespace std;
// 1. 空 string
string s1;
// 2. 字面量初始化
string s2 = "hello";
string s3("world");
// 3. 填充初始化
string s4(5, 'a'); // "aaaaa",5 个 'a'
string s5(3, 'x'); // "xxx"
// 4. 从数组/指针构造
char arr[] = "abc";
string s6(arr); // "abc"
string s7(arr, 2); // "ab",取前 2 个字符
// 5. 从迭代器构造
string s8(s2.begin(), s2.begin() + 3); // "hel"
// 6. 拷贝构造
string s9(s2); // "hello"
// 7. 移动构造(C++11)
string s10(std::move(s2)); // s2 被掏空,s10 = "hello"
// 8. 初始化列表(C++11)
string s11{'h', 'e', 'l', 'l', 'o'}; // "hello"
关键概念:
- size / length:当前字符个数(两者等价,
size()更常用) - capacity:已分配内存能容纳的最大字符个数(含
\0) c_str():返回以\0结尾的 C 风格字符串指针reserve()只改 capacity,不改 size;resize()两者都改
3. 核心操作一览
| 操作 | 写法 | 说明 | 复杂度 |
|---|---|---|---|
| 尾部追加 | s += str |
在末尾添加字符串 | O(n) 均摊 |
| 尾部追加 | s.append(str) |
在末尾添加字符串 | O(n) 均摊 |
| 尾部追加 | s.push_back(c) |
在末尾添加单个字符 | O(1) 均摊 |
| 指定位置插入 | s.insert(pos, str) |
在下标 pos 处插入字符串,插入会使插入位置后的元素向后搬移 | O(n) |
| 迭代器插入 | s.insert(it, str) |
在迭代器 it 前插入 | O(n) |
| 删除 | s.erase(pos, len) |
从下标 pos 开始删除 len 个字符 | O(n) |
| 删除 | s.erase(it) |
删除迭代器 it 处字符 | O(n) |
| 按下标访问 | s[i] |
不检查越界 | O(1) |
| 按下标访问 | s.at(i) |
越界抛 out_of_range 异常 |
O(1) |
| 首字符 | s.front() |
返回第一个字符的引用 | O(1) |
| 末字符 | s.back() |
返回最后一个字符的引用 | O(1) |
| C 风格字符串 | s.c_str() |
返回以 \0 结尾的 const char* |
O(1) |
| 底层指针 | s.data() |
返回底层数组指针(C++11 后保证有 \0) |
O(1) |
| 大小 | s.size() |
当前字符个数 | O(1) |
| 容量 | s.capacity() |
当前分配的内存容量 | O(1) |
| 预留空间 | s.reserve(n) |
至少分配 n 个字符的空间 | O(n) 最坏 |
| 调整大小 | s.resize(n) |
改变 size,多删少补(补 \0) |
O(n) 最坏 |
| 收缩空间 | s.shrink_to_fit() |
使 capacity == size(非强制) | O(n) |
| 清空 | s.clear() |
删除所有字符,size=0,capacity 不变 | O(n) |
| 判空 | s.empty() |
size == 0 时返回 true | O(1) |
| 交换 | s.swap(other) |
与另一个 string 交换内容 O(1) | O(1) |
| 子串 | s.substr(pos, len) |
返回从 pos 开始长度为 len 的子串 | O(n) |
| 查找 | s.find(str) |
从头查找 str,返回下标或 npos |
O(n*m) |
| 查找 | s.rfind(str) |
从尾查找 str,返回下标或 npos |
O(n*m) |
| 比较 | s1.compare(s2) |
返回 0(相等)、正数(s1>s2)、负数(s1<s2) | O(n) |
| 比较 | s1 == s2 |
直接比较内容 | O(n) |
| 追加 | s += c |
追加单个字符 | O(1) 均摊 |
| 追加 | s += {a, b, c} |
追加初始化列表(C++11) | O(n) |
| 赋值 | s1 = s2 |
拷贝赋值 | O(n) |
| 赋值 | s1 = std::move(s2) |
移动赋值(C++11) | O(1) |
| 流输出 | cout << s |
输出整个字符串 | O(n) |
| 流输入 | cin >> s |
读入一个单词(遇空格停止) | O(n) |
| 整行读入 | getline(cin, s) |
读入一整行(遇换行停止) | O(n) |
| 替换 | s.replace(pos, len, content) |
替换字符串内容(删除旧范围并写入),如果字符不足,则向后扩展,如果此时用copy迭代器算法实现,则不会进行扩容,这是因为copy不会删除,不会插入,不会自动扩容 | O(n) |
4. 增删查改实战
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main() {
string s;
// ===== 插入/追加 =====
s = "hello";
s += " world"; // "hello world"
s.append("!"); // "hello world!"
s.push_back('?'); // "hello world!?"
s.insert(5, ","); // "hello, world!?"
// ===== 访问 =====
cout << s[0] << endl; // h
cout << s.at(5) << endl; // ,
cout << s.front() << endl; // h
cout << s.back() << endl; // ?
cout << s.c_str() << endl; // hello, world!?
// ===== 查找 =====
size_t pos = s.find("world");
if (pos != string::npos) {
cout << "Found at index: " << pos << endl; // 7
}
// rfind 从后往前找
size_t rpos = s.rfind('o');
cout << "Last 'o' at: " << rpos << endl; // 8
// ===== 子串 =====
string sub = s.substr(7, 5); // "world"
cout << sub << endl;
// ===== 遍历 =====
// 1. 下标遍历
for (int i = 0; i < s.size(); i++) {
cout << s[i] << " ";
}
cout << endl;
// 2. 迭代器遍历
for (auto it = s.begin(); it != s.end(); ++it) {
cout << *it << " ";
}
cout << endl;
// 3. 范围 for(C++11)
for (auto& c : s) {
cout << c << " ";
}
cout << endl;
// ===== 删除 =====
s.erase(5, 2); // 删除下标 5 开始的 2 个字符
s.pop_back(); // 删除末尾字符
s.erase(s.begin()); // 删除首字符
s.clear(); // 清空所有字符
cout << s.size() << endl; // 0
return 0;
}
5. reserve() vs resize()(重点)
这是面试高频考点,两者本质不同:
string s;
s.reserve(10);
// size = 0, capacity = 10, 没有任何字符
// s[0] 是未定义行为!
s.resize(10);
// size = 10, capacity >= 10, 10 个 '\0' 字符
cout << s.size(); // 合法,输出 10
reserve(n) |
resize(n) |
|
|---|---|---|
| 改变 size | 不改 | 改 |
| 改变 capacity | 改 | 可能改 |
| 字符个数 | 不变 | 多删少补(补 \0) |
| 用途 | 预分配内存,避免频繁扩容 | 改变字符个数 |
坑:reserve 后直接用下标访问未初始化的内存 → 未定义行为!
string s;
s.reserve(100);
for (int i = 0; i < 100; i++) {
s[i] = 'a' + i; // 未定义行为!size 还是 0
}
// 正确做法:
for (int i = 0; i < 100; i++) {
s += 'a' + i; // 每次 += 才会真正构造字符
}
// 或者先 resize
s.resize(100);
for (int i = 0; i < 100; i++) {
s[i] = 'a' + i; // 合法,字符已构造
}
6. 内存布局:size 与 capacity 的关系
string 扩容时会分配一块更大的内存,把旧字符拷贝过去,再释放旧内存:
size: 0 1 2 3 4 5 6 7 8 ...
capacity: 0 1 2 4 4 8 8 8 8 16 ...
- 每次追加导致 size > capacity 时触发扩容。
- 扩容倍数由实现决定,通常是 2 倍(GCC)或 1.5 倍(MSVC)。
- 扩容代价大:O(n) 拷贝 + 内存分配,所以批量拼接前用
reserve()预分配。 - SBO(Small Buffer Optimization):许多实现对短字符串(通常 ≤ 15~22 字符)直接存储在栈上,避免堆分配,这是
string比vector<char>快的一个原因。
string s;
s.reserve(1000); // 预分配,后续拼接不触发扩容
for (int i = 0; i < 1000; i++) {
s += 'a'; // 全部 O(1) 均摊,无重新分配
}
7. 迭代器的特点
- string 的迭代器是随机访问迭代器:支持
++、--、it + n、it - n、it1 - it2、下标比较。 - 这是最强大的迭代器类型,可以配合任何 STL 算法。
迭代器失效规则(重点):
| 操作 | 失效情况 |
|---|---|
+= / append / push_back |
可能导致全部迭代器失效(扩容时) |
pop_back |
尾迭代器失效 |
insert / erase |
插入点及之后的所有迭代器失效 |
reserve / resize / shrink_to_fit |
全部迭代器失效 |
clear |
全部迭代器失效 |
// 危险:边遍历边 insert 可能死循环
for (auto it = s.begin(); it != s.end(); ++it) {
if (*it == 'a') {
s.insert(it, 'X'); // it 失效!后续 ++it 未定义行为
}
}
// 正确做法:insert 返回新的有效迭代器
for (auto it = s.begin(); it != s.end(); ) {
if (*it == 'a') {
it = s.insert(it, 'X'); // 用返回值更新
++it; // 跳过新插入的字符
++it; // 跳过原字符
} else {
++it;
}
}
// erase 的正确用法:erase 返回下一个有效迭代器
for (auto it = s.begin(); it != s.end(); ) {
if (*it == 'a') {
it = s.erase(it); // 返回下一个
} else {
++it;
}
}
8. string 与 vector、char[] 的对比
string |
vector<char> |
char[] |
|
|---|---|---|---|
| 底层 | 动态字符数组 | 动态字符数组 | 固定字符数组 |
| 内存连续 | 是 | 是 | 是 |
| 自动管理内存 | 是 | 是 | 否 |
| 字符串操作 | 丰富(find, substr, +) | 无(需手动实现) | 无(需手动实现) |
\0 处理 |
自动管理,不算入 size | 需手动管理 | 以 \0 结尾 |
| 随机访问 | O(1) | O(1) | O(1) |
| 尾部增删 | O(1) 均摊 | O(1) 均摊 | 不支持 |
| 中间增删 | O(n) | O(n) | 不支持 |
| SBO 优化 | 是(短字符串栈上存储) | 否 | 栈上分配 |
| 与 C API 交互 | s.c_str() |
需手动加 \0 |
直接使用 |
选择建议:
- 默认选择 string:任何涉及字符串的场景都用 string。
- 需要底层字符操作(如自定义协议解析)→
vector<char>。 - 已有 C API 需要兼容 →
string的c_str()即可,无需降级到char[]。 - 永远不要用裸
char[]:除非在极端性能敏感的嵌入式场景。
9. 字符串拼接的多种方式
#include <string>
#include <sstream>
#include <iostream>
using namespace std;
int main() {
// 方式 1:+ 运算符(产生临时对象,多次拼接效率低)
string s1 = "hello" + string(" ") + "world";
// 方式 2:+= 追加(原地修改,效率高)
string s2;
s2 += "hello";
s2 += " ";
s2 += "world";
// 方式 3:append(与 += 等价,可指定子串)
string s3;
s3.append("hello");
s3.append("world", 0, 5); // 取 "world" 的前 5 个
// 方式 4:stringstream(多类型混合拼接最方便)
stringstream ss;
ss << "Score: " << 95 << ", Name: " << "Alice";
string s4 = ss.str();
// 方式 5:format(C++20,推荐)
// string s5 = format("Score: {}, Name: {}", 95, "Alice");
cout << s1 << endl; // hello world
cout << s2 << endl; // hello world
cout << s3 << endl; // helloworld
cout << s4 << endl; // Score: 95, Name: Alice
return 0;
}
经验法则:少量拼接用 +/+=,大量或混合类型拼接用 stringstream,C++20 优先用 format。
10. 常见坑汇总
reserve后用下标访问:size 仍是 0,字符未构造,未定义行为。+=导致迭代器失效:扩容时所有迭代器失效,不能在循环中依赖旧迭代器。insert/erase后使用旧迭代器:插入/删除点及之后的迭代器全部失效。- 在循环中
+=可能无限扩容:循环前用reserve预分配。 string的size()返回size_t(无符号):与int比较可能产生意外结果:
string s = "hello";
for (int i = 0; i < s.size(); i++) { // 警告:有符号/无符号比较
cout << s[i];
}
// 正确:用 size_t 或 auto
for (size_t i = 0; i < s.size(); i++) {
cout << s[i];
}
clear不释放内存:size 变 0 但 capacity 不变。想释放内存用shrink_to_fit()或 swap 技巧:
string s(1000000, 'x');
s.clear(); // size=0, capacity 还是 1000000,内存未释放
// 释放内存的技巧
string().swap(s); // 临时空 string 和 s 交换,s 的内存被释放
// 或 C++11
s.shrink_to_fit();
cin >> s只读一个单词:遇空格/换行停止。读整行用getline(cin, s)。getline与cin >>混用时的换行符残留:
int n;
string s;
cin >> n; // 读入数字,换行符留在缓冲区
getline(cin, s); // 读到空行!
// 解决:cin >> n; cin.ignore(); getline(cin, s);
c_str()返回的指针在 string 修改后失效:不要缓存c_str()的返回值。string不是以\0为结尾的:s.size()不包含\0,但c_str()会自动追加\0。- 误以为
string是线程安全的:多线程读写同一个 string 需要加锁。
11. C++11 新特性
11.1 移动语义
string s1 = "hello";
string s2 = std::move(s1); // s1 被掏空,零拷贝
// s1 现处于有效但未指定状态,可以继续赋值使用
s1 = "world"; // 合法
11.2 右值引用与移动赋值
string s;
s = "hello" + string(" ") + string("world"); // 临时对象被移动,避免拷贝
11.3 初始化列表
string s{'h', 'e', 'l', 'l', 'o'}; // "hello"
11.4 原始字符串字面量
// 普通字符串:需要转义
string path = "C:\\Users\\test\\file.txt";
// 原始字符串:所见即所得(C++11)
string raw = R"(C:\Users\test\file.txt)"; // 无需转义
// 含 ) 的内容
string raw2 = R"(hello )world )"; // 有问题!
// 自定义分隔符
string raw3 = R"++(hello )world )++"; // 完美
11.5 constexpr(C++11/14/17 逐步增强)
constexpr string_view sv = "hello"; // C++17 string_view
// string 本身 C++20 才支持 constexpr 构造
11.6 string_view(C++17)
#include <string_view>
using namespace std;
// string_view 是 string 的只读视图,零拷贝
string_view sv = "hello world";
cout << sv.substr(0, 5) << endl; // "hello"
// sv 不拥有内存,底层字符串必须存活
// 与函数参数配合,避免拷贝
void print(string_view sv) {
cout << sv << endl;
}
print("hello"); // 隐式转换
print(string("abc")); // 零拷贝
12. 实战:字符串处理(string 最经典的场景)
#include <iostream>
#include <string>
#include <sstream>
#include <algorithm>
#include <vector>
using namespace std;
// 分割字符串
vector<string> split(const string& s, char delim) {
vector<string> result;
stringstream ss(s);
string token;
while (getline(ss, token, delim)) {
result.push_back(token);
}
return result;
}
// 去除首尾空格
string trim(const string& s) {
size_t start = s.find_first_not_of(" \t\n\r");
size_t end = s.find_last_not_of(" \t\n\r");
return (start == string::npos) ? "" : s.substr(start, end - start + 1);
}
// 转小写
string toLower(string s) {
transform(s.begin(), s.end(), s.begin(), ::tolower);
return s;
}
int main() {
string input = " Hello, World! ";
// 去空格
string trimmed = trim(input);
cout << "[" << trimmed << "]" << endl; // [Hello, World!]
// 转小写
string lower = toLower(trimmed);
cout << lower << endl; // hello, world!
// 分割
string csv = "apple,banana,cherry";
vector<string> fruits = split(csv, ',');
for (const auto& f : fruits) {
cout << f << " "; // apple banana cherry
}
cout << endl;
// 替换
string text = "hello world hello";
size_t pos = text.find("world");
if (pos != string::npos) {
text.replace(pos, 5, "C++");
}
cout << text << endl; // hello C++ hello
// 反转
string rev = "abcdef";
reverse(rev.begin(), rev.end());
cout << rev << endl; // fedcba
// 字符统计
string s = "hello";
int freq[26] = {};
for (char c : s) {
freq[c - 'a']++;
}
for (int i = 0; i < 26; i++) {
if (freq[i] > 0) {
cout << char('a' + i) << ": " << freq[i] << " ";
}
}
cout << endl; // e: 1 h: 1 l: 2 o: 1
return 0;
}
13. 实战场景
给定一个需求:需要处理用户输入的文本,支持查找、替换、分割、拼接等操作。
方法一:使用std::string建模,内存连续、缓存友好、提供丰富的字符串操作接口(find、substr、replace 等),默认选择。
方法二:使用std::vector<char>建模,更底层的字符序列操作,适合需要自定义协议解析或与底层 C API 交互的场景。
方法三:使用char[]/char*建模,C 风格字符串,无自动内存管理,容易越界,仅在极端性能敏感的嵌入式场景使用。
总结:
1、默认选择 string,内存连续缓存命中率最高,提供丰富的字符串操作接口
2、需要底层字符操作或与 C API 交互时选择 vector
3、永远不要用裸 char[],除非在极端性能敏感的嵌入式场景
4、string 的迭代器是随机访问迭代器,支持 it+n 这种跳跃操作
5、批量拼接前用 reserve 预分配内存,避免频繁扩容的拷贝开销
6、大量或混合类型拼接用 stringstream,C++20 优先用 format
7、string_view(C++17)是只读视图,零拷贝,适合作为函数参数传递字符串,避免不必要的 string 拷贝
8、string 的 size() 返回 size_t(无符号类型),与 int 比较时注意符号问题
9、getline 与 cin >> 混用时注意换行符残留问题,用 cin.ignore() 解决

浙公网安备 33010602011771号