
🧑💻博主名称:鱼子星_
✅数据结构专栏:【数据结构】
✅算法竞赛专栏:【算法竞赛】
✅C++系列专栏:【C++从零开始系列】
🎯 前言
在C++编程中,字符串处理是几乎每个程序都会遇到的基础任务。无论是简单的用户输入输出,还是复杂的文本处理、文件操作,字符串都扮演着至关重要的角色。然而,传统的C风格字符串(字符数组和char*指针)在使用上存在诸多不便:内存管理复杂、容易越界、操作函数繁琐且不安全。
C++标准库中的string类正是为了解决这些问题而设计的。作为STL(标准模板库)的重要组成部分,string不仅封装了字符序列,还提供了丰富、安全、高效的成员函数,让字符串操作变得简单直观。
📚 本文内容概览
本文将从零开始,全面系统地讲解C++ string类的使用,涵盖以下核心内容:
- 默认成员函数 - 构造函数、析构函数、赋值运算符重载
- 迭代器 - 正向/反向迭代器、const迭代器、范围for循环
- 容量管理 - size()、capacity()、resize()、reserve()等
- 元素访问 - operator\[\]、at()、front()、back()等
- 修改操作 - 拼接、插入、删除、替换等
- 字符串操作 - 查找、截取、C风格转换等
🎓 学习目标
通过本文的学习,你将能够:
- ✅ 熟练使用
string类的各种构造函数和赋值操作 - ✅ 掌握迭代器的使用,理解范围for循环的底层原理
- ✅ 合理管理字符串的容量和大小,优化程序性能
- ✅ 安全高效地访问和修改字符串内容
- ✅ 使用丰富的字符串操作函数解决实际问题
让我们开始探索C++ string类的强大功能吧!
一. 为什么要用string?
在之前,如果我们需要对字符串进行操作,就需要使用字符数组或者是char类型的指针,先不说频繁的使用指针容易出错,就是其输入输出的格式都有很多条条框框,一不小心就会输出错误。
string就是用来解决字符串操作复杂的问题的。string是C++标准库中封装的一个类,同时也是STL中的一个容器,专门存储字符类型的数据。它的最大的方便处在于,string的大部分操作都被封装成成员函数了,我们可以直接调用,那么接下来就直接进入string的学习吧~

【注】本篇和之后讲解STL容器的文章都会大量的使用C++标准的参考文档,网站链接:C++ Reference
二. string的基本使用
1. 默认成员函数
标准库中,string的默认成员函数被分为了三类,构造函数,析构函数和赋值运算符重载

构造函数(constructor)
string中一共重载了7个构造函数,其使用方式如下:
cpp
string s1;
string s2("change world");
string s3 = s2;
string s4(s2, 0, 6);
string s5("change world", 6);
string s6(10, 'x');

前3个初始化的方式都很普通,主要讲解后3个初始化的参数形式。从下图可以发现 s4 的初始化方式对应图中第3种初始化方式,而它的参数的意义为:从字符串str中的第pos个位置开始,截取len长度的字符串用来初始化 s4

所以这里 s4 的值为change,那函数参数的npos这个缺省值又是什么呢?其实它是string类中的公共静态成员变量 ,而它代表的数值为整型的最大值。如下图所示,或许有的人会有疑问,nops明明是 -1 啊,为什么又变成整型的最大值了?

注意仔细看npos的类型,是无符号整型,而-1的补码全部都是1,所以-1对应的无符号整数就是整型的最大值了。当然,如果使用int类型接收npos的值,那么它就是-1
cpp
//无符号类型接收npos,此时x为整型的最大值
size_t x = string::npos;
//使用整型接收npos,此时y为-1
int y = string::npos;
cout << x << endl;
cout << y << endl;

那么,这里为什么要使用整型的最大值来作缺省参数呢?这是因为,string的底层的编写的人想实现当用户不传递len的数值时,截取的字符串就变成,从str从pos位置开始到结尾的字符串
那么要如何优雅的完成这个功能呢?为len定义一个很大的缺省值,且保证该值一定会比所有的字符串都长。这样就能保证一定会截取到字符串str的结尾。
s5 的初始化方式对应上图中(5)的重载函数,它的函数功能为:从常量字符串 s 中截取前n个字符用来初始化 s5
s6 的初始化方式对应上图(6)的重载函数,它的函数功能为:使用n个由字符c拼接起来的字符串初始化 s6

析构函数(destructor)
析构函数就比较简单,平时也不需要自己调用string的析构函数,编译器会自行调用。

赋值运算符重载(operator=)
赋值运算符重载的函数接口只有3个,但是都很实用,其使用方式如下:
cpp
string s1;
string s2 = "hello world";
//使用string赋值
s1 = s2;
//使用常量字符串赋值
s1 = "change world";
//使用字符赋值
s1 = 'a';

