C++ string类从入门到精通:核心用法与实战指南
用逻辑丈量宇宙。
| 导航 | 链接 |
|---|---|
| 个人主页 | 🏠 星轨初途 |
| 基础语言专栏 | 💻 C语言 、 📚 数据结构 |
| C++ 进阶专栏 | 🏆 C++学习(竞赛类) 、 ⚙️ C++专栏(开发类) |
| 刷题实战专栏 | 🚀 算法及编程题分享 |

文章目录
前言
嗨!我们又见面啦٩(๑❛ᴗ❛๑)۶!欢迎回到星轨的 C++ 进阶频道!
在上一篇 【C++进阶】让编译器替你打工!一文打通泛型编程与 C++ 模板 中,我们手撕了底层的“造物模具”,彻底解锁了让编译器自动写代码的黑魔法。
有了模板的加持,今天我们终于要正式敲开 C++ 顶级宝库的大门——STL(标准模板库)!
而在 STL 这支钢铁洪流中,打头阵的先锋正是我们日常开发最常用、也最离不开的 std::string。为了让大家既能“用得溜”又能“懂底层”,我将这部分内容拆分成了硬核的双子篇:本篇我们将先聚焦 string 的核心用法,带你掌握现代 C++ 的字符串操作技巧;下一篇,我们将继续深挖,徒手拆解它的底层实现原理与黑科技。
准备好迎接 STL 带来的降维打击了吗?废话不多说,本篇 string 核心使用指南,我们正式起飞!🚀
一、学习string的目的
1、C语言中字符串的劣势
在 C 语言中,字符串本质上是以 '\0' 结尾的字符集合。它就像一团没有理顺的毛线球,非常让人头疼:
- 数据与方法分离 (不符合 OOP):C 标准库提供了
<string.h>系列的库函数(如strlen、strcat),但这些函数与字符串本身是分离的,调用起来极其原始。 - 内存管理的无底洞:底层空间需要程序员手动
malloc/free,稍不留神就会引发数组越界(Buffer Overflow)或内存泄漏,甚至成为黑客攻击的漏洞。 - 动态扩容极其痛苦:如果你想拼接两个长字符串,必须先计算两个字符串的长度,然后重新开辟一块更大的空间,再把原数据拷贝过去。代码又臭又长,效率极低。
2. C++ 中 string 类的绝对优势
string 类完美解决了上述痛点。它将底层的 char* 数组完全封装,自带内存池管理与自动扩容机制,天然支持包括拼接、查找、截取在内的百余种操作。
二、string类的了解及使用
string的参考文档 string
1、了解string类

