【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 < size
s1.resize(10);
cout << s1 << endl;
cout << "size: " << s1.size() << endl;
cout << "capacity: " << s1.capacity() << endl;
cout << endl;
//2.size < n < capacity
s1.resize(25,'x');
cout << s1 << endl;
cout << "size: " << s1.size() << endl;
cout << "capacity: " << s1.capacity() << endl;
cout << endl;
//3.n > capacity
s1.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];
}
//定义迭代器接口begin
iterator begin()
{
return _str;
}
//迭代器接口end
iterator end()
{
return _str + _size;
}
//const版本的begin
const_iterator begin() const
{
return _str;
}
//const版本的end
const_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:
//声明静态成员变量npos
static 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
{
//定义全局静态变量npos
const 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库里的函数strstr
size_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数组存满时就+=到str
if (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);
}
}
总结
以上就是本文的全部内容,感谢支持,祝大家新年快乐 !