赋值运算符重载的3个使用方式都比较简单直接,直接对照标准库使用即可

📊 默认成员函数总结表格
| 函数类别 | 函数名 | 功能描述 | 常用重载形式 | 使用场景 |
|---|---|---|---|---|
| 构造函数 | string() |
默认构造函数,创建空字符串 | string s1; |
创建空字符串对象 |
string(const char* s) |
用C风格字符串初始化 | string s2("hello"); |
从C字符串创建string | |
string(const string& str) |
拷贝构造函数 | string s3 = s2; |
复制已有string对象 | |
string(const string& str, size_t pos, size_t len = npos) |
从str的pos位置开始截取len长度 | string s4(s2, 0, 6); |
截取子串初始化 | |
string(const char* s, size_t n) |
用s的前n个字符初始化 | string s5("hello", 3); |
截取C字符串前n个字符 | |
string(size_t n, char c) |
用n个字符c初始化 | string s6(5, 'x'); |
创建重复字符的字符串 | |
| 析构函数 | ~string() |
销毁对象,释放内存 | 自动调用 | 对象生命周期结束时自动调用 |
| 赋值运算符 | operator=(const string& str) |
字符串赋值 | s1 = s2; |
string对象间的赋值 |
operator=(const char* s) |
C字符串赋值 | s1 = "hello"; |
用C字符串赋值 | |
operator=(char c) |
字符赋值 | s1 = 'a'; |
用单个字符赋值 |
2. 迭代器
C++中为了减少不同的STL容器的使用的耦合性,引入了一个新的类型:迭代器 。迭代器一般用来遍历STL中的容器,通过解引用迭代器,可以访问容器中的值。所以,我们可以将它看成像指针一样的东西,
迭代器的类型名称为iterator,标准库中为每个STL的容器都封装了它们自己的迭代器,通常定义迭代器的方式为:类名::iterator it = 对象.begin();
这里的begin是什么?不急,下面来看迭代器相关的成员函数。
begin() / end()
begin()用于返回STL容器起始位置的迭代器,end()用于返回STL容器的结束位置的下一个位置的迭代器,其指向位置的抽象示意图如下:

上面说了,迭代器是用来遍历STL容器的,那么设置begin()和end()这两个函数也就很合理了。下面来看如何使用迭代器通过遍历string容器来修改它的值
cpp
string s1 = "123456789";
cout << "s1 = " << s1 << endl;
//begin返回s1起始位置的迭代器
string::iterator it = s1.begin();
//只要到结束位置迭代器的下一个位置,就继续遍历
while (it != s1.end())
{
//通过解引用迭代器修改s1的值
(*it)++;
//将迭代器移动到下一个位置
it++;
}
cout << "s1 = " << s1 << endl;
💻️ 输出
由上不难看到,只需要拿到容器起始位置的迭代器就可以开始遍历容器了,而结束条件就使用容器结束位置的后继位置的迭代器判断即可
那说到这里,迭代器降低STL容器使用的耦合性到底体现在哪了?这里先用后面会讲的 vector 容器做示例,你可以先将vector看成是一个顺序表
cpp
//顺序表
vector<int> v{ 1,2,3,4,5 };
vector<int>::iterator it = v.begin();
while (it != v.end())
{
//修改容器的值
(*it)++;
//打印
cout << (*it) << " ";
it++;
}

可以看到,使用迭代器遍历STL中不同的容器,其使用方式都是大同小异的,就好像在套同一个模板一样,而只要学会了如何使用迭代器遍历一个STL的容器,那你就会遍历所有的STL的容器了。
这会知道迭代器的好了吧~其实,迭代器的方便之处远远不止于此,在后续学习的场景会经常使用到迭代器,以此体现它的强大之处,下面来讲解一个底层使用迭代器来实现的快速遍历STL容器的方式:范围for。而在讲解范围for前,先来认识一个新东西:auto。
认识auto
在C++11中引入了一个新的类型auto,它和其他内置类型不同,auto会自动识别类型并将对象转换为该类型,使用以下示例讲解:
cpp
auto a = 10;
auto d = 3.14;
此时a使用整型10赋值,那么auto会自动识别类型将a转换为int类型,以此类推,d也就被转换成了double类型。

既然知道了auto的自动识别类型的功能,接下来看范围for的遍历方式
cpp
string s1 = "123456789";
for (auto ch : s1)
{
cout << ch << " ";
}
范围for的使用格式如上,它的功能就是可以自动遍历容器。遍历方式为:使用ch接收s1的单个字符的值,再直接打印出来。而范围for的底层实现就是迭代器。

