当前位置: 首页 > article >正文

【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);}
}


总结

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

相关文章:

【C++】STL介绍 + string类使用介绍 + 模拟实现string类

目录 前言 一、STL简介 二、string类 1.为什么学习string类 2.标准库中的string类 3.auto和范围for 4.迭代器 5.string类的常用接口说明 三、模拟实现 string类 前言 本文带大家入坑STL&#xff0c;学习第一个容器string。 一、STL简介 在学习C数据结构和算法前&#xff0c;我…...

【Redis】List 类型的介绍和常用命令

1. 介绍 Redis 中的 list 相当于顺序表&#xff0c;并且内部更接近于“双端队列”&#xff0c;所以也支持头插和尾插的操作&#xff0c;可以当做队列或者栈来使用&#xff0c;同时也存在下标的概念&#xff0c;不过和 Java 中的下标不同&#xff0c;Redis 支持负数下标&#x…...

【愚公系列】《循序渐进Vue.js 3.x前端开发实践》033-响应式编程的原理及在Vue中的应用

标题详情作者简介愚公搬代码头衔华为云特约编辑&#xff0c;华为云云享专家&#xff0c;华为开发者专家&#xff0c;华为产品云测专家&#xff0c;CSDN博客专家&#xff0c;CSDN商业化专家&#xff0c;阿里云专家博主&#xff0c;阿里云签约作者&#xff0c;腾讯云优秀博主&…...

未来无线技术的发展方向

未来无线技术的发展趋势呈现出多样化、融合化的特点&#xff0c;涵盖速度、覆盖范围、应用领域、频段利用、安全性等多个方面。这些趋势将深刻改变人们的生活和社会的运行方式。 传输速度提升&#xff1a;Wi-Fi 技术迭代加快&#xff0c;如 Wi-Fi7 理论峰值速率达 46Gbps&#…...

MySQL知识点总结(十四)

mysqldump和mysqlpump实用程序在功能上有哪些相同和不同的地方&#xff1f; 二者都能用来执行逻辑备份&#xff0c;将所有数据库&#xff0c;特定数据库或特定表转储到文本文件&#xff0c;可移植&#xff0c;独立于存储引擎&#xff0c;是很好的复制/移动策略&#xff0c;适合…...

实时数据处理与模型推理:利用 Spring AI 实现对数据的推理与分析

在现代企业中&#xff0c;实时数据处理与快速决策已经成为关键需求。通过集成 Spring AI&#xff0c;我们不仅可以高效地获取实时数据&#xff0c;还可以将这些数据输入到 AI 模型中进行推理与分析&#xff0c;以便生成实时的业务洞察。 本文将讲解如何通过 Spring AI 实现实时…...

PETSc源码分析: Optimization Solvers

本文结合PETSc源代码&#xff0c;分析PETSc中的优化求解器。 注1&#xff1a;限于研究水平&#xff0c;分析难免不当&#xff0c;欢迎批评指正。 注2&#xff1a;文章内容会不定期更新。 参考文献 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&#xff0c;以及coc.nvim&#xff0c;并基于以下verilog parser(my-language.so&#xff0c;文末下载链接), 可以在vim中实时显示自定义的verilog 匹配。效果图如下&#xff1a; 需要的配置如下&#xff1a; 系列文章&#xff1a; 芯片…...

几种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…...

强大到工业层面的软件

电脑数据删不干净&#xff0c;简直是一种让人抓狂的折磨&#xff01;明明已经把文件扔进了回收站&#xff0c;清空了&#xff0c;可那些残留的数据就像牛皮癣一样&#xff0c;怎么也除不掉。这种烦恼简直无处不在&#xff0c;让人从头到脚都感到无比烦躁。 首先&#xff0c;心…...

全面解析文件包含漏洞:原理、危害与防护

目录 前言 漏洞介绍 漏洞原理 产生条件 攻击方式 造成的影响 经典漏洞介绍 防御措施 结语 前言 在当今复杂的网络安全环境中&#xff0c;文件包含漏洞就像潜藏在暗处的危险陷阱&#xff0c;随时可能对防护薄弱的 Web 应用发起致命攻击。随着互联网的迅猛发展&#xff…...

基于Django的Boss直聘IT岗位可视化分析系统的设计与实现

【Django】基于Django的Boss直聘IT岗位可视化分析系统的设计与实现&#xff08;完整系统源码开发笔记详细部署教程&#xff09;✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 该系统采用Python作为主要开发语言&#xff0c;利用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

喜欢的话别忘了点赞、收藏加关注哦&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 14.6.1. 从cratea.io安装二进制crate 通过cargo_install命令可以从crates.io安装二进制crate。 这并不是为了替换系统包&#xff0c;它应…...

【Redis】hash 类型的介绍和常用命令

