【C++】STL介绍 + string类使用介绍 + 模拟实现string类
-
目录
前言
一、STL简介
二、string类
1.为什么学习string类
2.标准库中的string类
3.auto和范围for
4.迭代器
5.string类的常用接口说明
三、模拟实现 string类

前言
本文带大家入坑STL,学习第一个容器string。
一、STL简介
在学习C++数据结构和算法前,我们需要先了解C++的STL,方便后续学习其他数据结构
1.什么是STL?
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
2.STL的版本
- 原始版本:Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许 任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本--所有STL实现版本的始祖。
- P. J. 版本:由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。
- RW版本:由Rouge Wage公司开发,继承自HP版本,被C++ Builder 采用,不能公开或修改,可读性一般。
- SGI版本:由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程风格上看,阅读性非常高。我们后面学习STL要阅读部分源代码,主要参考的就是这个版本。
3.STL的六大组件

以上内容后续都会了解到,总之我们要明白STL的重要性。STL在C++的笔试和面试中占比很大,在工作上更是“不懂STL,不要说你会C++”。STL是C++中的优秀作品,有了它的陪伴,许多底层的数据结构以及算法都不需要自己重新造轮子,站在前人的肩膀上,健步如飞的快速开发。
二、string类
1.为什么学习string类
- 原字符串的缺陷:C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想(封装、继承和多态),而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
- 实用性:在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、 快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
2.标准库中的string类
在使用string类时,必须包含#include <string>头文件,平时学习可使用using namespace std;展开命名空间方便使用
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("hello world");//定义对象s1并初始化cout << s1 << endl;//打印输出return 0;
}
简单说明下命名空间std 与 头文件 还有 STL 之间的联系:
- 命名空间std 是C++标准库的命名空间,也就是C++编程的重要组成部分,它不仅包含了STL的所有组件,也包括了更多的东西。例如输入输出流(cin,cout)、容器(string,vector,list,map等)、迭代器、智能指针、内存管理工具、算法等
- 所以我们平时使用库函数和容器等,如果不使用using namespace std;展开这个命名空间的话,就需要在前面指定命名空间std::。
- 头文件的包含,比如<iostream>和<strintg>,你实际上是在告诉编译器你想要使用该头文件中定义的功能,这些功能都是 std 这个命名空间的一部分,因此可以说,我们是通过包含不同的头文件来解锁和访问 std 命名空间中不同部分的内容。
- 不过需要注意的是,C++语言本身不仅仅是由 std 命名空间组成。C++的核心内容还包括:语言语法、基础数据类型、内存模型和管理、面向对象编程特性、模版和泛型编程、异常处理。
- 切记,关键字不是 std 中定义的,C++关键字是语言本身的一部分,它们不是由标准库提供的,而是直接由编译器识别的。
继续了解string类
其实严格意义上,string不属于容器,在下图容器分类中就没有看到string
这是因为string在STL之前就已经有了,因为在设计上与STL中容器很相似,因此就有 串 这么一个数据结构,后面使用方法中就可以看出 string 设计的方法非常冗余,因为要照顾旧方法同时又要融入STL。
基础串
string类其实是一个类模版,它的原模版就叫 basic_string(基础串)
在基础串模版中,后两个参数有默认的模版参数,string 的定义中给基础串的第一个参数传递了 char 并进行了重命名,所以我们创建 string 模版类时没有给定模版参数
当然,除了经常使用的 string 类外,还有另外两个不同的string类:
这两个不同的string类,一个是一个字符占2个字节,另一个是4个字节,这里大小不同的原因是因为编码不同。编码在下文学习完string的使用后会讲到。这里主要是讲为什么要搞一个基础串的模版,原因就在这里。
不管怎样,我们最常用的还是前面的string,主要因为它兼容的编码多。
深入学习string前先学习两个语法糖
3.auto和范围for
1. auto关键字
#include <iostream>
using namespace std;int main()
{int a = 1;auto b = a;//根据表达式右边自动推导出b的类型cout << b << endl;cout << sizeof(b) << endl;return 0;
}
运行结果:

auto使用注意事项:(C++11)
- 在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
- auto不能作为函数的参数,可以做返回值(c++11支持),但是建议谨慎使用。
- 补充:c++20开始,支持 auto 作为函数参数类型
- auto不能直接用来声明数组
例如:
(1)推导指针类型
#include <iostream>
using namespace std;int main()
{//自动推导指针int a = 10;auto b = &a;cout << b << endl << &a << endl;return 0;
}
运行结果:

(2)推导引用类型
#include <iostream>
using namespace std;int main()
{//引用类型推导int a = 10;int& b = a;auto& c = b;cout << &a << endl;cout << &c << endl << endl;//不加&推导出来的不是引用类型,而是原数据类型auto d = b;cout << &d << endl;return 0;
}
运行结果:

(3)可做函数返回值,但不能做参数
//auto做返回值
auto func1()
{int a = 10;return a;
}//auto做参数
//报错:error C3533: 参数不能为包含“auto”的类型
//int func2(auto x)
//{
// int a = x;
// return a;
//}
但auto作为返回值有时候会是个坑,因为如果代码复杂,维护时会导致无法快速判断该函数返回值,写了函数注释还好,没写就会大大增加代码维护成本,所以慎用。
2.范围for
语法:
for(类型 e : 容器)
{
//每循环一次e自动指向下一个数据
//直到容器遍历完成
}
#include <iostream>
#include <string>
using namespace std;int main()
{//范围for用于遍历容器string s1("hello world");for (char c : s1){cout << c << " ";}cout << endl;//可配合auto使用//如果想改变容器内容,需要使用引用类型for (auto& c : s1){++c;}cout << s1 << endl;return 0;
}
运行结果:

范围for用处总结:
- 对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11 中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
- 范围for可以作用到数组和容器对象上进行遍历
- 范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
4.迭代器
- 迭代器,STL六大组件之一,关于迭代器的介绍,C++迭代器是一种用于遍历容器(如数组、链表、向量等)中元素的工具。它们提供了统一的接口,使得不同类型的容器可以以相似的方式进行访问和操作。
- 我们现阶段可以先理解迭代器为一种指针,但本质上不是指针,我们先学会使用
常见迭代器:
- iterator
- 常量迭代器 const_iterator
- 反向迭代器 reverse_iterator
- 常量反向迭代器 const_reverse_iterator
声明迭代器时,一般是 std::容器名(如果是模版需要模版参数):: iterator 对象名。
展开了命名空间std就可省略,因为不同容器迭代器底层实现不同,因此需要指定容器
例如:利用迭代器遍历string
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("hello world");//迭代器遍历string::iterator it = s1.begin();while (it != s1.end()){cout << *it << " ";++it;}cout << endl;return 0;
}
运行结果:

解释:
首先 string类的接口 begin()和end():


- 我们发现,它们的返回值就是迭代器,它们的作用就是返回容器的头部与尾部的迭代器,对于string来说,end()指向的就是'\0',begin()指向的就是下标为0的字符。
在使用上,用法和指针相似:
- 使用
*运算符解引用迭代器以访问它所指向的元素。 - 使用->运算符访问指向的对象的成员(如果该对象是一个类或结构体)。
- 可以使用++或--来前进或后退迭代器。
注意:判断迭代器是否走到容器的结尾是使用 != 容器.end() 来判断,而不是其他关系判断,另外,一般使用while循环遍历,for循环虽然也行,但写法上相较复杂点。
迭代器的全部接口:

r开头的就是支持反向迭代器,c开头的就是就是常量迭代器,但是前面我们注意到了,begin 和 end 都有重载const版本的,在 rbegin 和 rend 中一样都有重载 const 版本的(注意,这种成员函数重载是根据 const 区分的,不是参数)。因此,我们一般不使用c开头的以及cr开头的迭代器接口。


- rend() 指向的是第一个元素前一个位置
- rbegin() 指向的是最后一个元素的位置,对于string来说,就是'\0'前一个字符
- 因为倒着遍历还是从 rbegin 开始,一直到 != rend() 结束,因此这种安排合理
剩余三种迭代器遍历演示:
#include <iostream>
#include <string>
using namespace std;int main()
{//1.反向迭代器string s1("hello world");string::reverse_iterator rit = s1.rbegin();while (rit != s1.rend()){cout << *rit << " ";++rit;}cout << endl;//2.const迭代器const string s2("hello world");//string::const_iterator cit = s1.begin();auto cit = s2.begin();//使用前面所学的auto自动识别类型更加方便while (cit != s2.end()){cout << *cit << " ";++cit;}cout << endl;//3.const反向迭代器auto crit = s2.rbegin();while (crit != s2.rend()){cout << *crit << " ";++crit;}cout << endl;return 0;
}
运行结果:

需要注意的是:反向迭代器虽是倒着遍历,但依旧是使用++使迭代器指向下一个元素。因为对于反向迭代器来说,它正方向就是从右往左。
另外,迭代器与指针类似,当然也可以修改非const容器对象的内容
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("hello world");auto it = s1.begin();while (it != s1.end()){++(*it);++it;}cout << s1 << endl;return 0;
}
运行结果:

小结:
迭代器的是所有的STL容器通用的一种元素访问方式,不同的容器,迭代器底层会有些不同,但是用法是一样的,因此学好迭代器很重要
5.string类的常用接口说明
强调,C++为了适配C语言,因此 string 类对象的末尾也是存在 '\0' 的
1.string类的常见构造

上图中:
- (1)就是不传参的默认构造
- (2)就是拷贝构造
- (3)从string对象 str 的 pos(下标) 位置开始,拷贝 len(默认 nops)个字符进行构造
- (4)使用字符串进行构造
- (5)使用字符串 s 的前 n 个字符进行构造
- (6)用 n 个相同字符 c 进行构造
- (7)使用迭代器进行构造
下面演示一下(3)(4)(5)(6)(7)
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("hello world");//使用字符串初始化构造cout << "s1:" << s1 << endl;string s2(s1, 6, 5);//利用string对象的下标+长度进行构造cout << "s2:" << s2 << endl;string s3("xxxxxxxxxxxx", 4);//使用字符串的前4个字符进行构造cout << "s3:" << s3 << endl;string s4(5, 'y');//用n个相同字符进行构造cout << "s4:" << s4 << endl;string s5(s1.begin(), s1.end() - 5);//利用迭代器进行构造cout << "s2:" << s5 << endl;return 0;
}
运行结果:

赋值运算符重载:

演示:
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("111");string s2("222");//(1)s1 = s2;cout << s1 << endl;//(2)s1 = "333";cout << s1 << endl;//(3)s1 = '*';cout << s1 << endl;return 0;
}
运行结果:

补充:npos和析构
(1)npos
- npos 是 string 类的一个静态成员变量,无符号整形并且等于-1,因此就是整形的最大值(2进制位全是1),常用作缺省参数,表示最大值。
(2)string 类的析构
类的析构函数会在对象作用域结束时自动调用,用于销毁对象
2.string类对象的容量操作
| 函数名称 | 简要功能说明 |
| size | 返回字符串有效字符长度 |
| length | 返回字符串有效字符长度 |
| max_size | 返回字符串可以达到的最大长度。 |
| resize | 将有效字符的个数该成n个,多出的空间用字符c填充 |
| capacity | 返回空间总大小 |
| reserve | 为字符串预开辟空间 |
| clear | 清空有效字符 |
| empty | 检测字符串释放为空串,是返回true,否则返回false |
| shrink_to_fit | 缩容,减小字符串容量以适应其大小 |
(1)size和length
- size和length的功能相同,都是返回字符串有效字符个数,而这样设计的原因是历史原因导致的,主要就是STL出来之前,string已经存在了。为了保留原string接口,同时为了和STL其余容器保持通用性,因此设计了size,其余STL容器都是size返回有效元素个数。对于string,我们平时也基本是使用size,而不是length。
演示:
(2)max_size
- 这个用处不大,编译器也开不了这么大的空间。
演示:(64位)
(3)capacity
返回空间容量,无需多言
演示:

(4)resize
将字符串大小调整为 n 个字符的长度,那么这里就有3中情况:
- 如果 n 小于当前字符串长度,则当前值将缩短为其前 n 个字符,并删除第 n 个字符以外的字符。
- 如果 n 大于当前字符串长度却又小于当前空间容量,则将当前字符串大小调整为 n,指定了c,则新元素将初始化为 c ,未指定则初始化为'\0'
- 如果 n 大于当前空间容量,则需要扩容,然后初始化新元素,新元素处理同上
演示:
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("11111111111111111111");cout << s1 << endl;cout << "size: " << s1.size() << endl;cout << "capacity: " << s1.capacity() << endl;cout << endl;//resize//1.n < sizes1.resize(10);cout << s1 << endl;cout << "size: " << s1.size() << endl;cout << "capacity: " << s1.capacity() << endl;cout << endl;//2.size < n < capacitys1.resize(25,'x');cout << s1 << endl;cout << "size: " << s1.size() << endl;cout << "capacity: " << s1.capacity() << endl;cout << endl;//3.n > capacitys1.resize(40, 'y');cout << s1 << endl;cout << "size: " << s1.size() << endl;cout << "capacity: " << s1.capacity() << endl;cout << endl;return 0;
}
运行结果:

(5)reserve
- 更改空间容量,如果 n 大于当前字符串容量,则该函数会导致容器将其容量增加到 n 个字符(或更大)。此函数对字符串长度没有影响,也无法更改其内容。因此reserve不能缩小容量,也就是 n 小于当前字符串容量,没有什么实际效果。
- 适用场景:提前知道大概需要多少空间,提前开辟可以避免多次扩容,提升效率。
演示:

拓展:
我们观察string类对象每次扩容的大小:
#include <iostream>
#include <string>
using namespace std;int main()
{string s1;size_t old = s1.capacity();cout << "capacity: " << old << endl;for (size_t i = 0; i < 100; i++){s1 += 'x';if (s1.capacity() != old){cout << "capacity: " << s1.capacity() << endl;old = s1.capacity();}}return 0;
}
运行结果:

- 我们发现:除了第一次到第二次是2倍扩容以外,31以后就是 1.5 倍扩容了。
- 首先,这个底层扩容倍率每个平台是不一定一样的,以上是vs2022的结果。
- 然后为啥第一次不是1.5倍扩容的原因:string 底层还存在一个类似 char buff[16] 大小的字符数组,如果数据小于16的话就会存在这个数组里面,大于16就储存在堆上开的空间中。这样做是为了避免存储数据小时频繁开辟空间。所以第一次的容量 15 不算是扩容。
我们可以通过计算空间大小验证一下:(32位)

- 28的由来:底层字符串指针 4 字节、底层 size 和 capacity 记录大小和容量的无符号整形一共占 8 字节、剩下的 16 个字节就是 char buff[16] 数组。
我们在调试窗口中也能观察到该数组:

(5)clear
清空有效字符,对应字符串来说,就是将'\0'移动到第一位

(6)empty
- 判空,为空返回ture(1),反之返回false(0)。

(7)shrink_to_fit
- 缩容,将容量缩小与有效字符一样大的空间,注意,该函数不是任意情况下都会进行缩容,而是当capacity 与 size 相差过大时才会缩容。

3.string类对象的访问接口
(1)operator[ ]
- 运算符重载函数,返回对应下标的引用(越界会直接报错)
- 最常用的元素访问接口
演示:
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("hello world");cout << s1[4] << endl;s1[4] = 'x';//因为返回的是引用类型,因此修改可直接影响原对象cout << s1[4] << endl;return 0;
}
运行结果:
配合 size()接口,可以实现遍历string类对象:
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("hello world");for (size_t i = 0; i < s1.size(); i++){cout << s1[i] << " ";}cout << endl;return 0;
}
运行结果:
(2)at
- at 功能大致与 operator[ ] 相同,区别是 at 访问失败会抛出异常,而 [ ] 是直接断言报错
演示:关于捕获异常的知识,我会在后续篇章中单独讲解

(3)back 和 front
- back 和 front 分别是返回字符串第一个字符和最后一个字符,因为这些 [ ] 也可以轻松做到,所以这两个接口用的不多,访问元素用的最多的就是 [ ]。
演示:

4.string类对象的修改操作
| 函数名称 | 功能说明 |
| operator+= | 在字符串后追加字符或字符串 |
| append | 在字符串后追加一个字符串 |
| push_back | 尾插一个字符 |
| assign | 为字符串分配一个新值,替换其当前内容 |
| insert | 在指定位置前插入字符或字符串 |
| erase | 删除指定位置字符 |
| replace | 替换指定位置字符 |
| swap | 交换两个字符串 |
| pop_back | 尾删一个字符 |
(1)operator+=
- 我们可以直接尾插一个string类对象,或者一个字符串,或者一个字符
- += 是字符串尾插中运用最多的接口
演示:

- 除了+=以外,string 也重载了 + 运算符,区别就是不会修改本身,返回值为 + 的结果:

(2)push_back
- 尾插一个字符
演示:

(3)append
append 重载了许多函数,功能都是尾插一段字符串:
- (1)尾插一个 string 类对象
- (2)从待尾插 string 对象的 subpos 位置开始,尾插 sublen 个字符
- (3)尾插一段字符串
- (4)尾插一段字符串的前 n 个字符
- (5)尾插 n 个相同的字符 c
- (6)以迭代器的方式,尾插一段字符串
演示:
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("111");string s2("xxxx");string s3("hello world");//(1)s1.append(s2);cout << s1 << endl;//(2)s1.append(s3, 0, 5);cout << s1 << endl;//(3)s1.append("world");cout << s1 << endl;//(4)s1.append("yyyyyyyyyyy", 3);cout << s1 << endl;//(5)s1.append(2, 'a');cout << s1 << endl;//(6)s1.append(s3.begin() + 5, s3.end());cout << s1 << endl;return 0;
}
运行结果:

(4)insert
- insert 支持头插以及中间指定位置之前插入元素,重载了很多函数,类比构造和append函数,其实不难看出每种重载函数的用法,以下不一一列举了
- 提醒:insert 进行头插和中间插入时,需要挪动数据,因此效率低下,不建议多次使用。
演示:
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("111");string s2("22");string s3("xxxxxx");string s4("orld");//(1)s1.insert(0, s2);cout << s1 << endl;//(2)s1.insert(3, s3, 0, 2);cout << s1 << endl;//(3)s1.insert(s1.size(), "hello");cout << s1 << endl;//(4)s1.insert(0, "yyyyyyyyyyyyy", 3);cout << s1 << endl;//(5)s1.insert(0, 4, 'm');cout << s1 << endl;//(6)s1.insert(s1.end(), 'w');cout << s1 << endl;//(7)s1.insert(s1.end(), s4.begin(), s4.end());cout << s1 << endl;return 0;
}
运行结果:

(5)erase
erase 用于删除字符:
- (1)缺省参数 0 和 npos,npos前面说过是整形最大值,也就是说什么都不传,默认全部删除(效果和 clear 一样),传参则按照指定位置大小删除。
- (2)删除迭代器位置的字符
- (3)删除迭代器区间的字符
演示:
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("I want to be a C++ master");//(1)s1.erase(0, 1);cout << s1 << endl;//(2)s1.erase(s1.begin());cout << s1 << endl;//(3)s1.erase(s1.begin(), s1.begin() + 5);cout << s1 << endl;//(1)s1.erase();cout << s1 << endl;return 0;
}
运行结果:

(6)assign
- 该函数主要功能是对字符串进行重新赋值
- 相比重载的赋值运算符,功能上有重合,虽然assign更灵活,但用的更多的还是重载的赋值运算符函数。
演示:(根据前面函数的参数,很容易判断每种重载函数的功能,因此不再详细演示)

(7)replace
- replace 主要功能就是替换,也提供了一大堆重载函数,我们不用一个个去记忆,需要的时候查阅就行,前面我们已经判断了很多重载函数的功能,根据参数就大致能判断出每种重载函数的用法。
- 另外在替换过程中,如果是平替(替换与被替换字符数相等)则效率高,如果不是平替,少替多,多替少,替换次数多了时,效率就会很低,因此除了平替或者替换次数少,不建议经常使用
演示:(只演示一个)

(8)pop_back
- 尾删一个字符
演示:

(9)swap
关于 swap,string类提供了一个,还有一个全局的,算法库里面也有一个,这么设计的原因是什么?
原因:
- 第一个 swap 是 string类 的成员函数,例如两个string对象s1、s2,使用 s1.swap(s2) 即可调用到该函数完成交换,该交换是直接交换两个字符串的地址,因此效率高。
- 而我们平时习惯性写成 swap(s1,s2),这样就会调用到算法库里的swap,也就是第三个swap,该swap是一个函数模版,其内部对于 string 对象来说是深拷贝,深拷贝效率没有直接交换两个字符串地址效率高。因此为了避免调用到第三个swap,就创造了第二个全局的swap函数。
- 第二个 swap 函数内部就是调用第一个swap,直接交换两字符串地址,因此效率比第三个swap高,第三个swap是函数模版,对于函数模版来说,有现成的就会直接使用现成的,不会再实例化一份。因此写成 swap(s1,s2)不会调用到第三个swap,而是调用第二个swap。
- 关于这样的设计,其它容器也是如此,都是为了方便调用到成员函数的swap。
5.string类对象的其它常见操作
| 函数名称 | 函数功能 |
| c_str | 将string类对象的数据以C语言字符串的格式返回 |
| copy、substr | 相比copy,substr用的更多,用于截取当前字符串的子串 |
| find系列 | 用于查找字符或字符串 |
| 关系运算符重载、compare | 因为string重载了关系运算符,所以一般很少使用compare判断两字符串关系 |
| operator<<、operator>> | 重载的流插入、流提取运算符 |
| getline | 从输入流中读取字符 |
(1)c_str
- 获取C语言格式的字符串,因为C++兼容C语言,所以有时候需要混合编程,但是C语言中关于字符串的库函数是不支持string类对象的,因此使用该函数就能解决这些问题
演示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
using namespace std;int main()
{//C++使用C语言的文件操作string s1("test.cpp");FILE* pf = fopen(s1.c_str(), "r");//c_str返回C格式的字符串char ch = fgetc(pf);while (ch != EOF){cout << ch;ch = fgetc(pf);}cout << endl;return 0;
}
运行结果:
(2)substr
- 返回当前字符串 pos 位置开始,len 长的子串。
- 因为都有缺省参数,所以默认返回整个字符串
演示:
#include <iostream>
#include <string>
using namespace std;int main()
{string s1("****hello world****");string s2 = s1.substr(4, 11);cout << s2 << endl;return 0;
}
运行结果:
(3)find系列
1. find
- 第一个参数 str、s、c 就是需要查找的字符串或字符
- 参数 pos 是查找的起始位置,默认0则从头开始找
- 第三个重载函数的参数 n 是指定需查找的字符串 s 的长度
- find 查找成功会返回匹配的第一个字符的下标,查找失败则返回 string::npos
演示:
#include <iostream>
#include <string>
using namespace std;int main()
{//将下面字符串的空格全部替换为 '#' 号string s1("There are two needles in this haystack with needles.");size_t pos = s1.find(' ');while (pos != string::npos){s1[pos] = '#';pos = s1.find(' ', pos + 1);}cout << s1 << endl;return 0;
}
运行结果:
2.rfind
- rfind 就是倒着找,其他的和 find 一样
- 适用于找后缀的场景
演示:
#include <iostream>
#include <string>
using namespace std;int main()
{//指出下面文件的后缀名string s1("test.cpp.zip");size_t pos = s1.rfind('.');cout << s1.substr(pos) << endl;return 0;
}
运行结果:
3.find_first_of
- 作用:在字符串中搜索与其参数中指定的任何字符匹配的第一个字符
- 简单点说:find是查找单一字符或字符串,find_first_of是查找一个集合,只要查找的字符串中出现了这个集合中的字符,那么它就会返回该下标
- 成功返回下标,失败返回 string::npos
演示:
#include <iostream>
#include <string>
using namespace std;int main()
{//屏蔽5个元音字母string s1("qwertyuiopasdfghjklzxcvbnm");size_t pos = s1.find_first_of("aeiou");while (pos != string::npos){s1[pos] = '*';pos = s1.find_first_of("aeiou", pos + 1);}cout << s1 << endl;return 0;
}
运行结果:
4.find_first_not_of
- 该函数与 find_first_not_of 相反,它是找出所有不在匹配串中的字符位置
演示:
#include <iostream>
#include <string>
using namespace std;int main()
{//屏蔽5个元音字母以外的字母string s1("qwertyuiopasdfghjklzxcvbnm");size_t pos = s1.find_first_not_of("aeiou");while (pos != string::npos){s1[pos] = '*';pos = s1.find_first_not_of("aeiou", pos + 1);}cout << s1 << endl;return 0;
}
运行结果:
5.find_last_of 与 find_last_not_of
- 相比 find_first_of 和 find_first_not_of,区别就是倒着找,这里不再赘述和演示
(4)关系运算符重载
- 注意是全局函数,不是成员函数
- 模拟时会详细说明
(5)operator<<、operator>>
- 模拟时会详细说明
(6)getline
- 解决流提取时,无法读取空格和换行符等问题
- 参数 delim 是自定义读取结束符,不传参默认读到换行符结束
你是否遇到以下困扰?cin流提取时遇到空格或者换行符会自动截断,导致赋值不完整。

而getline就是专门解决这个问题的:

三、模拟实现 string类
了解完string类的使用,接下来就是自己模拟实现出string类
为了避免太复杂,我们不使用模版实现,还是按照声明和定义分离的方式来实现string类,模拟实现的意义是让我们对string的使用更加深刻,不是完全的模拟实现,主要是对常用的接口的模拟实现。
1.string.h 头文件
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <cassert>
using namespace std;//为避免与std中的string冲突,因此定义一个命名空间分隔
namespace txp
{class string{public://定义迭代器using iterator = char*;using const_iterator = const char*;//声明构造,拷贝构造string(const char* str = "");string(const string& s);//赋值运算符重载string& operator=(string s);//声明析构函数~string();//对于一些简短的函数,直接在头文件中定义,较长的函数则放到定义文件中//定义c_str成员函数const char* c_str() const{return _str;}//定义size成员函数size_t size() const{return _size;}//定义重载运算符[]char& operator[](size_t i){assert(i < _size);return _str[i];}//const版本 []const char& operator[](size_t i) const{assert(i < _size);return _str[i];}//定义迭代器接口beginiterator begin(){return _str;}//迭代器接口enditerator end(){return _str + _size;}//const版本的beginconst_iterator begin() const{return _str;}//const版本的endconst_iterator end() const{return _str + _size;}//定义clear成员函数void clear(){_str[0] = '\n';_size = 0;}//声明reserve函数void reserve(size_t n);//声明push_back函数void push_back(char ch);//声明append函数void append(const char* str);//声明运算符重载函数+=string& operator+=(char ch);//声明第二个版本的+=string& operator+=(const char* str);//声明insert成员函数void insert(size_t pos, char ch);//声明重载的insert成员函数void insert(size_t pos, const char* str);//声明erase成员函数void erase(size_t pos, size_t len = npos);//声明find成员函数size_t find(char ch, size_t pos = 0) const;size_t find(const char* str, size_t pos = 0) const;//声明substr成员函数string substr(size_t pos = 0, size_t len = npos) const;//声明swap成员函数void swap(string& str);private://底层结构char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;public://声明静态成员变量nposstatic const size_t npos;};//声明关系运算符重载函数bool operator==(const string& s1, const string& s2);bool operator!=(const string& s1, const string& s2);bool operator>(const string& s1, const string& s2);bool operator<(const string& s1, const string& s2);bool operator>=(const string& s1, const string& s2);bool operator<=(const string& s1, const string& s2);//声明流插入、流提取运算符重载函数,以及getline函数ostream& operator<<(ostream& os, const string& str);istream& operator>>(istream& is, string& str);istream& getline(istream& is, string& str, char delim = '\n');//声明全局的swap函数void swap(string& s1, string& s2);
}
2.string.cpp 文件
因函数之间存在复用关系,因此大家直接看注释吧
#include "string.h"namespace txp
{//定义全局静态变量nposconst size_t string::npos = -1;//默认构造,注意只能在声明处给缺省值,因此定义时没有写缺省值string::string(const char* str):_size(strlen(str)){_capacity = _size;_str = new char[_size + 1];//多开辟一个空间用于存储'\0'strcpy(_str, str);}//拷贝构造//1.传统写法:自己开空间+自己拷贝/*string::string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}*///2.现代写法:利用构造开空间+利用swap拷贝string::string(const string& s){string tmp(s._str);//创建临时对象tmp用于拷贝s的_str进行构造swap(tmp);//交换后,this指向的对象就是拷贝构造出来的对象,而tmp出了函数就会被析构}//赋值运算符重载//1.传统写法/*string& string::operator=(const string& s){if (this != &s){delete[] _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_capacity = s._capacity;_size = s._size;}return *this;}*///2.现代写法:string& string::operator=(string s)//利用传值传参进行拷贝构造{swap(s);//再进行交换,原this指向的空间就由s析构带走了return *this;}//注意:现代写法没有效率提升,只是更简洁了,本质是一种复用//析构string::~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}//reserve开空间,只考虑扩容的情况void string::reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}//尾插字符void string::push_back(char ch){/*if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;*/insert(_size, ch);//当我们实现insert后,可以直接复用来实现push_back的效果}//尾插字符串void string::append(const char* str){/*size_t len = strlen(str);if ((_size + len) > _capacity){size_t newCapacity = 2 * _capacity;if ((len + _size) > newCapacity){newCapacity = len + _size;}reserve(newCapacity);}strcpy(_str + _size, str);_size += len;*/insert(_size, str);//可直接复用insert}//重载运算符+=string& string::operator+=(char ch){push_back(ch);//复用push_back即可return *this;}//重载版本string& string::operator+=(const char* str){append(str);//复用append即可return *this;}//insert插入void string::insert(size_t pos, char ch){assert(pos <= _size);//需扩容时按照2倍扩容if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}//挪动数据size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];--end;}//插入_str[pos] = ch;++_size;}//insert重载void string::insert(size_t pos, const char* str){assert(pos <= _size);//由于不确定插入的字符串大小,因此扩容时需进行2次判断size_t len = strlen(str);if ((_size + len) > _capacity){size_t newCapacity = 2 * _capacity;if ((len + _size) > newCapacity)//2倍扩容不够,就需要多少开多少{newCapacity = len + _size;}reserve(newCapacity);}//挪动数据size_t end = _size + len;//对于字符串来说,停止条件不能写成end>pos,会导致越界,pos+len是最后一次挪动的位置//因此要保证end = pos+len时继续挪动,所以停止条件为end > (pos+len-1)while (end > (pos + len - 1)){_str[end] = _str[end - len];--end;}//插入for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;}//删除void string::erase(size_t pos, size_t len){assert(pos < _size);//第一种情况,要删除的字符数大于剩余的字符,直接挪动'\0'所在位置即可if (len >= (_size - pos)){_str[pos] = '\0';_size = pos;}else//剩下的情况就是要手动挪动剩余数据了{size_t end = pos + len;while (end <= _size){_str[end - len] = _str[end];//从后向前挪++end;}_size -= len;}}//查找size_t string::find(char ch, size_t pos) const{assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;}//字符串查找算法有很多,我们直接使用C库里的函数strstrsize_t string::find(const char* str, size_t pos) const{assert(pos < _size);const char* ptr = strstr(_str + pos, str);if (ptr == nullptr){return npos;}else{return ptr - _str;}}//取子串string string::substr(size_t pos, size_t len) const{assert(pos < _size);//len大于剩余串长度,则直接取到结尾if (len > (_size - pos)){len = _size - pos;}txp::string sub;sub.reserve(len);for (size_t i = 0; i < len; i++){sub += _str[pos + i];}return sub;}//交换void string::swap(string& str){//调用算法库中的swap即可std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}//关系运算符重载bool operator==(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) == 0;//直接利用C的库函数}bool operator!=(const string& s1, const string& s2){return !(s1 == s2);//复用==}bool operator>(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) > 0;//利用C库}bool operator<(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) < 0;//利用C库}bool operator>=(const string& s1, const string& s2){return s1 > s2 || s1 == s2;//复用>和==}bool operator<=(const string& s1, const string& s2){return s1 < s2 || s1 == s2;//复用<和==}//流插入ostream& operator<<(ostream& os, const string& str){for (size_t i = 0; i < str.size(); i++){os << str[i];}return os;}//流提取istream& operator>>(istream& is, string& str){str.clear();//先清空数据int i = 0;char buff[256];//为避免多次扩容,选择创建一个buff数组char ch;//传统的流提取会忽略掉空格和换行符,怎么解决呢?ch = is.get();//get为istream类对象的一个接口,可以读取任意字符while (ch != ' ' && ch != '\n'){buff[i++] = ch;//当buff数组存满时就+=到strif (i == 255){buff[i] = '\0';i = 0;str += buff;}ch = is.get();}//如果buff中还有剩余字符未处理if (i > 0){buff[i] = '\0';str += buff;}return is;}//定义getline函数istream& getline(istream& is, string& str, char delim){str.clear();int i = 0;char buff[256];char ch;ch = is.get();while (ch != delim)//与流提取的差别就是这里,delim控制结束符{buff[i++] = ch;if (i == 255){buff[i] = '\0';i = 0;str += buff;}ch = is.get();}if (i > 0){buff[i] = '\0';str += buff;}return is;}//全局交换void swap(string& s1, string& s2){//调用成员函数的swap即可s1.swap(s2);}
}
总结
以上就是本文的全部内容,感谢支持,祝大家新年快乐 !
相关文章:
【C++】STL介绍 + string类使用介绍 + 模拟实现string类
目录 前言 一、STL简介 二、string类 1.为什么学习string类 2.标准库中的string类 3.auto和范围for 4.迭代器 5.string类的常用接口说明 三、模拟实现 string类 前言 本文带大家入坑STL,学习第一个容器string。 一、STL简介 在学习C数据结构和算法前,我…...
【Redis】List 类型的介绍和常用命令
1. 介绍 Redis 中的 list 相当于顺序表,并且内部更接近于“双端队列”,所以也支持头插和尾插的操作,可以当做队列或者栈来使用,同时也存在下标的概念,不过和 Java 中的下标不同,Redis 支持负数下标&#x…...
【愚公系列】《循序渐进Vue.js 3.x前端开发实践》033-响应式编程的原理及在Vue中的应用
标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主&…...
未来无线技术的发展方向
未来无线技术的发展趋势呈现出多样化、融合化的特点,涵盖速度、覆盖范围、应用领域、频段利用、安全性等多个方面。这些趋势将深刻改变人们的生活和社会的运行方式。 传输速度提升:Wi-Fi 技术迭代加快,如 Wi-Fi7 理论峰值速率达 46Gbps&#…...
MySQL知识点总结(十四)
mysqldump和mysqlpump实用程序在功能上有哪些相同和不同的地方? 二者都能用来执行逻辑备份,将所有数据库,特定数据库或特定表转储到文本文件,可移植,独立于存储引擎,是很好的复制/移动策略,适合…...
实时数据处理与模型推理:利用 Spring AI 实现对数据的推理与分析
在现代企业中,实时数据处理与快速决策已经成为关键需求。通过集成 Spring AI,我们不仅可以高效地获取实时数据,还可以将这些数据输入到 AI 模型中进行推理与分析,以便生成实时的业务洞察。 本文将讲解如何通过 Spring AI 实现实时…...
PETSc源码分析: Optimization Solvers
本文结合PETSc源代码,分析PETSc中的优化求解器。 注1:限于研究水平,分析难免不当,欢迎批评指正。 注2:文章内容会不定期更新。 参考文献 Balay S. PETSc/TAO Users Manual, Revision 3.22. Argonne National Labora…...
面向对象设计(大三上)--往年试卷题+答案
目录 1. UML以及相关概念 1.1 动态图&静态图 1.2 交互图 1.3 序列图 1.4 类图以及关联关系 1.4.1类图 1.4.2 关系类型 (1) 用例图中的包含、扩展关系(include & extend) (2) 类图中的聚合、组合关系(aggragation & composition) 1.5 图对象以及职责划…...
芯片AI深度实战:进阶篇之vim内verilog实时自定义检视
本文基于Editor Integration | ast-grep,以及coc.nvim,并基于以下verilog parser(my-language.so,文末下载链接), 可以在vim中实时显示自定义的verilog 匹配。效果图如下: 需要的配置如下: 系列文章: 芯片…...
几种K8s运维管理平台对比说明
目录 深入体验**结论**对比分析表格**1. 功能对比****2. 用户界面****3. 多租户支持****4. DevOps支持** 细对比分析1. **Kuboard**2. **xkube**3. **KubeSphere**4. **Dashboard****对比总结** 深入体验 KuboardxkubeKubeSphereDashboard 结论 如果您需要一个功能全面且适合…...
TikTok 推出了一款 IDE,用于快速构建 AI 应用
字节跳动(TikTok 的母公司)刚刚推出了一款名为 Trae 的新集成开发环境(IDE)。 Trae 基于 Visual Studio Code(VS Code)构建,继承了这个熟悉的平台,并加入了 AI 工具,帮助开发者更快、更轻松地构建应用——有时甚至无需编写任何代码。 如果你之前使用过 Cursor AI,T…...
【MySQL — 数据库增删改查操作】深入解析MySQL的 Retrieve 检索操作
Retrieve 检索 示例 1. 构造数据 创建表结构 create table exam1(id bigint, name varchar(20) comment同学姓名, Chinesedecimal(3,1) comment 语文成绩, Math decimal(3,1) comment 数学成绩, English decimal(3,1) comment 英语成绩 ); 插入测试数据 insert into ex…...
强大到工业层面的软件
电脑数据删不干净,简直是一种让人抓狂的折磨!明明已经把文件扔进了回收站,清空了,可那些残留的数据就像牛皮癣一样,怎么也除不掉。这种烦恼简直无处不在,让人从头到脚都感到无比烦躁。 首先,心…...
全面解析文件包含漏洞:原理、危害与防护
目录 前言 漏洞介绍 漏洞原理 产生条件 攻击方式 造成的影响 经典漏洞介绍 防御措施 结语 前言 在当今复杂的网络安全环境中,文件包含漏洞就像潜藏在暗处的危险陷阱,随时可能对防护薄弱的 Web 应用发起致命攻击。随着互联网的迅猛发展ÿ…...
基于Django的Boss直聘IT岗位可视化分析系统的设计与实现
【Django】基于Django的Boss直聘IT岗位可视化分析系统的设计与实现(完整系统源码开发笔记详细部署教程)✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 该系统采用Python作为主要开发语言,利用Django这一高效、安全的W…...
SSM开发(三) spring与mybatis整合(含完整运行demo源码)
目录 本文主要内容 一、Spring整合MyBatis的三个关键点 二、整合步骤 1、创建一个Maven项目 2、在pom.xml文件中添加jar包的依赖 3、配置MyBatis 注解实现方式 XML配置文件实现 4、配置Spring 5、测试运行 本文主要内容 1. Spring + Mybatis整合; 2. MyBatis两种SQL…...
【Rust自学】14.6. 安装二进制crate
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω・) 14.6.1. 从cratea.io安装二进制crate 通过cargo_install命令可以从crates.io安装二进制crate。 这并不是为了替换系统包,它应…...
【Redis】hash 类型的介绍和常用命令
1. 介绍 Redis 中存储的 key-value 本身就是哈希表的结构,存储的 value 也可以是一个哈希表的结构 这里每一个 key 对应的一个 哈希类型用 field-value 来表示 2. 常用命令 命令 介绍 时间复杂度 hset key field value 用于设置哈希表 key 中字段 field 的值为…...
低代码产品表单渲染架构
在React和Vue没有流行起来的时候,低代码产品的表单渲染设计通常会使用操作Dom的方式实现。 下面是一个表单的例子: 产品层 用户通过打开表单,使用不同业务场景业务下的表单页面,中间的Render层就是技术实现。 每一个不同业务的表单…...
深入理解 C 语言基本数据类型:从常量变量到输入输出
深入理解 C 语言基本数据类型:从常量变量到输入输出 在 C 语言的世界里,数据是程序运行的核心,而对数据类型的掌握则是编写高效、准确程序的基础。今天,就让我们一起深入探讨 C 语言中的基本数据类型。 一、数据的表现形式 在 C …...
多线程-线程池的使用
1. 线程池 1.1 线程状态介绍 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么 Java 中的线程存在哪几种状态呢?Java 中的线程 状态被定义在了 java.lang.Thread.…...
计算机网络 IP 网络层 2 (重置版)
IP的简介: IP 地址是互联网协议地址(Internet Protocol Address)的简称,是分配给连接到互联网的设备的唯一标识符,用于在网络中定位和通信。 IP编制的历史阶段: 1,分类的IP地址: …...
Linux学习笔记——网络管理命令
一、网络基础知识 TCP/IP四层模型 以太网地址(MAC地址): 段16进制数据 IP地址: 子网掩码: 二、接口管命令 ip命令:字符终端,立即生效,重启配置会丢失 nmcli命令:字符…...
供应链系统设计-供应链中台系统设计(十)- 清结算中心概念片篇
综述 我们之前在供应链系统设计-中台系统设计系列(五)- 供应链中台实践概述文章中针对中台到底是什么进行了描述,对于中台的范围也进行划分,如下图所示: 关于商品中心,我们之前用4篇文章介绍了什么是商品中…...
C++,STL 简介:历史、组成、优势
文章目录 引言一、STL 的历史STL 的核心组成三、STL 的核心优势四、结语进一步学习资源: 引言 C 是一门强大且灵活的编程语言,但其真正的魅力之一在于其标准库——尤其是标准模板库(Standard Template Library, STL)。STL 提供了…...
OpenAI-Edge-TTS:本地化 OpenAI 兼容的文本转语音 API,免费高效!
文本转语音(TTS)技术已经成为人工智能领域的重要一环,无论是语音助手、教育内容生成,还是音频文章创作,TTS 工具都能显著提高效率。今天要为大家介绍的是 OpenAI-Edge-TTS,一款基于 Microsoft Edge 在线文本…...
手写instanceof、手写new操作符
文章目录 1 手写instanceof2 手写new操作符 1 手写instanceof instanceof:用于判断构造函数的prototype属性是否出现在对象原型链中的任何位置实现步骤: 获取类型的原型。获取对象的原型。一直循环判断对象的原型是否等于构造函数的原型对象,…...
29. C语言 可变参数详解
本章目录: 前言可变参数的基本概念可变参数的工作原理如何使用可变参数 示例:计算多个整数的平均值解析: 更复杂的可变参数示例:打印可变数量的字符串解析: 总结 前言 在C语言中,函数参数的数量通常是固定的ÿ…...
蓝桥杯python语言基础(5)——函数
目录 一、作业:四个函数 二、math 三、collections 1. Counter 2. deque 3. defaultdict 4. OrderedDict 四、heapq 五、functool partial偏函数 六、itertools 1.无限迭代器 2.有限迭代器 3.排列组合迭代器 一、作业:四个函数 计算最大公…...
node 爬虫开发内存处理 zp_stoken 作为案例分析
声明: 本文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 前言 主要说3种我们补环境过后如果用…...


