或许有的人会问如何通过范围for的遍历修改容器的值,很简单,让ch变成引用就可以
cpp
string s1 = "123456789";
for (auto& ch : s1)
{
cout << ++ch << " ";
}

使用范围for可以很方便,简洁的遍历一个容器/数组,提高程序的可读性,而范围for的底层又是使用迭代器实现的,所以可以看出迭代器的厉害之处。
而迭代器不仅仅有普通的迭代器,还有const迭代器,当const对象使用begin()时,返回的就是 const 迭代器,此时如果使用普通迭代器接收返回值,编译器会报错。

- 这里需要注意,const迭代器并不是使用const修饰的迭代器,而是直接定义了新的类型名字
const_iterator - const迭代器不能使用解引用修改容器的值,否则编译器会报错

cpp
const string s2 = "change world";
string::const_iterator it = s2.begin();
//const迭代器只能用来遍历容器
while (it != s2.end())
{
cout << (*it);
it++;
}

rbegin() / rend()
迭代器除了普通的从前向后遍历的迭代器,还有一种从后向前 遍历的迭代器,称为反向迭代器 。反向迭代器由rbegin()和rend()函数返回,其类型名称为reverse_iterator
rbegin()和rend()的函数原型如下图所示:


rbegin和rend的使用演示
cpp
string s1("1234567");
//rbegin返回反向迭代器
string::reverse_iterator it = s1.rbegin();
//反向迭代器是从后向前遍历
while (it != s1.rend())
{
cout << (*it) << " ";
it++;
}

反向迭代器也分为普通反向迭代器和const反向迭代器,const反向迭代器的类型名为const_reverse_iterator。因为rbegin和rend都有对const修饰的类的函数重载,所以,对于被const修饰的类的对象,会返回const反向迭代器
cpp
const string s2("7654321");
//const反向迭代器
string::const_reverse_iterator it2 = s2.rbegin();
while (it2 != s2.rend())
{
cout << (*it2) << " ";
it2++;
}

❗️注意:标准库中的cbegin() / cend()和crbegin() / crend()其实是限制了只返回const迭代器,其他的功能和begin()/end(),rbegin()/rend()并没有太大的区别

📊 迭代器相关函数总结表格
| 函数类别 | 函数名 | 返回类型 | 功能描述 | 使用场景 |
|---|---|---|---|---|
| 正向迭代器 | begin() |
iterator |
返回指向第一个元素的迭代器 | 正向遍历容器,可修改元素 |
end() |
iterator |
返回指向最后一个元素下一个位置的迭代器 | 遍历结束条件判断 | |
cbegin() |
const_iterator |
返回const正向迭代器(只读) | 遍历const对象,不可修改元素 | |
cend() |
const_iterator |
返回const正向迭代器(只读) | const对象遍历结束判断 | |
| 反向迭代器 | rbegin() |
reverse_iterator |
返回指向最后一个元素的反向迭代器 | 反向遍历容器,可修改元素 |
rend() |
reverse_iterator |
返回指向第一个元素前一个位置的反向迭代器 | 反向遍历结束条件判断 | |
crbegin() |
const_reverse_iterator |
返回const反向迭代器(只读) | 反向遍历const对象 | |
crend() |
const_reverse_iterator |
返回const反向迭代器(只读) | const对象反向遍历结束判断 |
🔄 迭代器类型对比
| 迭代器类型 | 类型名称 | 遍历方向 | 是否可修改 | 适用对象 |
|---|---|---|---|---|
| 普通正向迭代器 | iterator |
从前向后 | ✅ 可修改 | 非const string对象 |
| const正向迭代器 | const_iterator |
从前向后 | ❌ 不可修改 | const string对象 |
| 普通反向迭代器 | reverse_iterator |
从后向前 | ✅ 可修改 | 非const string对象 |
| const反向迭代器 | const_reverse_iterator |
从后向前 | ❌ 不可修改 | const string对象 |
🎯 使用示例对比
cpp
// 正向遍历(可修改)
string::iterator it = s.begin();
while (it != s.end()) { (*it)++; it++; }
// 正向遍历(只读)
string::const_iterator cit = s.cbegin();
while (cit != s.cend()) { cout << *cit; cit++; }
// 反向遍历(可修改)
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend()) { (*rit)++; rit++; }
// 反向遍历(只读)
string::const_reverse_iterator crit = s.crbegin();
while (crit != s.crend()) { cout << *crit; crit++; }
3. 容量
标准库中为string封装的关于容量大小的成员函数一共有9个,如下图所示。虽然说一共有9个成员函数,但是,真正重要且经常使用的成员函数确只有4个(后面会重点标记)。那么,接下来就来学习如何使用这些成员函数吧~