我们可以通过string文档来进行对string的了解,相当于就是字符串的封装,
在使用string类时,必须包含#include头文件以及using namespace std;
🚨 预备工作:使用
string类,必须包含头文件#include <string>,并展开命名空间using namespace std;。注意,是<string>而不是 C语言的<string.h>!
2. 字符串创建 :核心 4 种姿势
STL 标准库为 string 提供了十几种重载的构造函数,但在日常开发中,掌握以下 4 种就足以应对 99% 的场景:
void TestConstructor()
{
// 姿势 1:默认构造 (生成一个空字符串 "")
string s1;
// 姿势 2:用 C 格式字符串构造 (最常用)
string s2("hello bit");
// 姿势 3:拷贝构造 (用已有的 string 对象生成新对象)
string s3(s2);
// 姿势 4:重复字符构造 (创建包含 n 个相同字符的字符串)
// 例如:生成 5 个 'x' 组成的字符串 "xxxxx"
string s4(5, 'x');
// 进阶用法:取 C 字符串的前 n 个字符
string s5("hello world", 5); // 结果为 "hello"
}
3.字符串遍历的三种方式
在学习 string 遍历之前,为了让我们的代码更加现代、优雅,必须先补充两个 C++11 的王炸语法糖:
3.1前置知识
3.1.1 auto 关键字
早期 C++ 中 auto 意义不大。但在 C++11 中,它化身为类型推导指示符。它可以让编译器在编译期自动推导出变量的类型。
int a = 10;
auto b = a; // 编译器自动推导 b 为 int
auto c = 'x'; // 编译器自动推导 c 为 char
// 🚨 避坑注意:用 auto 声明引用时必须加 &
auto& d = a; // d 是 a 的引用,修改 d 就会修改 a
3.1.2 范围 for 循环 (Range-based for)
遍历一个数组或容器,由程序员去指定边界既繁琐又容易写错(比如漏掉 <=)。范围 for 循环能够自动取数据、自动判断结束。
int arr[] = {1, 2, 3, 4, 5};
// 自动将 arr 中的每一个元素依次赋值给 e
for (auto e : arr)
{
cout << e << " ";
}
注:范围 for 的底层其实就是替换成了迭代器(Iterator),这个我们后面马上会讲到。
3.2.字符串遍历三种方式
| 函数名称 | 功能说明 |
|---|---|
operator[] |
返回 pos 位置的字符,可供普通及 const string 类对象调用 |
begin + end |
begin 获取第一个字符的迭代器 + end 获取最后一个字符下一个位置的迭代器 |
rbegin + rend |
rbegin 获取最后一个字符的迭代器 + rend 获取首字符前一个位置的迭代器 |
范围 for |
C++11 支持更简洁的范围 for 的新遍历方式 |
假设我们要遍历字符串,将内部的每一个小写字母都修改为大写,我们有以下三种方式:
void TestTraversal()
{
string str("hello world");
// 方式 1:operator[] 下标访问(最符合 C 程序员直觉)
// size() 函数可以获取字符串有效长度
for (size_t i = 0; i < str.size(); ++i)
{
str[i] -= 32; // 将小写转大写,str[i] 越界时底层会触发 assert 报错
}
// 方式 2:正向迭代器 iterator 遍历(🔥 工业级代码推荐)
// 迭代器是 STL 容器通用的遍历方式,不管底层是数组还是链表、树,都可以用迭代器!
for (string::iterator it = str.begin(); it != str.end(); ++it)
{
*it -= 32;
}
// 方式 3:范围 for 遍历(底层基于 begin 和 end 迭代器实现,最简洁)
// 🚨 注意:如果需要修改字符串里的内容,变量前必须加引用 & !
for (auto& ch : str)
{
ch -= 32;
}
}
💡 黄金开发经验:反向遍历怎么写?
如果需要从尾到头倒着遍历,下标控制常常让人头晕(一不小心i = -1变成超级大正数)。此时反向迭代器闪亮登场:for (string::reverse_iterator rit = str.rbegin(); rit != str.rend(); ++rit)
4、string 对象的容量操作
容量操作是面试中最常考察、也是日常开发中最容易踩坑的地方!
| 函数名称 | 功能说明 |
|---|---|
| size()/ length() | 返回字符串有效字符的长度。两者底层完全一致,通常推荐使用 size() 保持与 STL 其他容器接口一致。 |
| capacity | 返回底层实际开辟的总空间大小(通常大于等于 size)。 |
| empty() | 判断字符串是否为空,空返回 true。 |
| clear() | 清空所有有效字符。 |
| reserve(n) | 预分配空间:将容量扩展到至少 n,不改变有效字符内容。 |
| resize(n, c) | 调整有效字符数:将 size 改成 n,多出的空间用字符 c 填充。 |
我们用代码来进行直观理解
#include <iostream>
#include <string>
using namespace std;
void TestStringCapacity()
{
// 1. 初始化字符串,观察基础容量属性
string s("Hello C++");
cout << "========== 1. 初始状态 ==========" << endl;
cout << "当前内容: " << s << endl;
cout << "size() : " << s.size() << " (有效字符数)" << endl;
cout << "length() : " << s.length() << " (等同于 size())" << endl;
// capacity 的具体数值受编译器优化(如 SSO)影响,通常大于等于 size
cout << "capacity : " << s.capacity() << " (底层开辟的总空间)" << endl;
cout << "empty() : " << (s.empty() ? "true" : "false") << endl;
// 2. clear() 清空数据测试
cout << "\n========== 2. 调用 clear() ==========" << endl;
s.clear();
cout << "调用后 size() : " << s.size() << " (数据已被清空)" << endl;
// 🚨 核心避坑:clear 只清空数据,绝不释放底层容量!
cout << "调用后 capacity : " << s.capacity() << " (容量保持不变!)" << endl;
cout << "调用后 empty() : " << (s.empty() ? "true" : "false") << endl;
// 3. reserve(n) 预分配空间测试
cout << "\n========== 3. 调用 reserve(100) ==========" << endl;
s = "Star"; // 重新赋个短值
s.reserve(100);
cout << "当前内容: " << s << endl;
cout << "调用后 size() : " << s.size() << " (size 绝对不发生改变)" << endl;
cout << "调用后 capacity : " << s.capacity() << " (容量已扩充至至少 100)" << endl;
// 4. resize(n, c) 扩充大小测试
cout << "\n========== 4. 调用 resize(10, '*') ==========" << endl;
s.resize(10, '*');
cout << "当前内容: " << s << endl;
cout << "调用后 size() : " << s.size() << " (有效字符扩充到 10,多出的用 * 填充)" << endl;
cout << "调用后 capacity : " << s.capacity() << endl;
// 5. resize(n) 缩小大小测试
cout << "\n========== 5. 调用 resize(4) ==========" << endl;
s.resize(4);
cout << "当前内容: " << s << endl;
cout << "调用后 size() : " << s.size() << " (后面的字符被截断丢弃)" << endl;
// 🚨 核心避坑:resize 缩小 size 时,同样不会缩容!
cout << "调用后 capacity : " << s.capacity() << " (底层容量依然保持不变!)" << endl;
}
int main()
{
TestStringCapacity();
return 0;
}

