目录
[0.1 概述&序言](#0.1 概述&序言)
[1.2 测试代码](#1.2 测试代码)
[1.3 resize:改长度](#1.3 resize:改长度)
[1.4 reserve:预留空间(重点)](#1.4 reserve:预留空间(重点))
[1.5 clear 与 empty](#1.5 clear 与 empty)
[1.6 shrink_to_fit:缩容(了解)](#1.6 shrink_to_fit:缩容(了解))
[1.7 插曲:string 到底怎么扩容?(面试加分点)](#1.7 插曲:string 到底怎么扩容?(面试加分点))
[2.1 operator+=(重点中的重点)](#2.1 operator+=(重点中的重点))
[2.2 push_back 与 append(冗余)](#2.2 push_back 与 append(冗余))
[2.3 insert:插入(了解)](#2.3 insert:插入(了解))
[2.4 operator+:拼接(了解)](#2.4 operator+:拼接(了解))
[3.1 assign:整体替换(冗余)](#3.1 assign:整体替换(冗余))
0.1 概述&序言
这里是白杨,上一篇我们认识了 string、学会了构造和三大遍历(没看过的同学请走传送门:C++入门篇(十):string(上)------认识string:构造与三大遍历(一条龙讲透operator\[\]、迭代器、auto、范围for)-CSDN博客)。本篇进入接口大全:
- 容量相关:size、capacity、reserve、resize、clear、empty......(面试高频)
- 增删改查:+=、insert、erase、replace、find、substr......(日常高频)
string 接口上百个,你要的不是全背下来,而是知道每个场景该拿哪个。老规矩,表格里用 (重点)/(了解)/(冗余) 标注优先级。探本溯源,学有所得,让我们开始吧。
一、容量相关接口:
1.1先搞清楚三个概念
先说结论:
size是"当前装了有多少个字符"(有效长度),capacity是"底层空间一共能装多少",length是size的历史别名。
| 函数 | 功能 | 说明 |
|---|---|---|
| size()(重点) | 返回有效字符个数(不含'\0') | 日常最常用 |
| ength() | 与size()完全相同 | 历史遗留别名,统一用size() |
| capacity() | 返回底层空间总大小 | 扩容用 |
| max_size()(了解) | 理论上能容纳的最大长度 | 受系统限制,几乎用不到 |
1.2 测试代码
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("hello bit");
cout << "size: " << s.size() << endl; // 9
cout << "length: " << s.length() << endl; // 9
cout << "capacity: " << s.capacity() << endl; // 15(不同编译器可能不同)
cout << "empty: " << s.empty() << endl; // 0(非空)
return 0;
}
打印结果(实测,VS x64):
size: 9
length: 9
capacity: 15
empty: 0

疑问解答:为什么size是9,capacity却是15?
因为编译器开空间时会在你要的长度基础上留余量,避免每次都精确扩容。具体留下多少,不同编译器策略不同------后面 1.7 节细聊。
1.3 resize:改长度
先说结论:
resize(n)把有效字符个数改为n:n变大了就在末尾补字符(resize(n)补'\0',resize(n,c)补字符c);n变小了就截断。增多时可能扩容,减少时容量不变。
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("hello");
cout << s.size() << " " << s.capacity() << endl; // 5 15
s.resize(8, 'a'); // 增多:末尾补3个'a'
cout << s << " size=" << s.size() << endl; // helloaaa size=8
s.resize(3); // 减少:截断,只留前3个
cout << s << " size=" << s.size() << " capacity=" << s.capacity() << endl;
// hel size=3 capacity=15(容量不变)
s.resize(11);
cout << s << " " << "size=" << s.size() << " " << "capacity:" << s.capacity() << endl;
//未初始化的补\0;
return 0;
}
打印结果(实测,VS x64):
5 15
helloaaa size=8
hel size=3 capacity=15

调试可以观察到:n大了就在末尾补:\0.

1.4 reserve:预留空间(重点)
先说结论:
reserve(n)请求把扩容量至少扩容到n,但不改变有效字符个数、不修改内容。n小于当前容量时:一般什么都不发生(不缩容)。
**什么时候用?**如果能预估字符串最终会很长(比如拼一个几万字的字符的文件内容),先reserve把空间一次性留够,避免+=过程中反复扩容搬动数据,效率更高。
cpp
string s("hello");
cout << s.capacity() << endl; // 15
s.reserve(100); // 预留100个字符的空间
cout << s.capacity() << endl; // 请求容量至少100;编译器会酌情多分配内存。旧版VS得到105,新版VS得到111
cout << s << endl; // hello(内容不变)
cout << s.size() << endl; // 5(长度不变)
s.reserve(10); // 参数小于当前容量
cout << s.capacity() << endl; // 111(什么都不做,不缩容)
打印结果(实测,VS x64):
15 111 hello 5 111

一个小插曲: VS 的**reserve()**语义是"请求容量至少100;编译器会酌情多分配内存。",所以 5 个字符的字符串 reserve(100) 后容量是 111;而 g++ 严格按参数来,直接就是 100。这不是 bug,标准只要求"容量不小于 n",两者都合规------面试被问到 VS/g++ 的 reserve 差异,这就是标准答案。
一张表分清 reserve 和 resize(新手最易混的两个接口):
| 对比 | resize(n,c) | reserve(n) |
|---|---|---|
| 作用对象 | 有效字符个数(size) | 底层容量(capacity) |
| 内容 | 会改变(截断/补字符) | 不改变 |
| 谁在用 | 想要指定长度 | 想要预留空间 |
| 参数小于当前 | 截断到n | 什么都不做(不缩容) |
1.5 clear 与 empty
先说结论:
clear只清内容不清容量;empty等价于size()==0。
| 函数 | 功能 | 注意 |
|---|---|---|
| **clear()(**重点) | 清空有效字符,变成空串 | 只清理内容不清理底层空间(capacity不变) |
| empty()(重点) | 判断是否为空 | 等价于size()==0 |
cpp
string s("123456");
cout << s.capacity() << endl; // 15
s.clear();
cout << s.size() << endl; // 0(内容清空了)
cout << s.capacity() << endl; // 15(空间还在)
cout << s.empty() << endl; // 1(空了)

1.6 shrink_to_fit:缩容(了解)
先说结论:
shrink_to_fit请求把容量缩容到和size一样大。注意是"请求"------是否真缩容由编译器决定。
cpp
string s("hello world");
s.reserve(100);
cout << s.capacity() << endl; // 111(VS:100 + 当前长度11)
s.shrink_to_fit();
cout << s.capacity() << endl; // 15(白杨实测:真的缩了)

1.7 插曲:string 到底怎么扩容?(面试加分点)
容量接口学完了,最后聊个有意思的------我写了个小程序,往 string 里疯狂 push_back 200 个字符,每次容量变化就打印出来:
cpp
string s("a");
int old = 0;
for (int i = 0; i < 200; ++i)
{
s.push_back('b');
if (old != s.capacity())
cout << "capacity -> " << s.capacity() << endl;
old = s.capacity();
}
打印结果(实测,VS x64):
capacity -> 15
capacity -> 31
capacity -> 47
capacity -> 70
capacity -> 105
capacity -> 157
结论:VS下第一次2倍、之后1.5倍扩容。原因:VS的string有"小字符缓冲区"(本文第八节详细讲),第一次扩容是从缓冲区搬到堆上,需要 2 倍;之后就按 1.5 倍了。而 g++ 一直是标准的 2 倍扩容(15 → 30 → 60 → 120)。
小贴士:这段代码只是核心片段,你可以自己套一个int main() 跑跑看
面试能说出"不同编译器扩容策略不同、VS 先 2 倍再 1.5 倍、g++ 2 倍",直接加分。
二、增加数据的接口
2.1 operator+=(重点中的重点)
一句话记忆: 尾插全用 +=,字符、字符串、对象通吃。
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("hello");
s += '!'; // 加字符
cout << s << endl; // hello!
s += " world"; // 加C字符串
cout << s << endl; // hello! world
s += " C++"; // 再加字符串
cout << s << endl; // hello! world C++
return 0;
}
打印结果:
hello!
hello! world
hello! world C++

它有三种重载,知道长什么样就行:+= const string&、+= const char*、+= char。
2.2 push_back 与 append(冗余)
一句话记忆: 有 += 在,这俩基本退休。
- 什么时候别用 : 日常尾插------push_back(c) ==s += c,append(str) ==s += str,风格统一用 +=
- 什么时候用: 基本没有。append 的截子串追加等冷门重载,遇到查文档即可
- 面试问到: "push_back 和 append 都能被 += 替代,工作中统一用 += 保持风格一致。"
2.3 insert:插入(了解)
一句话记忆: 往指定位置下标之前插内容。
什么时候别用: 需要频繁插入时------每次插入都要把后面的字符整体后移,O(n),性能灾难。
什么时候用: 偶尔插一两个字符,影响可以忽略。
cpp
string s("my name is pengyuyan");
s.insert(0, "jj");//jjmy name is pengyuyan
cout << s << endl;
s.insert(2, 2, 'x');//jjxxmy name is pengyuyan
cout << s << endl;
运行结果:

2.4 operator+:拼接(了解)
一句话记忆: 也能拼接,但传值返回(内部要深拷贝),效率不如 +=,能少用就少用。
对比一下就知道取舍:2.1 的s += " world" 是原地追加、不开新对象;而 s + " world" 要构造临时对象再拷贝,多一次深拷贝。日常拼接无脑 +=,operator+ 只在"拼接结果要赋给新对象"的初始化场景顺手用用。
它为什么被重载成全局函数?为了兼容两种情况:
cpp
string s1("hello");
string s2 = s1 + "world"; // 成员函数能处理:s1.operator+("world")
string s3 = "world" + s1; // 成员函数处理不了:"world" 不是对象,必须全局重载
三、修改数据的接口
3.1 assign:整体替换(冗余)
一句话记忆: 整体换新,是**=**的复杂版。
什么时候别用: 能用**s = "xxx"**的地方,就别用 assign。
什么时候用: 用 n 个字符 c 整体填充**(assign(5, 'c'))**这类 = 表达不了的场景,偶尔会碰上。
cpp
string s("Hello world");
s.assign("Ciallo world");
cout << s << endl; // Ciallo world
s.assign(5, 'c');
cout << s << endl; // ccccc
打印结果:
Ciallo world
ccccc

3.2replace:局部替代(了解)
一句话记忆 :assign是整体换新,replace是对某个区间动手。
什么时候别用:高频替换------要移动字符,效率不高。
什么时候用 :偶尔改一处(比如replace(6,5,"C++")把"world"换成"C++")。
cpp
string s("hello world");
s.replace(6, 5, "C++");
cout << s << endl; // hello C++
s.replace(0, 1, 2, 'A');
cout << s << endl; // AAello C++
打印结果:
hello C++
AAello C++

四、删除数据接口
4.1erase:删除(了解)
一句话记忆:按位置或区间删字符,省略参数等于clear()。
三种常用形式:
- erase(pos,len);从pos删len个字符(len缺省或为npos则删到末尾)
- erase(p):删迭代器p指向的一个字符
- erase(first,last):删区间[first,last)
什么时候别用:频繁中间删除(同样要移动数据)。
什么时候用:偶尔删一段,比手写循环省事。
cpp
string s("123456789");
s.erase(3, 3);
cout << s << endl; // 123789
s.erase(s.begin() + 1);
cout << s << endl; // 13789
打印结果:
123789
13789

4.2pop_back:尾删(了解)
**一句话记忆:**删掉最后一个字符。
cpp
string s("hello world");
s.pop_back();
cout << s << endl; // hell
打印结果:
hello worl

五、查找与截取的接口
5.1find与rfind(重点)
一句话记忆:find从pos往后找,rfind从pos往前找;找到返回下标,找不到返回npos。
判断"有没有找到"永远是!=string::npos。find的目标可以是子串、c字符串或单字串。
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("hello world hello");
size_t pos = s.find("world");
if (pos != string::npos)
cout << "\"world\" 在下标 " << pos << " 处" << endl; // 6
cout << "find(\"xyz\") = " << s.find("xyz") << endl; // 找不到:npos
cout << "rfind(\"hello\") = " << s.rfind("hello") << endl; // 12(从后往前找)
return 0;
}
打印结果:
"world :在下标6处
find("xyz") = 18446744073709551615
rfind("hello") = 12

**高频用法:循环查找所有出现位置。**每次从"上次找到位置+1"继续找,直到npos;
cpp
string s("aa bb cc aa");
size_t pos = s.find("aa");
while (pos != string::npos)
{
cout << "找到 aa,位置 " << pos << endl;
pos = s.find("aa", pos + 1); // 从下一个位置继续找
}
打印结果:
找到aa,位置0
找到aa,位置9

5.2find_first_of:查找字符集合(了解)
一句话记忆 :find找的是"完整目标",find_first_of找的是"目标里**任意一个字符"**首次出现的位置。
cpp
string s("hello world");
cout << s.find_first_of("world") << endl; // 2:'l' 第一次出现
打印结果:
2
5.3substr:截取子串(重点)
一句话记忆:从pos截取len个字符,len省略就截到结尾。find定位、substr取货------双子星。
cpp
string s("hello world");
cout << s.substr(6, 5) << endl; // world
cout << s.substr(6) << endl; // world(省略len:截到结尾)
打印结果:
world
world

六、比较运算符重载(重点)
一句话记忆:像数字一样直接比大小,按ASCLL表逐字符比较。
什么时候用:字符串排序、判断相等、字典序比较。有了它,判断相等再也不用手写循环或 strcmp。
cs
string s1("apple"), s2("banana");
cout << (s1 < s2) << endl; // 1:'a' < 'b'
cout << (s1 == s2) << endl; // 0
打印结果:
1
0

(顺带一提:string 还有 swap 交换内容的接口,下篇讲模拟实现时会重点登场,这里先留个印象。)
七、输入输出接口(重点)
7.1流重载<<与>>
一句话记忆:cin>>遇到空格/换行就停,cout<<直接输出.
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
cin >> s; // 输入 hello world
cout << s << endl; // 只输出 hello,空格后面没读进来
return 0;
}
打印结果(实测,VS x64):
输入:hello world
输出:hello

7.2getilne:读一整行(重点)
一句话记忆: cin>>以空格/换行终止,getline以换行终止------读一整行(含空格)必须用getline。
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string line;
cout << "请输入一行(含空格):";
getline(cin, line);
cout << "你输入的是:" << line << endl;
return 0;
}
打印结果(实测,VS x64):
请输入一行(含空格):hello world C++
你输入的是:hello world C++

7.3一道错题引入:cin和getline混用的坑
下面代码想先读一个数字,在读一行字符串。输入2026回车后,程序直接跳过了输入行------为什么?
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
int n;
cin >> n; // 输入 2026 回车
string line;
getline(cin, line); // 还没等你输入,就结束了!
cout << "数字是: " << n << " | 行内容是: [" << line << "]" << endl;
return 0;
}
打印结果(实测,VS x64):
数字是2026|行的内容是【】
原因与原理:cin>>读取完字符后,你敲的回车**('\n')还残留在缓冲区里** ;紧接着getline读到这个残留的'\n',认为"这一行读完了"(空行),根本不等你输入。
解决办法: 在 getline 之前用cin.ignore() 吃掉残留的换行符;或者读数字也用 getline 读进来再转数字。这是新手必踩的坑,面试也偶尔考。

八、选读:string的底层结构(面试向)
本节不影响你使用 string------赶时间的同学可以跳过,面试前回来看看。VS/g++ 的实现差异、SSO 这些都是面试题常客。
现说结论:
不同编译器对string的实现完全不同:VS是"小缓冲区+堆空间"(32位下28字节,64位下32字节),旧版g++是"写时靠背"(32位下只有4字节)。面试时能说出差异就是亮点。
VS下:
- 内部有个联合体:字符串长度小于16时用内部固定字符数组存(不用申请堆,快);大于16才去开空间
- 再加一个size_t存长度、一个size_t存容量
- 32位下16+4+4=28字节;64位下16+8+8=32字节
旧版g++下(32位平台验证):
- string 只有 4 字节,就一根指针,指向堆上的一块空间
- 堆空间里存了:空间总大小、有效长度、引用计数、字符串本体
- 这就是写时拷贝(实现细节下篇会简单介绍,了解即可)
cpp
cout << sizeof(std::string) << endl;
打印结果(实测,VS x64 Release):
32

九、重点接口速查表(建议收藏👍👍👍)
把本文所以(重点)接口收进一张表,以后忘了直接翻这里:
| 分类 | 接口 | 一句话说明 |
|---|---|---|
| 容量 | size() | 有效字符个数(不含'/0') |
| 容量 | capac() | 底层空间总大小 |
| 容量 | empty() | 是否为空串(等价size()==0) |
| 容量 | clear() | 清空内容,不清容量 |
| 容量 | reserve(n) | 预留空间,只扩容,不改变长度 |
| 容量 | resize(n,c) | 改长度,多的用c补充 |
| 增 | s+=str/s+=c | 尾插字符串/字符(首先) |
| 删 | erase(pos,len) | 从pos往后找,找不到返回npos |
| 查 | find(str,pos) | 从pos往后找,找不到返回npos |
| 查 | rfind(str,pos) | 从pos往后找 |
| 截取 | substr(pos,len) | 从pos截取len个字符,len可省略 |
| 转换 | c_str() | 转换C语言字符串,配合C库函数 |
| 比较 | == != < <= > >= | 比窘 |
| 输入 | getline(cin,s) | 读一整行(含空格) |
口诀:查找用find,截用substr,尾插全用+=,读行必须用getline
冷门接口补充: data() 与 c_str() 完全等价,统一用 c_str;max_size()、shrink_to_fit()、find_first_of() 等用到了查文档即可。
- C++ Reference
https://legacy.cplusplus.com/reference/string/
十、最终总结
- 三个概念:size 有效长度 / length 是别名 / capacity 底层容量
- resize 改长度:增多补字符(可能扩容)、减少只截断(容量不变)
- reserve 预留空间:只扩容量不改长度;参数小了不缩容;VS 是"长度之外再加 n",g++ 严格按 n
- 扩容策略:VS 先 2 倍后 1.5 倍;g++ 2 倍(面试加分点)
- clear 只清内容不清容量;shrink_to_fit 请求缩容(非强制)
- 增删改查的"正道":尾插 +=、删除 erase、查找 find/rfind(找不到返回 npos)、截取 substr
- 接口取舍:push_back/append/assign 有更顺手的替代(+= 和 =),属冗余;
- insert/erase/replace 要移动数据、效率低,慎用
- 读一行用 getline(注意和 cin >> 混用会踩残留换行符的坑);拼接少用 operator+;比较直接比大小
- VS 28/32 字节(SSO)/ 旧版 g++ 4 字节(写时拷贝)------实现差异巨大,以实测为准
好了本期内容就到这里。下篇进入实战与原理:六道 OJ 真题(含上篇预告的两道面试题)+ 手写模拟实现 string(浅拷贝、深拷贝、现代写法),把面试官最爱考的那道题彻底拿下。
如果对你有帮助,不要忘记点赞三连一波哦,我是白杨,我们下期见。