size()(重点)
函数原型
cpp
size_t size() const;

函数功能
返回容器string对象的大小/元素个数
size()的基本使用
cpp
//元素个数为12
string s1("change world");
cout << s1.size() << endl;

既然使用size()可以知道string对象的元素个数,那么是否可以使用这个size()来做到遍历string容器呢?答案是可以的,使用方式如下:
cpp
string s1("change world");
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i];
}

这时有的人会有疑问,为什么string的对象可以像数组一样直接使用[]访问容器中的元素呢?首先我们要知道,string虽然被封装成了一个类,但是它的底层还是char*类型的字符数组,或者说,可以将string看成是char类型的顺序表
这时有人就会反驳了,虽然string的底层是数组,但是它还是被封装成了类啊,也不能直接使用[]访问容器内的元素啊~
确实是这样没错,但是,标准库中为string封装了一个[]的运算符重载啊,而封装这个运算符重载的成员函数就是为了string对象可以直接访问和修改容器中的元素

为了更加直观的感受标准库为string封装的[]运算符重载,下面直接使用operator[]访问string中的元素
cpp
string s1("change world");
for(int i = 0; i < s1.size(); i++)
{
cout << s1.operator[](i) << endl;
}

length()
🏷️ length的功能和使用方式与size()完全一样,这里不过多赘述
max_size()
max_size()用于返回string容器最多可以存储的元素个数,但是实际上能存储的元素个数不到max_size()个,所以,这个函数在实际中并没有什么意义,简单了解即可

resize()
函数原型
cpp
void resize (size_t n);
void resize (size_t n, char c);

函数功能
修改string对象的大小/元素个数
参数解释
size_t n:将string类的对象元素个数修改为n个。对于增加元素,如果没有传递第二个参数,默认多出来的元素全部为'\0'char c:当n大于string原先的元素个数时,多出来的元素都将是字符c
📚️ 如果n小于string原先的元素个数,resize()将会删除多余的元素
resize()的基本使用
cpp
string s1("112233445566");
cout << s1 << endl;
//将元素个数增加到20,多出来的元素全部为x
s1.resize(20, 'x');
cout << s1 << endl;
//将元素个数删除到只剩6个
s1.resize(6);
cout << s1 << endl;

如果增加元素时没有指定多出来的元素的值(没有传递第二个参数),就默认为'\0'
cpp
string s1("112233445566");
s1.resize(30);

capacity()
函数原型
cpp
size_t capacity() const;

函数功能
返回string容器当前的容量大小
capacity()的基本使用
cpp
string s2 = "abcdef";
//容量
cout << s2.capacity() << endl;
int old = s2.capacity();
for (int i = 1; i <= 100; i++)
{
s2.push_back('x');
if (old != s2.capacity())
{
old = s2.capacity();
cout << "i = " << i << " capacity = "
<< s2.capacity() << endl;
}
}

📚️ 这里多提一嘴,通过上面我们会发现,string的容量(capacity)是会随着其元素个数不断扩大的。vs编译器下除了第一次扩容外,每次的扩容大小大概是原先的1.5倍。g++的编译器下每次扩容大小是原先的2倍。
reserve()(重点)
函数原型
cpp
void reserve (size_t n = 0);

函数功能
为string容器扩容或者缩容,对于string对象的扩容,会扩容到目标容量或者更大(一般情况下都会多扩容一些),对于缩容不具有约束力。
📚️ 缩容情况不具有约束力的意思就是,你可以使用reserve缩容,但是是否真的会缩小容量,是编译器决定的,不同的编译器会有不同的操作
函数参数
size_t n:将string容器的容量扩增到n个元素大小的容量
reserve的基本使用
cpp
string s1("change world");
cout << s1.capacity() << endl;
s1.reserve(100);
cout << s1.capacity() << endl;

clear()(重点)
函数原型
cpp
void clear();

函数功能
清空string容器的元素。这里需要注意,清空字符串是让size()变成0,但是capacity()不一定会改变。
clear()的基本使用
cpp
string s1("change world");
//清空前
cout << "size = " << s1.size() << endl;
cout << "capacity = " << s1.capacity() << endl;
s1.clear();
//清空后
cout << "size = " << s1.size() << endl;
cout << "capacity = " << s1.capacity() << endl;

empty()(重点)
函数原型

函数功能
判断string是否为空,如果为空就返回true,否则返回false
empty()的基本使用
cpp
string s1;
if(s1.empty()) cout << "empty" << endl;
else cout << "non-empty" << endl;
shrink_to_fit()
函数原型

