【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类

  1. 原字符串的缺陷:C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想(封装、继承和多态),而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
  2. 实用性:在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中情况:

  1. 如果 n 小于当前字符串长度,则当前值将缩短为其前 n 个字符,并删除第 个字符以外的字符。
  2. 如果 n 大于当前字符串长度却又小于当前空间容量,则将当前字符串大小调整为 n,指定了c,则新元素将初始化为 c ,未指定则初始化为'\0'
  3. 如果 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);}
}


总结

以上就是本文的全部内容,感谢支持,祝大家新年快乐 !

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/68508.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用 MSYS2 qemu 尝鲜Arm64架构国产Linux系统

近期&#xff0c;我的师弟咨询我关于Arm64架构的国产CPU国产OS开发工具链问题。他们公司因为接手了一个国企的单子&#xff0c;需要在这类环境下开发程序。说实在的我也没有用过这个平台&#xff0c;但是基于常识&#xff0c;推测只要基于C和Qt&#xff0c;应该问题不大。 1. …

电路研究9.2.3——合宙Air780EP中FTP——FTPGET 命令使用方法研究

怎么说呢&#xff0c;之前也是看的&#xff0c;但是也很迷茫&#xff0c;感觉上虽然是对的&#xff0c;但是无法联系到应用里面&#xff0c;今天研究一下FTP 命令使用方法吧。 15.29 使用方法举例 这里发现下面那些看的不懂呢&#xff0c;于是就返回FTP的应用了。 9.5.4 FTP 应…

单细胞分析基础-第一节 数据质控、降维聚类

