C++入门篇(十一):string(中)——容量与增删改查:一篇吃透所有常用接口(万字详解)

目录

[0.1 概述&序言](#0.1 概述&序言)

一、容量相关接口:

1.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:整体替换(冗余))

3.2replace:局部替代(了解)

四、删除数据接口

4.1erase:删除(了解)

4.2pop_back:尾删(了解)

五、查找与截取的接口

5.1find与rfind(重点)

5.2find_first_of:查找字符集合(了解)

5.3substr:截取子串(重点)

六、比较运算符重载(重点)

七、输入输出接口(重点)

7.1流重载<<与>>

7.2getilne:读一整行(重点)

7.3一道错题引入:cin和getline混用的坑

八、选读:string的底层结构(面试向)

九、重点接口速查表(建议收藏👍👍👍)

十、最终总结


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/

十、最终总结

  1. 三个概念:size 有效长度 / length 是别名 / capacity 底层容量
  2. resize 改长度:增多补字符(可能扩容)、减少只截断(容量不变)
  3. reserve 预留空间:只扩容量不改长度;参数小了不缩容;VS 是"长度之外再加 n",g++ 严格按 n
  4. 扩容策略:VS 先 2 倍后 1.5 倍;g++ 2 倍(面试加分点)
  5. clear 只清内容不清容量;shrink_to_fit 请求缩容(非强制)
  6. 增删改查的"正道":尾插 +=、删除 erase、查找 find/rfind(找不到返回 npos)、截取 substr
  7. 接口取舍:push_back/append/assign 有更顺手的替代(+= 和 =),属冗余;
  8. insert/erase/replace 要移动数据、效率低,慎用
  9. 读一行用 getline(注意和 cin >> 混用会踩残留换行符的坑);拼接少用 operator+;比较直接比大小
  10. VS 28/32 字节(SSO)/ 旧版 g++ 4 字节(写时拷贝)------实现差异巨大,以实测为准

好了本期内容就到这里。下篇进入实战与原理:六道 OJ 真题(含上篇预告的两道面试题)+ 手写模拟实现 string(浅拷贝、深拷贝、现代写法),把面试官最爱考的那道题彻底拿下。

如果对你有帮助,不要忘记点赞三连一波哦,我是白杨,我们下期见。

相关推荐
老歌老听老掉牙1 小时前
旅行商问题:从数学模型到 Python 实战
开发语言·python
CC数分1 小时前
数据可视化入门:用Python把一堆数字变成一张图
开发语言·python·信息可视化·数据分析
java_nnnn1 小时前
JavaEE进阶-JavaScript初识
开发语言·前端·javascript·java-ee·ecmascript
SimonKing1 小时前
SSE、WebSocket 连接丢 Redis 里?那可踩大坑了!
java·后端·程序员
倔强的石头1061 小时前
【Linux指南】动静态库系列(九):动态库如何进入进程地址空间:从磁盘 .so 到共享内存映射
java·linux·服务器
朔北之忘 Clancy1 小时前
2024 CSP-J 第二轮真题解析
c++·算法·青少年编程·noip·csp·信奥赛·noi
Lazionr1 小时前
set和map的使用——从基础接口到实际应用
c++
YYYing.1 小时前
【设计模式系列 (五) 】原型模式
开发语言·后端·设计模式·原型模式·c/c++
TeamDev1 小时前
JxBrowser 9.5.2 版本发布啦!
java·前端·windows·chromium·混合应用·jxbrowser·浏览器控件