我们通过代码能很清楚了解到string对象的容量操作的过程,其中我们有几点要注意
-
clear() 的陷阱
当你调用s.clear()时,它确实会把字符串里的所有字符清空(size变为 0),但是它绝对不会缩容! 底层的capacity依然保持原来的大小,以便你后续再插入数据时不用重新申请内存。 -
reserve() vs resize() 的终极对决
resize(n):不仅会改变底层容量,还会真正的插入数据!如果指定的n大于当前的size,它会把新增的空间全部用\0(或你指定的字符)填满。reserve(n):只改变容量,绝不插入数据! 它是纯粹的提前向操作系统申请好内存。
💡 黄金避坑与性能压榨指南:
如果你在做一个循环,需要往string里面push_back(下一条会讲)追加 10万 个字符。如果不做任何处理,string会在底层频繁触发**“申请新空间 -> 拷贝旧数据 -> 释放旧空间”**的扩容机制,极其消耗 CPU!
正确做法:在循环开始前,直接调用s.reserve(100000)!一次性把空间申请到位,消灭所有的扩容开销,性能瞬间实现降维打击!
5、字符串的修改操作
在实际开发中,我们对字符串的操作无外乎四种:追加、插入删除、查找截取、格式转换。STL 为此提供了极其丰富的接口,下面我们逐一拆解最常用的几个核心函数,并附上避坑指南。
| 函数名称 | 功能说明 |
|---|---|
| push_back | 在字符串后尾插字符 c |
| append | 在字符串后追加一个字符串 |
| operator+=( 重点) | 在字符串后追加字符串 str |
| insert | 在指定位置 pos 插入字符或字符串 |
| erase | 从指定位置 pos 开始删除 n 个字符 |
| c_str (重点) | 返回 C 格式字符串 |
| find + npos | 从前往后找字符/子串,返回位置 |
| substr | 截取子串 |
| pop_back | 删除字符串中尾部的一个字符 |
1. 尾部追加拼接:push_back / append / operator+=
这是平时写代码最常用到的操作。C++ 提供了三种方式在字符串尾部添加数据:
push_back(c):只能在字符串尾部追加单个字符c。append(str):在字符串尾部追加一个字符串。operator+=(🔥 绝对主力):既能追加单个字符,也能追加字符串。
💡 黄金开发经验:无脑使用
+=
在实际工程中,我们强烈建议无脑使用operator+=!因为它的底层经过了充分优化,且大小通吃,代码可读性极强。只有在必须配合某些泛型算法时,才会去使用push_back或append。
void TestAppend()
{
string s = "Hello";
s.push_back(' '); // 尾插单字符
s.append("World"); // 追加字符串
s += "!!!"; // 🔥 最推荐的做法:极其直观
s += 'A'; // += 也能追加单字符
cout << s << endl; // 输出: Hello World!!!A
}

2.尾部删除数据 pop_back
pop_back() 用于删除字符串中尾部的一个字符。

实现
#include <iostream>
#include<string>
using namespace std;
int main()
{
string s = "hello";
cout << "s:" << s << endl;
//尾删
s.pop_back();
cout << "s:" << s << endl;
//尾删
s.pop_back();
cout << "s:" << s << endl;
return 0;
}
效果

