前言:为什么学习string类?
C++的
std::string相比于C语言字符串,核心优势在于自动内存管理、内置丰富操作函数、安全性高(避免缓冲区溢出)和编码便捷性,它让你告别手动分配/释放内存、拼接复制时的繁琐与风险,是现代C++编程中高效处理文本的必备工具,显著提升开发效率和代码健壮性。

啥?看不懂?说白了就俩字:方便!
当然了,**"方便"本质其实是其功能强大,库函数帮我们实现了很多的功能。而因为其多样的功能,我们学起来可能也稍微有一些挑战,希望大家可以一鼓作气拿下string。**OK,多说无益,我们直接开讲。
1.string类的常用接口说明
1.1 string类对象的常见构造
|------------------------------|-----------------------|
| 函数名称 | 功能说明 |
| string() (重点) | 构造空的string类对象,即空字符串 |
| string(const char* s) (重点) | 用C-string来构造string类对象 |
| string(size_t n, char c) | string类对象中包含n个字符c |
| string(const string&s) (重点) | 拷贝构造函数 |
举个例子:s1对应第一行的string(),s2对应第二行的string(const char* s)......以此类推:
cpp
string s1;
string s2("521020");
string s3(3,'c');
string s4(s2);
cout<<s1<<endl<<s2<<endl<<s3<<endl<<s4;
运行结果,第一行是s1,因为是空字符串,并未输出:

1.2 string类对象的容量操作
|--------------|------------------------------|
| 函数名称 | 功能说明 |
| size(重点) | 返回字符串有效字符长度 |
| capacity | 返回空间总大小 |
| empty (重点) | 检测字符串释放为空串,是返回true,否则返回false |
| clear (重点) | 清空有效字符 |
| reserve (重点) | 为字符串预留空间** |
| resize (重点) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
按照咱们的惯例,肯定是要每一个都要举例的,但是一个一个举例太长了,我举一个例子,里面包含所有操作用法,为方便大家看,已经详细注释:
cpp
int main()
{
string s1("521020");//构造s1字符串
cout <<"初始的s1:" << s1 << endl;
cout <<"字符串有效长度:" << s1.size()<<endl;//s1字符串有效字符长度
cout <<"字符串空间总大小:" << s1.capacity()<<endl;//s1空间总大小
s1.reserve(20);//为字符串预留空间大小为20
cout << "reserve后字符串空间总大小:" << s1.capacity() << endl;//s1空间总大小为31
s1.resize(10,'x');//将s1有效长度改为10,不足10用x填充
cout << "resize后的s1:" << s1 << endl;
s1.clear();//清空字符串中有效字符
cout <<"clear后的s1(看不到正常,因为清空了):" << s1<<endl;
}
运行结果如下:

注意:
size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。(因此咱们不讲length)
clear()只是将string中有效字符清空,不改变底层空间大小。
resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不 同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数 增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参 数小于string的底层空间总大小时,reserver不会改变容量大小。
至于为什么s1的有效长度是6,总长度却是15?想了解的同学直接去看疑难解答部分。(这里不得不提一句,我们的string是可以自动增长的,reserve一般用在需要频繁向堆申请内存的情况)。
1.3 string类对象的访问及遍历操作
|--------------|-----------------------------------------|
| 函数名称 | 功能说明 |
| operator[] | 返回pos位置的字符,const string类对象调用 |
| begin+ end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位 置的迭代器 |
1.3.1 operator[]访问字符串
高大上吧?其实就是像C语言中数组的访问方式(当然,string也是靠数组实现的):
cpp
string str = "Hello World";
// 访问单个字符(像数组一样)
cout << str[0]; // H(第一个字符)
cout << str[1]; // e(第二个字符)
cout << str[6]; // W(第七个字符)
// 修改字符
str[0] = 'h'; // 改成小写
cout << str; // "hello World"
// 注意:不会检查越界!
// str[100] = 'x'; // ❌ 可能崩溃或产生未定义行为
1.3.2 begin+ end 访问string
cpp
string str = "Hello";
//迭代器遍历
for(string::iterator it = str.begin(); it != str.end(); ++it) {
cout << *it << " "; // H e l l o
}
看不懂迭代器的同学移步疑难解答,博主当然贴心的配上了讲解
4.string类对象的修改操作
|------------------|----------------------------------|
| 函数名称 | 功能说明 |
| push_back | 在字符串后尾插字符 |
| append | 在字符串后追加一个字符串 |
| operator+= (重 点) | 在字符串后追加字符串str |
| c_str(重点) | 返回C格式字符串 |
| find + npos(重 点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的 位置 |
| rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的 位置 |
| substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
下面给大家举例说明:
cpp
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1("123");//构造s1字符串
string s2("000");//构造s2字符串
cout <<"初始的s1:" << s1 << endl;
s1.push_back('4');//尾部添加'4'
cout <<"push_back('4')后的s1:" << s1<<endl;
s1.append("567");//尾插字符串"567"
cout <<"append("567")后的s1:" << s1<<endl;
s1 += s2;
cout << "+=s2('000')后的s1:" << s1 << endl;
size_t pos1 = s1.find('3');//寻找s1中第一个3的位置
if (pos1 != string::npos)//相等即没找到
{
cout << "第一个3的位置为:" << pos1 << endl;
}
size_t pos2 = s1.rfind('0');//rfind的作用是逆序查找
cout << "最后一个0的位置为:" << pos2<<endl;
string s3;
s3 = s1.substr(0, 5);//截取s1的0-5部分
cout << "截取s1的0-5部分:" << s3 << endl;
}
c_str我们这里不进行演示,但其让我们在使用C++便利的同时依旧不失去与整个C语言生态系统交互的能力。它是 C++ 向后兼容和实用性的重要体现!
注意:
在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差 不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可 以连接字符串。
对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
2.疑难解答
2.1 有效长度和总空间大小的区别及相关操作的用法
我们依旧例子起手:
2.1.1 size 和 capacity 的区别
cpp
string hotel = "张三李四"; // 住了2个人
size() = 实际住的人数 (2人)
capacity() = 酒店总房间数(比如10间房,还能住8人)
2.1.2 string的自动增长
人满了就换酒店
cpp
// 一开始住2个人,酒店有10间房
hotel += "王五"; // 加1人,现在3人,房间够用
hotel += "赵六钱七孙八周九"; // 再加5人,现在8人,还够
hotel += "再来10个人!"; // 💥 爆炸!10+8=18人 > 10间房
这时自动增长启动:
找新酒店(申请新内存)
所有人搬过去(复制数据)
新酒店更大(容量变成20间)
继续住人
大家结合下面的代码理解:
cpp
#include <iostream>
#include <string>
using namespace std;
int main() {
string str;
cout << "初始状态:" << endl;
cout << "size() = " << str.size() << endl; // 0
cout << "capacity() = " << str.capacity() << endl; // 15(常见实现的小字符串优化)
// 添加字符,观察容量变化
for(int i = 0; i < 100; i++) {
str.push_back('a');
if(i % 10 == 0) {
cout << "size=" << str.size()
<< ", capacity=" << str.capacity() << endl;
}
}
/* 典型输出(GCC实现):
初始状态:size=0, capacity=15
size=1, capacity=15
size=11, capacity=15
size=21, capacity=31 ← 容量翻倍增长
size=31, capacity=47
size=41, capacity=70 ← 开始以1.5倍左右增长
size=51, capacity=70
size=61, capacity=105
size=71, capacity=105
size=81, capacity=158
size=91, capacity=158
*/
return 0;
}
2.1.3 reserve的运用情景
不知道认真思考的你有没有想过:string可以自动增长,那我们为什么还要reserve()去扩容?我们的string是可以自动增长的,reserve一般用在需要频繁向堆申请内存的情况以避免频繁的自动增长带来的性能损耗。
测试代码:
cpp
#include <iostream>
#include <string>
#include <chrono>
using namespace std;
// 测试:没有使用reserve的自动增长
void test_without_reserve() {
string s;
auto start = chrono::high_resolution_clock::now();
for(int i = 0; i < 1000000; i++) {
s += "x"; // 可能触发多次重新分配
}
auto end = chrono::high_resolution_clock::now();
auto duration = chrono::duration_cast<chrono::microseconds>(end - start);
cout << "没有reserve: " << duration.count() << " 微秒" << endl;
}
// 测试:使用reserve预分配
void test_with_reserve() {
string s;
s.reserve(1000000); // 一次性分配足够空间
auto start = chrono::high_resolution_clock::now();
for(int i = 0; i < 1000000; i++) {
s += "x"; // 不会触发重新分配
}
auto end = chrono::high_resolution_clock::now();
auto duration = chrono::duration_cast<chrono::microseconds>(end - start);
cout << "使用reserve: " << duration.count() << " 微秒" << endl;
}
int main() {
test_without_reserve();
test_with_reserve();
return 0;
}
编译器比较先进的同学可能看不出来效果,因为被优化了。
2.2 迭代器的初步认识
迭代器就是 C++ 中的"智能指针" ,它让你能像指针一样遍历容器(如
string、vector、list等),但更安全、更统一。
begin()≈ 数组首地址
end()≈ 数组尾后地址
*it≈ 解引用获取值
it++≈ 指针向后移动
cpp
// C语言方式(用指针遍历数组)
int arr[] = {1, 2, 3, 4, 5};
int* p = arr; // 指针指向开头
while(p != arr + 5) { // arr+5 是"尾后位置"
cout << *p << " "; // 解引用获取值
p++; // 移动到下一个
}
// C++方式(用迭代器遍历string)
string str = "Hello";
auto it = str.begin(); // 迭代器指向开头
while(it != str.end()) { // end() 是"尾后位置"
cout << *it << " "; // 解引用获取字符
it++; // 移动到下一个
}
其实在这里没有太直观的感受到迭代器方便是正常的,我们下面以链表举例:
C语言指针方式(原始指针)
cpp
void traverse_list(CNode* head) {
CNode* current = head; // 手动管理指针
while(current != NULL) { // 必须知道结束条件是NULL
printf("%d ", current->data);
current = current->next; // 必须知道"next"字段名
}
}
C++迭代器方式
cpp
#include <list>
void traverse_list(list<int>& mylist) {
for(auto it = mylist.begin(); it != mylist.end(); ++it) {
cout << *it << " "; // 统一的语法,与容器类型无关
}
}
一句话 :迭代器让C++拥有了通用、类型安全、高效的算法框架,这是原始指针无法比拟的!
string就到此结束了,各位学会了不要忘了练习。
学会了就给博主点个赞呗?(✪ω✪)
---------(如有问题,欢迎评论区提问)---------
