【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数据结构和算法前,我…...

Hive:基本查询语法
和oracle一致的部分 和oracle不一样的部分 排序 oracle中,在升序排序中,NULL 值被视为最大的值;在降序排序中,NULL 值被视为最小的值。 在MySQL中,NULL 被视为小于任何非空值。 在Hive中, NULL是最小的; Hive除了可以用order…...

日志收集Day008
1.zk集群优化 修改zookeeper的堆内存大小,一般情况下,生产环境给到2G足以,如果规模较大可以适当调大到4G。 (1)配置ZK的堆内存 vim /app/softwares/zk/conf/java.env export JAVA_HOME/sortwares/jdk1.8.0_291 export JVMFLAGS"-Xms2…...

【解决方案】VMware虚拟机adb连接宿主机夜神模拟器
1、本机(宿主机,系统windows10)ip为192.168.31.108 2、运行模拟器后本机cmd查看端口为62026 3、VMware虚拟机(系统,kali)adb连接192.168.31.108:62026报错 failed to connect to 192.168.31.108:16416: Co…...

基于金融新闻的大型语言模型强化学习在投资组合管理中的应用
“Financial News-Driven LLM Reinforcement Learning for Portfolio Management” 论文地址:https://arxiv.org/pdf/2411.11059 摘要 本研究探索了如何通过将大语言模型(LLM)支持的情感分析融入强化学习(RL)中&#…...

脚本运行禁止:npm 无法加载文件,因为在此系统上禁止运行脚本
问题与处理策略 1、问题描述 npm install -D tailwindcss执行上述指令,报如下错误 npm : 无法加载文件 D:\nodejs\npm.ps1,因为在此系统上禁止运行脚本。 有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_…...

借DeepSeek-R1东风,开启创业新机遇
DeepSeek-R1的崛起 DeepSeek-R1的推出引发了广泛关注,在AI领域引起了一阵旋风。作为新一代的智能模型,它在多项任务中表现出了卓越的能力。普通人可以借助这个强大的工具,开启属于自己的创业之路,抓住时代带来的机遇。 内容创作…...

C# lock使用详解
总目录 前言 在 C# 多线程编程中,lock 关键字是一种非常重要的同步机制,用于确保同一时间只有一个线程可以访问特定的代码块,从而避免多个线程同时操作共享资源时可能出现的数据竞争和不一致问题。以下是关于 lock 关键字的详细使用介绍。 一…...

简易CPU设计入门:控制总线的剩余信号(四)
项目代码下载 请大家首先准备好本项目所用的源代码。如果已经下载了,那就不用重复下载了。如果还没有下载,那么,请大家点击下方链接,来了解下载本项目的CPU源代码的方法。 CSDN文章:下载本项目代码 上述链接为本项目…...

使用 lock4j-redis-template-spring-boot-starter 实现 Redis 分布式锁
在分布式系统中,多个服务实例可能同时访问和修改共享资源,从而导致数据不一致的问题。为了解决这个问题,分布式锁成为了关键技术之一。本文将介绍如何使用 lock4j-redis-template-spring-boot-starter 来实现 Redis 分布式锁,从而…...

22_解析XML配置文件_List列表
解析XML文件 需要先 1.【加载XML文件】 而 【加载XML】文件有两种方式 【第一种 —— 使用Unity资源系统加载文件】 TextAsset xml Resources.Load<TextAsset>(filePath); XmlDocument doc new XmlDocument(); doc.LoadXml(xml.text); 【第二种 —— 在C#文件IO…...

编译器gcc/g++ --【Linux基础开发工具】
文章目录 一、背景知识二、gcc编译选项1、预处理(进行宏替换)2、编译(生成汇编)3、汇编(生成机器可识别代码)4、链接(生成可执行文件或库文件) 三、动态链接和静态链接四、静态库和动态库1、动静态库2、编译…...

58.界面参数传递给Command C#例子 WPF例子
界面参数的传递,界面参数是如何从前台传送到后台的。 param 参数是从界面传递到命令的。这个过程通常涉及以下几个步骤: 数据绑定:界面元素(如按钮)的 Command 属性绑定到视图模型中的 RelayCommand 实例。同时&#x…...

games101-(5/6)
光栅化 投影完成之后,视图区域被确定在从[-1,1]的单位矩阵中,下一步就是光栅化 长宽比:ratio 垂直的可视角度:fild-of-view 可以看到的y 轴的范围,角度越小 越接近正交投影 屏幕坐标系 、 将多边形转化成像素 显示…...

人工智能在计算机视觉中的应用与创新发展研究
一、引言 1.1 研究背景与意义 1.1.1 研究背景 在当今数字化与智能化飞速发展的时代,人工智能已成为推动各领域变革的核心力量,而计算机视觉作为人工智能领域中极具活力与潜力的重要分支,正发挥着日益关键的作用。计算机视觉旨在赋予计算机…...