注意:不可对空字符串继续进⾏pop_back()操作,否则程序出现异常
3. 任意位置的安插与抹除:insert 与 erase
如果你不想在尾部追加,而是想在字符串中间“搞事情”,insert 和 erase 这对“好兄弟”就能派上用场。值得注意的是,C++ 标准委员会为 insert 提供了极其丰富的重载版本,除了常规的下标索引,它还完美支持迭代器 (Iterator) 操作,以无缝衔接整个 STL 泛型算法生态。
👉 insert 的四大高频重载用法:
- 插入单个或整个字符串:
insert(pos, str)—— 在pos索引位置插入另一个string或 C 风格字符串。 - 连续插入多个相同字符:
insert(pos, n, c)—— 在pos索引位置连续插入n个字符c。 - 插入别人的一部分 (子串):
insert(pos, str, subpos, sublen)—— 把另一个字符串str中从subpos开始的sublen个字符截取过来插入。 - 迭代器单字符插入 (✨ 泛型利器):
insert(iterator, char)—— 在迭代器指向的位置插入单个字符,这是配合 STL 算法库时的核心姿势!

👉 erase 的核心用法:
erase(pos, n):从pos位置开始,删除n个字符。如果省略n(即缺省参数为string::npos),则默认从pos一直删到字符串末尾。
💻 核心实战代码演示:
void TestInsertEraseDetailed()
{
string s("hello");
// ==========================================
// 1. insert 的花式插入
// ==========================================
// 用法一:在指定位置插入一个完整的字符串
s.insert(0, "star "); // 在头部 (索引0) 插入 -> "star hello"
// 用法二:在指定位置连续插入 n 个相同的字符
// 例如:在末尾插入 3 个感叹号 '!'
s.insert(s.size(), 3, '!'); // -> "star hello!!!"
// 用法三:高端操作!插入另一个字符串的部分子串
string other = "welcome to earth";
// 解析:在 s 的索引 5 位置,插入 other 中从索引 0 开始的 8 个字符 ("welcome ")
s.insert(5, other, 0, 8); // -> "star welcome hello!!!"
// 用法四:迭代器单字符插入
string s3("world");
// 在字符串开头(begin() 返回的迭代器位置)插入一个空格字符
s3.insert(s3.begin(), ' '); // -> " world"
// ==========================================
// 2. erase 的精准抹除
// ==========================================
// 用法一:指定起点和长度进行删除
// 从索引 0 开始,删除 5 个字符 (把 "star " 删掉)
s.erase(0, 5); // -> "welcome hello!!!"
// 用法二:只传起点,默认“一刀切”到结尾!
// 从索引 7 的位置开始,后面全不要了
s.erase(7); // -> "welcome"
}
🚨 极限避坑预警 (O(N) 灾难):
string的底层是一个连续的字符数组。当你在头部或中间使用insert或erase时,会导致操作点之后的所有字符进行大规模的整体挪动!如果在一个大循环里频繁头插,时间复杂度会飙升至 O ( N 2 ) O(N^2) O(N2),直接导致程序超时。非必要,不头插!
4. 提取信息的黄金搭档:find 与 substr
find(str, pos)(精确查找):从pos位置开始,从前往后查找完全匹配的子串或字符。find(str, pos)(字符集查找/✨ 高阶利器):从pos开始,查找目标字符串中任意一个字符首次出现的位置。find从前往后找,rfind从后往前找。如果找到了,返回该字符所在的索引pos;如果没找到,会返回一个特殊的常量string::npos(本质上是size_t的最大值)。
👉 substr 的精准切割:
substr(pos, n):从pos位置开始,截取n个字符并返回一个新字符串。如果省略n,则默认直接截取到结尾。

void TestFindSubstr()
{
// 经典场景:提取文件后缀名
string file = "project.src.main.cpp";
// 从后往前找第一个 '.'
size_t pos = file.rfind('.');
//从前往后找第一个 '.'
size_t pos = file.find('.');
//从pos往后找第一个 '.'
size_t pos = file.find(pos,'.');
// 必须判断是否等于 string::npos,防止越界崩溃!
if (pos != string::npos)
{
// 缺省第二个参数,默认直接截取到字符串末尾
string ext = file.substr(pos);
//从 pos 位置开始,截取 11 个字符并返回一个新字符串
string ext = file.substr(pos,11);
cout << "文件后缀名为: " << ext << endl; // 输出: .cpp
}
}
5. 跨时代沟通的桥梁:c_str
C++ 的 string 虽好,但我们在做系统级开发(如网络 Socket 编程、文件操作)时,常常需要调用 C 语言遗留下来的底层 API(如 fopen、printf)。这些老古董是不认识 std::string 对象的,它们只认 const char* 指针。
c_str():返回一个指向正规 C 风格字符串(以\0结尾)的常量指针。
void TestCStr()
{
string filename = "test.txt";
// 🚨 错误示范:fopen 无法接收 string 对象
// FILE* fout = fopen(filename, "r");
// ✅ 正确姿势:使用 c_str() 转换为 C 风格字符串指针
FILE* fout = fopen(filename.c_str(), "r");
// 配合 printf 使用也必须加 c_str()
printf("打开的文件是: %s\n", filename.c_str());
}
6.读取带空格的完整字符串:getline()