函数功能
给string容器缩容,但是并不具有约束力。这代表着预期效果是编译器需要将string容器的容量缩容到size大小,但是实际上,并不是一定会将容量缩小到目标值,这是因为缩容具有很大的代价。
由于对于一块连续的内存空间,编译器不支持只释放部分的内存,只能全部的释放内存。所以,要完成缩容操作的话,需要先开辟一块更小的内存,并将原始数据拷贝到这块内存,然后再将之前的内存释放,从这些操作就可以看出来,缩容的会影响很大的效率。
📚️ 当什么情况下需要缩容的操作呢?当然是觉得容量过大,不想浪费容量的时候。但是缩容又很浪费时间。所以,缩容本质上就是以时间换空间,但是这是操作在实际情况下并不经常使用,这是因为以当下普通的实际情况来看,一个程序的时间是比空间更加珍贵的。
shrink_to_fit的基本使用
cpp
string s1("change world");
//缩容前
cout << "size = " << s1.size() << endl;
cout << "capacity = " << s1.capacity() << endl;
s1.shrink_to_fit();
//缩容后
cout << "size = " << s1.size() << endl;
cout << "capacity = " << s1.capacity() << endl;

📊 容量相关函数总结表格
| 函数名 | 函数原型 | 返回值类型 | 功能描述 | 是否常用 | 注意事项 |
|---|---|---|---|---|---|
size() |
size_t size() const | size_t |
返回字符串当前长度(元素个数) | ✅ 重点 | 与length()功能完全相同 |
length() |
size_t length() const | size_t |
返回字符串当前长度(元素个数) | ⚠️ 了解 | 功能同size(),历史遗留名称 |
max_size() |
size_t max_size() const | size_t |
返回string可存储的最大元素数 | ❌ 不常用 | 理论最大值,实际用不到 |
resize() |
void resize(size_t n) void resize(size_t n, char c) | void |
调整字符串大小 | ✅ 重点 | n<size时删除多余字符;n>size时填充字符c或'\0' |
capacity() |
size_t capacity() const | size_t |
返回当前分配的存储容量 | ⚠️ 了解 | 容量≥size,了解扩容机制时使用 |
reserve() |
void reserve(size_t n = 0) | void |
预留存储空间(预分配容量) | ✅ 重点 | 扩容有效,缩容不一定生效 |
clear() |
void clear() | void |
清空字符串内容 | ✅ 重点 | size变为0,capacity可能不变 |
empty() |
bool empty() const | bool |
判断字符串是否为空 | ✅ 重点 | size==0时返回true |
shrink_to_fit() |
void shrink_to_fit() | void |
请求减少容量以匹配size | ❌ 不常用 | 缩容不一定生效,效率较低 |
📈 容量管理函数对比
| 函数 | 影响size | 影响capacity | 主要用途 |
|---|---|---|---|
resize(n) |
✅ 改变 | 可能改变 | 调整字符串长度,可增可减 |
reserve(n) |
❌ 不变 | ✅ 改变 | 预分配空间,避免频繁扩容 |
clear() |
✅ 变为0 | ❌ 可能不变 | 清空内容,保留容量 |
shrink_to_fit() |
❌ 不变 | ✅ 可能减小 | 尝试释放多余容量 |
💡 使用建议
size()vslength():功能完全相同,推荐使用size()保持与其他STL容器一致- 扩容策略:VS编译器约1.5倍扩容,g++约2倍扩容
- 预分配 :已知需要大量追加时使用
reserve()提高效率 - 缩容谨慎 :
shrink_to_fit()可能无效且效率低,一般不需要主动缩容
4. 元素访问

operaotr[]
operator\[\]其实在size()的使用已经讲解过了,这里就不过多赘述了,只需要记住string可以直接通过[]访问容器内的元素,就是因为标准库中写了[]的函数重载。这里需要注意,如果使用[]访问string的元素时,越界访问了,这里程序会直接报错,报错类型为断言报错。
at()
at()的功能和operator\[\]的功能完全一样,但是如果使用at()越界访问了,会直接抛出异常,而不是断言错误
back() / front()
函数功能
返回string的容器的最后一个元素 / 第一个元素
back() / front()的基本使用
cpp
string s1("123456");
cout << s1.back() << endl;
cout << s1.front() << endl;