1. 介绍 Redis 中存储的 key-value 本身就是哈希表的结构&#xff0c;存储的 value 也可以是一个哈希表的结构 这里每一个 key 对应的一个 哈希类型用 field-value 来表示 2. 常用命令 命令 介绍 时间复杂度 hset key field value 用于设置哈希表 key 中字段 field 的值为…...

低代码产品表单渲染架构

在React和Vue没有流行起来的时候&#xff0c;低代码产品的表单渲染设计通常会使用操作Dom的方式实现。 下面是一个表单的例子&#xff1a; 产品层 用户通过打开表单&#xff0c;使用不同业务场景业务下的表单页面&#xff0c;中间的Render层就是技术实现。 每一个不同业务的表单…...

深入理解 C 语言基本数据类型:从常量变量到输入输出

深入理解 C 语言基本数据类型&#xff1a;从常量变量到输入输出 在 C 语言的世界里&#xff0c;数据是程序运行的核心&#xff0c;而对数据类型的掌握则是编写高效、准确程序的基础。今天&#xff0c;就让我们一起深入探讨 C 语言中的基本数据类型。 一、数据的表现形式 在 C …...

多线程-线程池的使用

1. 线程池 1.1 线程状态介绍 当线程被创建并启动以后&#xff0c;它既不是一启动就进入了执行状态&#xff0c;也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么 Java 中的线程存在哪几种状态呢&#xff1f;Java 中的线程 状态被定义在了 java.lang.Thread.…...

计算机网络 IP 网络层 2 (重置版)

IP的简介&#xff1a; IP 地址是互联网协议地址&#xff08;Internet Protocol Address&#xff09;的简称&#xff0c;是分配给连接到互联网的设备的唯一标识符&#xff0c;用于在网络中定位和通信。 IP编制的历史阶段&#xff1a; 1&#xff0c;分类的IP地址&#xff1a; …...

Linux学习笔记——网络管理命令

一、网络基础知识 TCP/IP四层模型 以太网地址&#xff08;MAC地址&#xff09;&#xff1a; 段16进制数据 IP地址&#xff1a; 子网掩码&#xff1a; 二、接口管命令 ip命令&#xff1a;字符终端&#xff0c;立即生效&#xff0c;重启配置会丢失 nmcli命令&#xff1a;字符…...

供应链系统设计-供应链中台系统设计(十)- 清结算中心概念片篇

综述 我们之前在供应链系统设计-中台系统设计系列&#xff08;五&#xff09;- 供应链中台实践概述文章中针对中台到底是什么进行了描述&#xff0c;对于中台的范围也进行划分&#xff0c;如下图所示&#xff1a; 关于商品中心&#xff0c;我们之前用4篇文章介绍了什么是商品中…...

C++,STL 简介:历史、组成、优势

文章目录 引言一、STL 的历史STL 的核心组成三、STL 的核心优势四、结语进一步学习资源&#xff1a; 引言 C 是一门强大且灵活的编程语言&#xff0c;但其真正的魅力之一在于其标准库——尤其是标准模板库&#xff08;Standard Template Library, STL&#xff09;。STL 提供了…...

OpenAI-Edge-TTS:本地化 OpenAI 兼容的文本转语音 API,免费高效!

文本转语音&#xff08;TTS&#xff09;技术已经成为人工智能领域的重要一环&#xff0c;无论是语音助手、教育内容生成&#xff0c;还是音频文章创作&#xff0c;TTS 工具都能显著提高效率。今天要为大家介绍的是 OpenAI-Edge-TTS&#xff0c;一款基于 Microsoft Edge 在线文本…...

手写instanceof、手写new操作符

文章目录 1 手写instanceof2 手写new操作符 1 手写instanceof instanceof&#xff1a;用于判断构造函数的prototype属性是否出现在对象原型链中的任何位置实现步骤&#xff1a; 获取类型的原型。获取对象的原型。一直循环判断对象的原型是否等于构造函数的原型对象&#xff0c…...

29. C语言 可变参数详解

本章目录: 前言可变参数的基本概念可变参数的工作原理如何使用可变参数 示例&#xff1a;计算多个整数的平均值解析&#xff1a; 更复杂的可变参数示例&#xff1a;打印可变数量的字符串解析&#xff1a; 总结 前言 在C语言中&#xff0c;函数参数的数量通常是固定的&#xff…...

蓝桥杯python语言基础(5)——函数

目录 一、作业&#xff1a;四个函数 二、math 三、collections 1. Counter 2. deque 3. defaultdict 4. OrderedDict 四、heapq 五、functool partial偏函数 六、itertools 1.无限迭代器 2.有限迭代器 3.排列组合迭代器 一、作业&#xff1a;四个函数 计算最大公…...

node 爬虫开发内存处理 zp_stoken 作为案例分析

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 前言 主要说3种我们补环境过后如果用…...