1-2 飞机大战游戏场景
前言: 根据前面的项目框架,搭建游戏的运行场景...... 1.0 框架预览 基于该框架首先实现游戏的运行场景 2.0 图片文件 创建图片文件,本次项目使用easyx作为图形库文件,在easyx中想要显示图片,需要有一张图片和图片的掩码…...

Mac Electron 应用签名(signature)和公证(notarization)
在MacOS 10.14.5之后,如果应用没有在苹果官方平台进行公证notarization(我们可以理解为安装包需要审核,来判断是否存在病毒),那么就不能被安装。当然现在很多人的解决方案都是使用sudo spctl --master-disable,取消验证模式&#…...

Sklearn 中的逻辑回归
逻辑回归的数学模型 基本模型 逻辑回归主要用于处理二分类问题。二分类问题对于模型的输出包含 0 和 1,是一个不连续的值。分类问题的结果一般不能由线性函数求出。这里就需要一个特别的函数来求解,这里引入一个新的函数 Sigmoid 函数,也成…...

【阅读笔记】New Edge Diected Interpolation,NEDI算法,待续
一、概述 由Li等提出的新的边缘指导插值(New Edge—Di-ected Interpolation,NEDI)算法是一种具有良好边缘保持效果的新算法,它利用低分辨率图像与高分辨率图像的局部协方差问的几何对偶性来对高分辨率图像进行自适应插值。 2001年Xin Li和M.T. Orchard…...

编程题-最长的回文子串(中等)
题目: 给你一个字符串 s,找到 s 中最长的回文子串。 示例 1: 输入:s "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。示例 2: 输入:s &…...

Versal - 基础3(AXI NoC 专题+仿真+QoS)
目录 1. 简介 2. 示例 2.1 示例说明 2.2 创建项目 2.2.1 平台信息 2.2.2 AXI NoC Automation 2.2.3 创建时钟和复位 2.3 配置 NoC 2.4 配置 AXI Traffic 2.5 配置 Memory Size 2.6 Validate BD 2.7 添加观察信号 2.8 运行仿真 2.9 查看结果 2.9.1 整体波形 2.9…...

知识库建设对提升团队协作与创新能力的影响分析
内容概要 在当今快速变革的商业环境中,知识库建设的重要性愈发凸显。它不仅是信息存储的载体,更是推动组织内部沟通与协作的基石。通过系统整理与管理企业知识,团队成员能够便捷地访问相关信息,使得协作过程更为流畅,…...

Java 实现Excel转HTML、或HTML转Excel
Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,以便更好地利用和展示数据。本文将介绍如何通过 Java 实现 E…...

stack 和 queue容器的介绍和使用
1.stack的介绍 1.1stack容器的介绍 stack容器的基本特征和功能我们在数据结构篇就已经详细介绍了,还不了解的uu, 可以移步去看这篇博客哟: 数据结构-栈数据结构-队列 简单回顾一下,重要的概念其实就是后进先出,栈在…...

云计算与虚拟化技术讲解视频分享
互联网各领域资料分享专区(不定期更新): Sheet 前言 由于内容较多,且不便于排版,为避免资源失效,请用手机点击链接进行保存,若链接生效请及时反馈,谢谢~ 正文 链接如下(为避免资源失效&#x…...

python flask 使用 redis写一个例子
下面是一个使用Flask和Redis的简单例子: from flask import Flask from redis import Redisapp Flask(__name__) redis Redis(hostlocalhost, port6379)app.route(/) def hello():# 写入到Redisredis.set(name, Flask Redis Example)# 从Redis中读取数据name re…...

深入解析 Linux 内核内存管理核心:mm/memory.c
在 Linux 内核的众多组件中,内存管理模块是系统性能和稳定性的关键。mm/memory.c 文件作为内存管理的核心实现,承载着页面故障处理、页面表管理、内存区域映射与取消映射等重要功能。本文将深入探讨 mm/memory.c 的设计思想、关键机制以及其在内核中的作用,帮助读者更好地理…...

跟我学C++中级篇——64位的处理
一、计算机的发展 计算机从二进制为基础开始描述整个世界,但正如现实世界一样,十进制为主的世界也会有万千百概念。所以在实际的应用中,会出现32位和64位的计算机系统。当然,前面还有过16位、8位和4位等,以后还可以会…...

指针的介绍2后
1.二级指针 1.1二级指针的介绍 二级指针是指向指针的指针 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h>int main() {int a 100;int* pa &a;int** ppa &pa;printf("a %d\n", a);printf("&a(pa) %p\n", pa);prin…...

Linux 学习笔记__Day3
十八、设置虚拟机的静态IP 1、VMware的三种网络模式 安装VMware Workstation Pro之后,会在Windows系统中虚拟出两个虚拟网卡,如下: VMware提供了三种网络模式,分别是:桥接模式(Bridged)、NAT…...