- getline是C++标准库中的一个函数,用于从输入流中读取一行文本,并将其存储为字符串。
- getline函数有两种不同的形式,分别对应着字符串的结束方式。
istream& getline (istream& is, string& str);
istream& getline (istream& is, string& str, char delim);
✅提示:
- istream是输入流类型,cin是istream类型的标准输入流对象。
- ostream是输出流类型,cout是ostream类型的标准输出流对象。
- getline函数是从输入流中读取一行文本信息,所以如果是在标准输入流(键盘)中读取数据,就可以传cin给第一个参数。
第一种(默认以‘\n’为结束标志)
第一种 getline 函数以换行符(‘\n’)作为字符串的结束标志,它的一般格式是:
getline(cin, string str)
//cin -- 表示从输入流中读取信息
//str 是存放读取到的信息的字符串
这种形式的 getline 函数从输入流(例如cin)中读取文本,直到遇到换行符(‘\n’)为止,然后将读取到的文本(不包括换行符)存储到指定的string类型的变量str中。
实例:
#include<iostream>
#include <string>
using namespace std;
int main ()
{
string name;
getline (cin, name);
cout << name << endl;
return 0;
}
效果:

第二种(自定义结束标志)
第二种 getline 函数允许用户自定义结束标志,它的一般格式是:
getline(cin, string str, char delim)
//cin -- 表示从输入流中读取信息
//str 是存放读取到的信息的字符串
//delim 是自定义的结束标志
这种形式的 getline 函数从输入流中读取文本,直到遇到用户指定的结束标志字符(delim)为止,然后将读取到的文本(不包括结束标志字符)存储到指定的string类型的变量str中。
实例:
#include<iostream>
#include <string>
using namespace std;
int main ()
{
string name;
getline (cin, name, 'q');
cout << name << endl;
return 0;
}
效果

三、string中字符串与基本数值类型的转换工具函数
(1)stoi/stol
- stoi:将字符串转换为
int类型的值 - stol:将字符串转换为
long int类型的值
两者用法类似,以stoi为例讲解:stoi可将string类型的字符串转化为整型,函数原型如下:
函数参数
int stoi (const string& str, size_t* idx = 0, int base = 10);
long stol (const string& str, size_t* idx = 0, int base = 10);
- str:被转换的
string类型字符串 - idx:输出型参数(指针类型)。需在外创建
size_t类型变量,传递其地址给idx;该参数会带回str中无法匹配数字的第一个字符的位置。 - base:被解析字符串的进制值(支持2、8、10、16、0):
- 默认值为10,表示解析10进制数字;
- 传2:解析2进制数字,最终转为10进制;
- 传8:解析8进制数字,最终转为10进制;
- 传16:解析16进制数字,最终转为10进制;
- 传0:自动推导进制(含
0x则为16进制,0开头则为8进制),最终转为10进制。
代码举例
#include <iostream>
#include<string>
using namespace std;
int main()
{
size_t pos = 0;
string s1 = "11x34";
int ret1 = stoi(s1, &pos, 16);
cout << ret1 << endl;
cout << "pos:" << pos << endl;
string s2 = "11x34";
int ret2 = stoi(s2, &pos, 2);
cout << ret2 << endl;
cout << "pos:" << pos << endl;
string s3 = "0x11x34";
int ret3 = stoi(s3, &pos, 0);
cout << ret3 << endl;
cout << "pos:" << pos << endl;
return 0;
}
效果如下:

(2)stod/stof
stod是将字符串转换成double类型的值,就是参数少了进制的描述
参数
double stod (const string& str, size_t* idx = 0);
float stof (const string& str, size_t* idx = 0);
代码示例
#include <iostream>
#include<string>
using namespace std;
int main()
{
string s = "3.14x456";
double ret = stod(s, NULL);
cout << ret << endl;
return 0;
}
(3)to_string
就是将数字转换为字符串
string to_string(数字类型 val);
代码举例
#include <iostream>
#include <string>
using namespace std;
int main()
{
string pi = "pi is " + to_string(3.14159);
cout << pi << endl;
return 0;
}
结束语
嗨!本篇到这里就结束啦!今天我们学习了string的相关操作,下一篇我们将要了解string的底层实现,敬请期待吧!感谢大家的支持啦!

更多推荐


所有评论(0)