目录
[0.1 概述&序言](#0.1 概述&序言)
[1.1 C语言字符串的痛](#1.1 C语言字符串的痛)
[1.2 现实中的需求](#1.2 现实中的需求)
[1.3 两个面试题(先感受一下)](#1.3 两个面试题(先感受一下))
[2.1 头文件与二八原则](#2.1 头文件与二八原则)
[三、string 的构造接口](#三、string 的构造接口)
[3.1 接口速查表格](#3.1 接口速查表格)
[3.2 测试代码](#3.2 测试代码)
[3.3 npos 是什么(重点补充)](#3.3 npos 是什么(重点补充))
[4.1 第一种:operator\[\] + 下标](#4.1 第一种:operator[] + 下标)
[4.2 第二种:迭代器](#4.2 第二种:迭代器)
[4.2.1 迭代器是什么、为什么存在](#4.2.1 迭代器是什么、为什么存在)
[4.2.2 迭代器四兄弟](#4.2.2 迭代器四兄弟)
[4.2.3 测试代码(逐步分析)](#4.2.3 测试代码(逐步分析))
[4.2.4 迭代器的注意事项](#4.2.4 迭代器的注意事项)
[4.3 第三种:auto------类型太长?交给编译器](#4.3 第三种:auto——类型太长?交给编译器)
[4.4 第四种:范围for------全自动遍历](#4.4 第四种:范围for——全自动遍历)
[4.4.1 一道经典题引入:范围for修改元素](#4.4.1 一道经典题引入:范围for修改元素)
[4.4.2 三大遍历方式小结](#4.4.2 三大遍历方式小结)
0.1 概述&序言
这里是白杨,上一篇我们打开了 STL 的大门(没看过的同学请走传送门:C++入门篇(九):模板与STL初探
https://blog.csdn.net/Bai_YangSQ/article/details/166643289)。从今天开始逐个拆解容器,第一站------string。
string 可能是你 STL 生涯里使用频率最高的容器,没有之一。内容比较多,我拆成三篇:
- 上篇(本文):认识 string------为什么学、怎么构造、三大遍历方式一条龙(operator\[\] → 迭代器 → auto → 范围for,环环相扣)
- 中篇:接口大全------容量操作、增删改查,一篇吃透所有常用接口
- 下篇:实战与原理------OJ 刷题 + 手写模拟实现(面试官最爱问的浅拷贝/深拷贝)
内容依旧干货满满,让我们开始吧!!!
一、为什么学习string类
1.1 C语言字符串的痛
先说结论:
C 语言的字符串是 '\0' 结尾的字符集合,靠 str 系列库函数操作:函数和数据分离、空间要自己管、一不小心就越界。
在 C 里操作字符串的日常:strlen要自己算长度、strcat要自己管容量、strstr要判空------字符串和操作它的函数是分离 的,底层空间还得自己维护,稍不留神就越界访问。而 string 把数据和操作封装在一起,自动管理内存、自动扩容,接口直观好记。
1.2 现实中的需求
现实中有太多"数字大到存不下"的场景:身份证号、银行卡号、订单号 ......这些"数字"早就超过了 int 甚至 long long 的存储上限,更别说它们前面还可能有前导 0。这类数据本质上就是一串字符 ,string就是为它们而生的。
1.3 两个面试题(先感受一下)
- 字符串转整形数字 → LeetCode 8. 字符串转换整数 (atoi)
- 字符串相加 → LeetCode 415. 字符串相加
先别急着做------现在你连 string 的接口都还没学,硬做只会打击信心。这两题的完整思路和解答我都放在了下篇的 OJ 实战里(未来即将更新),学完接口回来收它们,正好检验你的熟练度。
在 OJ 中,字符串相关的题基本都以 string 形式出现;工作中也几乎没人再用 C 库字符串函数。string 不是选修,是刚需。
二、一张图理清string
初见 string,先捋一捋知识结构,心里有张地图,后面就不迷路:

2.1 头文件与二八原则
头文件叫 #include
string 因为历史问题接口非常多,但根据二八原则,我们只需要掌握常用的 20% 就够了,剩下冷门的用到了再查文档。本文每张表格里我都会标注:
- (重点):必须会用
- (了解):知道有这回事,用到了再查
- (冗余):有其他接口可以替代,面试能说上两句即可
查接口的传送门:<string> - C++ Reference (C++ Reference 文档,遇到不会的接口就来这查)
三、string 的构造接口
3.1 接口速查表格
| 名称 | 函数签名 | 说明 |
|---|---|---|
| 默认构造(重点) | string() | 构造空的 string 对象 |
| 拷贝构造(重点) | string(const string& str) | 用另一个 string 对象拷贝 |
| 子串构造 | string(const string& str, size_t pos, size_t len = npos) | 取 str 从 pos 开始的 len 个字符;len 超出范围则取到末尾 |
| C 字符串构造(重点) | string(const char s)* | 用 C 风格字符串初始化 |
| 字符序列构造 | string(const char s, size_t n)* | 取字符数组的前 n 个字符 |
| 填充构造 | string(size_t n, char c) | 构造包含 n 个字符 c 的字符串 |
3.2 测试代码
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1; // (1) 默认构造:空串
string s2("hello bit"); // (4) C字符串构造
string s3(s2); // (2) 拷贝构造
string s4(3, 'x'); // (6) 填充构造:"xxx"
string s5 = "hello bit"; // 隐式类型转换构造
string s6("hello world", 5);// (5) 取前5个字符:"hello"
cout << "s1: [" << s1 << "]" << endl;
cout << "s2: " << s2 << endl;
cout << "s3: " << s3 << endl;
cout << "s4: " << s4 << endl;
cout << "s5: " << s5 << endl;
cout << "s6: " << s6 << endl;
return 0;
}
打印结果:如下图
s1: \[\] s2: hello bit s3: hello bit s4: xxx s5: hello bit s6: hello

3.3 npos 是什么(重点补充)
先说结论:
npos 是 string 的静态成员常量,值为 size_t 的最大值(size_t 是无符号类型,-1 转过去就是最大值)。用作参数时表示"直到字符串末尾",用作返回值时表示"没找到"。
很多接口都默认带pos参数,比如子串构造string(str, pos, len = npos)------len 默认就是 npos,意思是"不指定就从 pos 取到结尾"。这个老朋友在查找接口**(find)**里还会再见面。
四、三大遍历方式(一条龙)
遍历 string 有三种方式,它们的讲解顺序是精心设计的:operator\[\](最朴素)→ 迭代器(最通用,但类型名长)→ auto(解决类型名长的痛点)→ 范围for(auto + 迭代器的集大成)。学完你会发现它们环环相扣,不是三个孤立的知识点。
4.1 第一种:operator\[\] + 下标

先说结论:
string 本质是"字符数组",可以像数组一样用 下标 访问和修改每个字符。下标范围是 [0, size)。
在正式遍历前,先补两个小接口:
| 接口 | 作用 | 说明 |
|---|---|---|
| size() | 返回字符串有效字符个数 | 不含 '\0' |
| c_str() | 返回 C 风格字符串指针(const char*) | 和 C 库函数配合时用 |


截图皆来自:<string> - C++ Reference
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("123456789");
// 用 size() 控制下标范围
for (size_t i = 0; i < str.size(); ++i)
cout << str[i] << " ";
cout << endl;
// operator[] 还支持修改
str[0] = 'A';
cout << str << endl; // A23456789
// c_str():和 C 库函数配合
printf("%s\n", str.c_str());
return 0;
}
打印结果:如图
1 2 3 4 5 6 7 8 9 A23456789 A23456789

坑点:operator\[\] 越界访问不会报编译错误,运行时直接崩(assert 断言失败)。数组可没这种"安全检查"。
4.2 第二种:迭代器
4.2.1 迭代器是什么、为什么存在
先说结论:
迭代器是一个"抽象的访问器",用统一的方式顺序访问容器元素,而不暴露容器的底层存储结构。它的用法和指针非常像,但底层不一定是指针。
为什么要费劲发明迭代器?因为每个容器的存储结构不同 (string 是数组、list 是链表、map 是红黑树),如果每个容器都用自己的一套遍历方式,程序员每学一个容器就要学一套新语法。迭代器把遍历统一成一套:begin() 拿起点、++往后走、*取元素、end() 判断结束------一次学会,处处通用。
**string 底层是数组,所以 string 的迭代器底层就是指针;**但后面学的 list、map 的迭代器底层就不是指针了。
4.2.2 迭代器四兄弟
我们先理解一下这一堆接口:

| 函数 | 说明 |
|---|---|
| begin() / end() | 正向迭代器:begin 指向第一个字符,end 指向最后一个字符的下一个位置 |
| rbegin() / rend() | 反向迭代器:rbegin 指向最后一个字符,rend 指向第一个字符的前一个位置 |
| cbegin() / cend() | const 正向迭代器(只读),C++11 |
| crbegin() / crend() | const 反向迭代器(只读),C++11 |
4.2.3 测试代码(逐步分析)
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("hello");
string::iterator it = str.begin(); // 1.拿到指向首字符的迭代器
while (it != str.end()) // 2.end() 是结束标志
{
cout << *it << " "; // 3.解引用拿到字符
++it; // 4.迭代器向后移动
}
cout << endl;
return 0;
}
逐步分析:
- **
begin()**返回一个指向字符串首字符的迭代器,交给it - 循环条件**
it != end():**end() 是最后一个字符的下一个位置,it 走到那说明遍历完了 *it解引用,拿到当前指向的字符++it让迭代器向后走一个字符
**打印结果:**h e l l o
反向迭代器同理:
cpp
string::reverse_iterator rit = str.rbegin(); // 指向最后一个字符
while (rit != str.rend()) // rend 是第一个字符的前一个位置
{
cout << *rit << " ";
++rit; // 注意:反向迭代器 ++ 是往前走
}
打印结果:
o l l e h

4.2.4 迭代器的注意事项
- 永远是左闭右开区间 :[begin(), end())、[rbegin(), rend())
- const 迭代器和 const 修饰的迭代器不是一回事 :const 迭代器(
const_iterator)是它指向的内容不能改 ;const 修饰的迭代器(const string::iterator)是这个迭代器本身不能改(不能 ++,一般没人这么用) - 迭代器失效问题等学到 vector 细讲,先埋个伏笔
到这里,你应该已经开始觉得哪里不对劲了:string::iterator 这个类型名怎么这么长? 别急,下一个主角就是来解决这个痛点的。
4.3 第三种:auto------类型太长?交给编译器
先说结论:
auto 是类型指示符,编译器在编译期根据初始化表达式推导变量类型。它最大的用武之地,就是上一节我们刚体验过的"类型名长到离谱"的场景。
刚写完 string::iterator it = str.begin(),再把迭代器类型替换成 auto:
cpp
string str("hello");
// 完整写法:类型名很长
string::iterator it1 = str.begin();
// auto 写法:让编译器自己推导
auto it2 = str.begin();
两者完全等价。如果觉得 string::iterator 还不够长,看看 map 的迭代器------没有 auto 的时候,一行都写不下:
cpp
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main()
{
map<string, string> dict = { { "apple", "苹果" }, { "orange", "橙子" }, { "pear", "梨" } };
// 没有 auto:一行写不下,手都写抽筋
// map<string, string>::iterator it = dict.begin();
auto it = dict.begin(); // 有 auto:爽!
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
return 0;
}
打印结果:
apple:苹果 orange:橙子 pear:梨

auto 的使用规则(重点):
cpp
int x = 10;
auto y = &x; // y 是 int*,auto 和 auto* 没区别
auto* z = &x; // z 是 int*,同上
auto& m = x; // m 是 int&,声明引用必须加 &
auto aa = 1, bb = 2; // OK:同一行类型必须一致
// auto cc = 3, dd = 4.0; // error:编译器只对第一个类型推导,3 和 4.0 类型不同
// auto e; // error:auto 必须初始化
// auto array[] = {4,5,6}; // error:auto 不能直接声明数组
坑点总结:
- auto 声明的变量必须初始化------编译器要"看见"初始值才能推导
- 同一行多个 auto 变量类型必须相同
- auto 声明引用要加 &;声明指针 auto和 **auto***等价
- auto 不能做函数参数(void f(auto a) 报错);可以做返回值,但建议谨慎使用(降低了可读性)
- auto 不能直接声明数组
如何查看推导出来的类型?用typeid(x).name():
cpp
int a = 10;
auto b = a; // int
auto c = 'a'; // char
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
打印结果
int char

顺带说一句:auto 不只是给迭代器用的,它适用于一切类型推导场景(比如 auto d = 3.14;),只不过在 STL 里它的主场就是迭代器。
4.4 第四种:范围for------全自动遍历
先说结论:
范围 for 自动迭代、自动取数据、自动判断结束。括号里冒号左边是迭代变量,右边是被迭代的范围。学完 4.2 再看它:范围 for 的底层就是迭代器------如果说迭代器是半自动机,范围 for 就是全自动机。
还记得 4.2 的迭代器遍历吗?范围 for 把它浓缩成了一行:
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("hello world");
for (auto ch : str) // 自动取出每个字符给 ch
cout << ch << " ";
cout << endl;
int arr[] = { 1, 2, 3, 4, 5 };
for (auto e : arr) // 数组也能范围for
cout << e << " ";
cout << endl;
return 0;
}
打印结果:
h e l l o w o r l d
1 2 3 4 5

4.4.1 一道经典题引入:范围for修改元素
下面代码想通过范围 for 把字符串每个字符都改成 'x',看看结果:
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("hello");
for (auto ch : str) // 想修改每个字符
ch = 'x';
cout << str << endl; // 打印出来是什么?
return 0;
}
打印结果:
hello
傻眼了吧,根本没改!为什么?
原因与原理: 范围 for 底层会被编译器替换成迭代器遍历**(4.2 讲的),ch 拿到的是 *it 的一份拷贝,改拷贝当然影响不到原字符串。**
解决办法: 把 auto 改成 auto&------ch 变成字符串里字符的引用(别名),改 ch 就是改原字符:
看前后运行结果:
原则:要修改集合里的元素,迭代变量必须用引用 auto&;对象比较大的话,也建议加引用(避免拷贝的开销)。
4.4.2 三大遍历方式小结
| 方式 | 适合场景 |
|---|---|
| operator\[\] | 需要按下标访问(比如第 i 个字符) |
| 迭代器 | 通用写法,STL 全家桶通用 |
| 范围 for | 从头到尾无脑遍历(最常用) |
五、最终总结
- C 字符串之痛:函数与数据分离、空间自己管、容易越界------string 一站式解决
- 一张图记住: → basic_string 模板 → string/wstring/u16string/u32string
- 构造重点:默认构造、C 字符串构造、拷贝构造;npos 是 size_t 最大值
- 遍历一条龙:operator\[\] 按下标(最朴素)→ 迭代器(最通用,类型名长)→ auto(救星)→ 范围for(auto + 迭代器集大成)
- 迭代器左闭右开;const_iterator 是内容只读;用法像指针,底层不一定是指针
- auto:编译期推导、必须初始化;不能做参数、同一行类型要一致、不能声明数组
- 范围 for:全自动遍历;修改元素必须用 auto&,否则改的是拷贝
好了本期内容就到这里。下篇进入接口大全 ------容量、增删改查,每个接口配测试代码和打印结果,跟着敲一遍就能上手。如果对你有帮助,不要忘记点赞三连一波哦,我是白杨,我们下期见。