📊 元素访问函数总结表格
| 函数名 | 函数原型 | 返回值类型 | 功能描述 | 越界访问处理 | 使用场景 |
|---|---|---|---|---|---|
operator[] |
char& operator\[\] (size_t pos) | char& |
访问指定位置字符 | 断言错误(assertion failed) | 随机访问,性能最好 |
at() |
char& at (size_t pos) | char& |
访问指定位置字符 | 抛出std::out_of_range异常 |
需要异常安全的访问 |
front() |
char& front() | char& |
访问第一个字符 | 空字符串访问未定义 | 快速访问首字符 |
back() |
char& back() | char& |
访问最后一个字符 | 空字符串访问未定义 | 快速访问尾字符 |
🔍 访问方式对比
| 特性 | operator[] |
at() |
front() / back() |
|---|---|---|---|
| 性能 | ⚡ 最快(不检查边界) | ⚠️ 较慢(边界检查) | ⚡ 最快(直接访问) |
| 安全性 | ❌ 不安全(越界导致未定义行为) | ✅ 安全(抛出异常) | ❌ 不安全(空字符串未定义) |
| 使用频率 | ✅ 最常用 | ⚠️ 较少用 | ✅ 常用 |
| 适用场景 | 已知索引有效时 | 需要异常安全时 | 访问首尾元素时 |
📝 使用示例对比
cpp
string s = "Hello";
// operator[] - 最常用,性能最好
char c1 = s[0]; // 'H'
s[1] = 'a'; // 修改为"Hallo"
// at() - 安全但性能稍差
char c2 = s.at(1); // 'a'
// s.at(10); // 抛出std::out_of_range异常
// front() / back() - 快速访问首尾
char first = s.front(); // 'H'
char last = s.back(); // 'o'
s.front() = 'h'; // 修改为"hallo"
s.back() = '!'; // 修改为"halle!"
⚠️ 注意事项
- 越界访问 :
operator[]不检查边界,越界访问导致未定义行为;at()会检查边界并抛出异常 - 空字符串 :对空字符串调用
front()或back()是未定义行为,应先检查empty() - const版本:所有函数都有const重载版本,用于const对象访问
- 性能选择 :在确保索引有效的情况下,优先使用
operator[]以获得最佳性能
5. 修改
string中有很多功能重复但是使用非常麻烦的成员函数,以下对于平时很少使用的成员函数就大概了解即可,对于重点需要掌握的函数会较为详细的讲解。

- operator+=(重点)
函数原型
cpp
string& operator+= (const string& str);
string& operator+= (const char* s);
string& operator+= (char c);

函数功能
拼接字符串,该函数是直接将+=的字符串拼接在原字符串后面,而不是Ascall码的相加
operator+=的基本使用
cpp
string s1("change world ");
string s2("is possible!");
s1 += s2;
cout << s1 << endl;

- append()
append()的功能和operaotr+=功能基本上相同。实际情况下一般更多时间operator+=对字符串的拼接,不过在一些需要特殊的拼接的场景可能也会用到。

- push_back() / pop_back()(重点)

函数功能
push_back()是在string字符串后面添加一个字符,pop_back()是删除string最后一个字符。
cpp
string s1;
for(int i = 0; i < 10; i++) s1.push_back('x');
cout << "s1 = " << s1 << endl;
for(int i = 0; i < 10; i++) s1.pop_back();
cout << "s1 = " << s1 << endl;

- assign()
assign()的函数功能和赋值运算符重载的函数功能基本上相同,了解即可。
- insert()(重点)
cpp
string& insert (size_t pos, const string& str);
string& insert (size_t pos, const string& str, size_t subpos, size_t sublen);
string& insert (size_t pos, const char* s);
string& insert (size_t pos, const char* s, size_t n);
string& insert (size_t pos, size_t n, char c);

函数功能
在字符串string的指定位置插入一个字符串或者字符
各种函数重载解释(迭代器相关的除外)
- 在pos位置插入一个string类型的字符串str
- 在pos位置插入字符串str的第subpos位置开始,长度为sublen的子串
- 在pos位置插入常量字符串s
- 在pos位置插入常量字符串s的前n个字符
- 在pos位置插入n个字符c
📚️ 参数有迭代器的,插入字符的位置就是迭代器指向的位置,这一点从迭代器将
size_t pos取代就可以推断出来,而最后一个有关迭代器范围的参数在后续会进行讲解
insert()的基本使用
cpp
string s1("1122334455");
//在s1的0位置插入一个字符串常量"oo"
s1.insert(2, "oo");
cout << s1 << endl;
string s2("aaabbccddee");
//在s1的末尾插入一个字符串s2
s1.insert(s1.size(), s2);
cout << s1 << endl;
//在s1的0位置插入5个字符'x'
s1.insert(0, 5, 'x');
cout << s1 << endl;

- erase()(重点)

函数功能
删除字符串string的任意子串。
各种函数重载解释
- 如果pos位置没有传递参数,那么就从string的头部位置开始删除。而如果len没有传递参数,那么就删除从pos位置开始到字符串结尾的子串。
- 删除迭代器指向位置的字符
- 删除迭代器
[first, last)区间的子串
erase()的基本使用
cpp
string s1("123456789");
cout << "s1 = " << s1 << endl;
s1.erase(1, 3);
cout << "s1 = " << s1 << endl;
s1.erase(s1.begin());
cout << "s1 = " << s1 << endl;
s1.erase(s1.begin(), s1.end());
cout << "s1 = " << s1 << endl;