scRNA_pipeline\1.Seurat 生物技能树 可进官网查询 添加链接描述 分析流程 准备:R包安装 options("repos"="https://mirrors.ustc.edu.cn/CRAN/") if(!require("BiocManager")) install.packages("BiocManager",update = F,ask =…

【数组OJ】两数之和

两数之和 题目 思路 暴力枚举&#xff1a;逐一遍历&#xff0c;将当前数与之后的数个个相加、判断其相加后是否等于target 代码实现 /*** Note: The returned array must be malloced, assume caller calls free().*///暴力枚举&#xff1a; int* twoSum(int* nums, int nu…

ResNeSt: Split-Attention Networks 参考论文

参考文献 [1] Tensorflow Efficientnet. https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet. Accessed: 2020-03-04. 中文翻译&#xff1a;[1] TensorFlow EfficientNet. https://github.com/tensorflow/tpu/tree/master/models/official/efficien…

Java后端之AOP

AOP&#xff1a;面向切面编程&#xff0c;本质是面向特定方法编程 引入依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>示例&#xff1a;记录…

51单片机开发:独立键盘实验

实验目的&#xff1a;按下键盘1时&#xff0c;点亮LED灯1。 键盘原理图如下图所示&#xff0c;可见&#xff0c;由于接GND&#xff0c;当键盘按下时&#xff0c;P3相应的端口为低电平。 键盘按下时会出现抖动&#xff0c;时间通常为5-10ms&#xff0c;代码中通过延时函数delay…

java求职学习day18

常用的设计原则和设计模式 1 常用的设计原则&#xff08;记住&#xff09; 1.1 软件开发的流程 需求分析文档、概要设计文档、详细设计文档、编码和测试、安装和调试、维护和升级 1.2 常用的设计原则 &#xff08;1&#xff09;开闭原则&#xff08;Open Close Principle…

c++ 定点 new

&#xff08;1&#xff09; 代码距离&#xff1a; #include <new> // 需要包含这个头文件 #include <iostream>int main() {char buffer[sizeof(int)]; // 分配一个足够大的字符数组作为内存池int* p new(&buffer) int(42); // 使用 placement new…

本地部署 DeepSeek-R1 大模型指南:基于 Ollama 的完整流程

Ollama是什么 Ollama 是一个开源的大语言模型本地化部署与管理工具&#xff0c;支持&#xff1a; 一键式模型下载与版本管理 本地化模型推理服务部署 REST API 接口提供 多平台客户端接入支持 整体步骤 安装 OllamaOllama服务配置部署模型客户端配置&#xff08;Page Ass…

Git Bash 配置 zsh

博客食用更佳 博客链接 安装 zsh 安装 Zsh 安装 Oh-my-zsh github仓库 sh -c "$(curl -fsSL https://install.ohmyz.sh/)"让 zsh 成为 git bash 默认终端 vi ~/.bashrc写入&#xff1a; if [ -t 1 ]; thenexec zsh fisource ~/.bashrc再重启即可。 更换主题 …

Controller 层优化四步曲

Controller 层优化四步曲 前言 在开发过程中&#xff0c;Controller 层作为系统与外界交互的桥梁&#xff0c;承担着接收请求、解析参数、调用业务逻辑、处理异常等职责。 然而&#xff0c;随着业务复杂度的增加&#xff0c;Controller 层的代码往往会变得臃肿且难以维护。 …

面试经典150题——图

文章目录 1、岛屿数量1.1 题目链接1.2 题目描述1.3 解题代码1.4 解题思路 2、被围绕的区域2.1 题目链接2.2 题目描述2.3 解题代码2.4 解题思路 3、克隆图3.1 题目链接3.2 题目描述3.3 解题代码3.4 解题思路 4、除法求值4.1 题目链接4.2 题目描述4.3 解题代码4.4 解题思路 5、课…

FLTK - FLTK1.4.1 - 搭建模板,将FLTK自带的实现搬过来做实验

文章目录 FLTK - FLTK1.4.1 - 搭建模板&#xff0c;将FLTK自带的实现搬过来做实验概述笔记my_fltk_test.cppfltk_test.hfltk_test.cxx用adjuster工程试了一下&#xff0c;好使。END FLTK - FLTK1.4.1 - 搭建模板&#xff0c;将FLTK自带的实现搬过来做实验 概述 用fluid搭建UI…

【外文原版书阅读】《机器学习前置知识》1.线性代数的重要性,初识向量以及向量加法

目录 ​编辑 ​编辑 1.Chapter 2 Why Linear Algebra? 2.Chapter 3 What Is a Vector? 个人主页&#xff1a;Icomi 大家好&#xff0c;我是Icomi&#xff0c;本专栏是我阅读外文原版书《Before Machine Learning》对于文章中我认为能够增进线性代数与机器学习之间的理解的…

Python设计模式 - 组合模式

定义 组合模式&#xff08;Composite Pattern&#xff09; 是一种结构型设计模式&#xff0c;主要意图是将对象组织成树形结构以表示"部分-整体"的层次结构。这种模式能够使客户端统一对待单个对象和组合对象&#xff0c;从而简化了客户端代码。 组合模式有透明组合…

Java 基于 SpringBoot 的校园外卖点餐平台微信小程序(附源码,部署,文档)

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

当高兴、尊重和优雅三位一体是什么情况吗?

英语单词 disgrace 表示“失脸&#xff0c;耻辱&#xff0c;不光彩&#xff0c;名誉扫地”一类的含义&#xff0c;可做名词或动词使用&#xff0c;含义基本一致&#xff0c;只是词性不同。 disgrace n.丢脸&#xff1b;耻辱&#xff1b;不光彩&#xff1b;令人感到羞耻的人(或…

LeetCode热题100(八)—— 438.找到字符串中所有字母异位词

LeetCode热题100&#xff08;八&#xff09;—— 438.找到字符串中所有字母异位词 题目描述代码实现思路解析 你好&#xff0c;我是杨十一&#xff0c;一名热爱健身的程序员在Coding的征程中&#xff0c;不断探索与成长LeetCode热题100——刷题记录&#xff08;不定期更新&…

八股——Java基础(四)

目录 一、泛型 1. Java中的泛型是什么 ? 2. 使用泛型的好处是什么? 3. Java泛型的原理是什么 ? 什么是类型擦除 ? 4.什么是泛型中的限定通配符和非限定通配符 ? 5. List和List 之间有什么区别 ? 6. 可以把List传递给一个接受List参数的方法吗&#xff1f; 7. Arra…