【C++】string(上):string的基本使用

🧑‍💻博主名称:鱼子星_

✅数据结构专栏:【数据结构】

✅算法竞赛专栏:【算法竞赛】

✅C++系列专栏:【C++从零开始系列】


🎯 前言

在C++编程中,字符串处理是几乎每个程序都会遇到的基础任务。无论是简单的用户输入输出,还是复杂的文本处理、文件操作,字符串都扮演着至关重要的角色。然而,传统的C风格字符串(字符数组和char*指针)在使用上存在诸多不便:内存管理复杂、容易越界、操作函数繁琐且不安全。

C++标准库中的string类正是为了解决这些问题而设计的。作为STL(标准模板库)的重要组成部分,string不仅封装了字符序列,还提供了丰富、安全、高效的成员函数,让字符串操作变得简单直观。

📚 本文内容概览

本文将从零开始,全面系统地讲解C++ string类的使用,涵盖以下核心内容:

  1. 默认成员函数 - 构造函数、析构函数、赋值运算符重载
  2. 迭代器 - 正向/反向迭代器、const迭代器、范围for循环
  3. 容量管理 - size()、capacity()、resize()、reserve()等
  4. 元素访问 - operator\[\]、at()、front()、back()等
  5. 修改操作 - 拼接、插入、删除、替换等
  6. 字符串操作 - 查找、截取、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() ❌ 不变 ✅ 可能减小 尝试释放多余容量

💡 使用建议

  1. size() vs length() :功能完全相同,推荐使用size()保持与其他STL容器一致
  2. 扩容策略:VS编译器约1.5倍扩容,g++约2倍扩容
  3. 预分配 :已知需要大量追加时使用reserve()提高效率
  4. 缩容谨慎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!"

⚠️ 注意事项

  1. 越界访问operator[]不检查边界,越界访问导致未定义行为;at()会检查边界并抛出异常
  2. 空字符串 :对空字符串调用front()back()是未定义行为,应先检查empty()
  3. const版本:所有函数都有const重载版本,用于const对象访问
  4. 性能选择 :在确保索引有效的情况下,优先使用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的指定位置插入一个字符串或者字符

各种函数重载解释(迭代器相关的除外)

  1. 在pos位置插入一个string类型的字符串str
  2. 在pos位置插入字符串str的第subpos位置开始,长度为sublen的子串
  3. 在pos位置插入常量字符串s
  4. 在pos位置插入常量字符串s的前n个字符
  5. 在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的任意子串。

各种函数重载解释

  1. 如果pos位置没有传递参数,那么就从string的头部位置开始删除。而如果len没有传递参数,那么就删除从pos位置开始到字符串结尾的子串。
  2. 删除迭代器指向位置的字符
  3. 删除迭代器[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!!"

💡 使用建议

  1. 尾部追加优先 :使用operator+=push_back(),效率最高
  2. 中间操作谨慎insert()erase()涉及元素移动,效率较低
  3. 参数检查 :使用insert()erase()时注意pos参数不要越界
  4. 迭代器版本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"

💡 使用建议

  1. C语言兼容 :需要调用C标准库函数(如printf、fopen等)时,使用c_str()转换
  2. 查找选择 :需要查找第一次出现用find(),查找最后一次出现用rfind()
  3. 子串截取substr()比构造函数更直观,推荐用于获取字符串片段
  4. 参数默认值find()默认从开头查找,rfind()默认从结尾查找,substr()默认截取到末尾
  5. 返回值处理 :查找函数返回size_t类型,未找到时返回string::npos

本篇完结

相关推荐
AOwhisky1 小时前
Python 学习笔记(第四期)——字符串:格式化、操作与文本处理——核心知识点自测与详解
开发语言·笔记·python·学习·列表·元组·字典
chnyi6_ya1 小时前
论文阅读笔记 | VIDEOPHY: Evaluating Physical Commonsense for Video Generation
论文阅读·笔记
AOwhisky2 小时前
Python 学习笔记(第六期)——函数:定义、参数传递与作用域
笔记·python·学习·云原生·运维开发·函数·参数传递
神仙别闹2 小时前
编写基于C++ Huffman 算法的无损压缩程序和解压程序
java·c++·算法
炸膛坦客2 小时前
单片机/C/C++八股:(二十三)#define 和 typedef 的区别
c语言·c++·单片机
旖-旎2 小时前
《LeetCode 646 最长数对链 || LeetCode 1143 最长公共子序列》
c++·算法·leetcode·动态规划
kdxiaojie3 小时前
Linux 驱动研究 —— V4L2 (2)
linux·运维·笔记·学习
阿米亚波3 小时前
【C++ STL】std::list
c++·windows·笔记·stl·list
橙色阳光五月天3 小时前
CMake 构建配置文件解析
c++·cmake·plugin