- replace()

函数功能
将string字符的任意子串替换为其他的字符串/字符。replace()的重载函数有很多,但是一般会用到的只有图中红色方框框起来的两种,其他的简单了解即可。
cpp
string s1("123456789");
s1.replace(0, 3, "xxx");
cout << s1 << endl;

📊 修改操作函数总结表格
| 函数名 | 函数原型(常用形式) | 返回值类型 | 功能描述 | 是否常用 | 使用场景 |
|---|---|---|---|---|---|
operator+= |
string& operator+= (const string& str) | string& |
字符串拼接 | ✅ 重点 | 尾部追加字符串/字符 |
append() |
string& append (const string& str) | string& |
字符串追加 | ⚠️ 了解 | 功能同operator+=,参数更灵活 |
push_back() |
void push_back (char c) | void |
尾部添加字符 | ✅ 重点 | 逐个字符追加 |
pop_back() |
void pop_back() | void |
删除尾部字符 | ✅ 重点 | 删除最后一个字符 |
assign() |
string& assign (const string& str) | string& |
字符串赋值 | ❌ 不常用 | 功能同operator=,了解即可 |
insert() |
string& insert (size_t pos, const string& str) | string& |
指定位置插入 | ✅ 重点 | 在任意位置插入字符串/字符 |
erase() |
string& erase (size_t pos = 0, size_t len = npos) | string& / iterator |
删除子串 | ✅ 重点 | 删除指定位置/范围的字符 |
replace() |
string& replace (size_t pos, size_t len, const string& str) | string& |
替换子串 | ⚠️ 了解 | 将指定范围的字符替换为新内容 |
🔧 修改操作分类对比
| 操作类型 | 主要函数 | 特点 | 效率 |
|---|---|---|---|
| 尾部追加 | operator+=, append(), push_back() |
在字符串末尾添加内容 | ⚡ 高效 |
| 头部/中间插入 | insert() |
在指定位置插入内容 | ⚠️ 较低(需要移动后续元素) |
| 删除操作 | erase(), pop_back() |
删除指定位置或范围的字符 | ⚠️ 较低(需要移动后续元素) |
| 替换操作 | replace() |
替换指定范围的字符 | ⚠️ 中等 |
📝 常用操作示例
cpp
string s = "Hello";
// 尾部追加
s += " World"; // "Hello World"
s.push_back('!'); // "Hello World!"
s.append("!!!"); // "Hello World!!!"
// 插入操作
s.insert(6, "Beautiful "); // "Hello Beautiful World!!!"
// 删除操作
s.erase(6, 10); // 删除"Beautiful " -> "Hello World!!!"
s.pop_back(); // 删除最后一个'!' -> "Hello World!!"
s.erase(s.begin()); // 删除第一个字符 -> "ello World!!"
// 替换操作
s.replace(0, 4, "Hi"); // 替换前4个字符 -> "Hi World!!"
💡 使用建议
- 尾部追加优先 :使用
operator+=或push_back(),效率最高 - 中间操作谨慎 :
insert()和erase()涉及元素移动,效率较低 - 参数检查 :使用
insert()和erase()时注意pos参数不要越界 - 迭代器版本 :
insert()和erase()有迭代器版本,可与算法配合使用
6. 字符串操作(部分)
关于字符串操作的函数,有很多在实际场景根本就用不到,这里讲解几个经常使用的成员函数

- c_str()
c_str()的函数功能就是返回一个char类型的指针,该指针指向string底层存储的字符串首元素的位置,可以理解成该指针就是存储字符串的字符数组的名字指向的位置。其函数原型如下图所示

这里有的人会问,那标准库提供这个函数有什么作用呢?前面说了,C++是兼容C语言的,也就是说即使在C++的程序中,可能也会使用C语言的一些函数。但是,有些C语言的函数接口是不认识string类型的,而在C++中字符串又基本上都是使用string存储了,这时如果要使用C语言的函数要怎么办呢?
此时,c_str()函数的作用就体现出来了,可以将string类的对象转换成char*类型传递给C语言的函数接口。这里举一个最常见的例子,如何使用printf打印string类型的字符串?
如果直接打印的话编译器肯定会报错,因为printf可不认识string是什么,此时,就要用到string的成员函数c_str()了。
cpp
string s1("change world");
printf("%s\n", s1.c_str());

- find()
find()函数用于在string中查找字符串或者单个字符。默认从string的起始位置开始查找,如果找到了目标字符串,就会返回该字符串在string中的起始位置。如果string中有多个和该字符串相同的字符串,则返回从查找位置开始该字符串第一次出现的位置,其函数原型如下

从图中也可以看出来,find函数查找字符串是可以指定开始查找的位置的,不过如果没有传参,就默认从0位置开始查找,也就是string的开头。
这里给出一个find函数实际的使用场景,现在给你一个若干个文件名,要知道它们文件后缀是什么。这个解决方案很简单,直接使用find查找'.'就可以了。如下代码所示,这里还用到了string的构造函数的重载。
cpp
string filename1("test.cpp");
string suffix(filename1, filename1.find('.'));
cout << suffix << endl;

- rfind()
rfind / find的关系和rbegin / begin 的关系是相同的,find函数是从前往后查找字符串,而rfind是从后往前查找字符串 。rfind也可以指定开始查找的位置,但是这个指定的位置是从前往后计算的,即如果指定rfind从pos位置开始查找,rfind就会从pos位置开始,往0位置查找。其函数原型如下

如上如所示,rfind的默认的起始查找位置是npos,这也就代表了,rfind默认起始查找位置为string的结尾。
现在还是需要查找文件名的后缀,但是我们知道,一些特殊的文件后缀会有多个,如test.cpp.net.edu,而现在我只想要最后面的那个后缀,要如何拿到?这里如果继续使用find会有些麻烦,但是使用rfind的话,就可以快速解决问题了。
cpp
string filename1("test.cpp.net.edu");
string suffix(filename1, filename1.rfind('.'));
cout << suffix << endl;

- substr()
substr的函数功能是截取字符串,一般在只需要一个字符串的某个部分的场景使用。substr可以指定截取的开始位置和截取的长度,默认从起始位置开始截取,截取到字符串的末尾

还是刚才的使用场景,如何截取文件名的后缀?使用构造函数的特性是一个方案,但是使用构造函数确要创造一个对象啊,这时,使用substr就会更加的方便。
cpp
string filename1("test.cpp.net.edu");
cout << filename1.substr(filename1.find('.')) << endl;

📊 字符串操作函数总结表格
| 函数名 | 函数原型(常用形式) | 返回值类型 | 功能描述 | 是否常用 | 使用场景 |
|---|---|---|---|---|---|
c_str() |
const char* c_str() const | const char* |
返回C风格字符串指针 | ✅ 重点 | 与C语言接口交互,如printf |
find() |
size_t find (const string& str, size_t pos = 0) const | size_t |
正向查找子串/字符 | ✅ 重点 | 查找字符串首次出现位置 |
rfind() |
size_t rfind (const string& str, size_t pos = npos) const | size_t |
反向查找子串/字符 | ✅ 重点 | 查找字符串最后出现位置 |
substr() |
string substr (size_t pos = 0, size_t len = npos) const | string |
截取子串 | ✅ 重点 | 获取字符串的指定部分 |
🔍 查找函数对比
| 特性 | find() |
rfind() |
|---|---|---|
| 查找方向 | 从前向后查找 | 从后向前查找 |
| 默认起始位置 | 0(开头) | npos(结尾) |
| 返回值 | 首次出现位置 | 最后一次出现位置 |
| 典型应用 | 查找文件扩展名 | 查找多层扩展名的最后部分 |
📝 使用示例对比
cpp
string s = "test.cpp.net.edu";
// c_str() - 与C语言接口交互
printf("C风格字符串: %s\n", s.c_str());
// find() - 正向查找
size_t pos1 = s.find('.'); // 查找第一个'.'的位置
cout << "第一个点位置: " << pos1 << endl; // 4
// rfind() - 反向查找
size_t pos2 = s.rfind('.'); // 查找最后一个'.'的位置
cout << "最后一个点位置: " << pos2 << endl; // 12
// substr() - 截取子串
string ext1 = s.substr(pos1); // 从第一个点开始截取
cout << "所有扩展名: " << ext1 << endl; // ".cpp.net.edu"
string ext2 = s.substr(pos2); // 从最后一个点开始截取
cout << "最后扩展名: " << ext2 << endl; // ".edu"
💡 使用建议
- C语言兼容 :需要调用C标准库函数(如printf、fopen等)时,使用
c_str()转换 - 查找选择 :需要查找第一次出现用
find(),查找最后一次出现用rfind() - 子串截取 :
substr()比构造函数更直观,推荐用于获取字符串片段 - 参数默认值 :
find()默认从开头查找,rfind()默认从结尾查找,substr()默认截取到末尾 - 返回值处理 :查找函数返回
size_t类型,未找到时返回string::npos
本篇完结
