STL —— string(3)

news2024/12/23 4:05:22

目录

1. 使用

1.1 c_str()

 1.2 find() & rfind()

1.3 substr()

1.4 打印网址的协议域名等

1.5 find_first_of()

2. string() 模拟实现

2.1 构造函数的模拟实现

2.2 operator[] 和 iterator 的模拟实现

2.3 push_back() & append() & += 的模拟实现

2.4 insert() & erase() 的模拟实现

2.5 resize() 模拟实现

2.6 析构函数和复制重载模拟


大部分的用法我在上一篇已经讲解过了,这边文章主要讲解还有的部分用法以及string类的模拟实现。

1. 使用

1.1 c_str()

  • c_str() 用于返回一个指向以 null 结尾的 C 字符串(即以 null 终止的字符数组)的指针,该字符串包含了 std::string 对象的内容。
void string_test1()
{
    std::string str("Hello, world!");

    // 使用 c_str() 返回指向以 null 结尾的 C 字符串的指针
    const char* cstr = str.c_str();

    // 打印返回的 C 字符串
    std::cout << "C string: " << cstr << std::endl;
}

 1.2 find() & rfind()

find 函数用于在字符串中查找子串。find 函数有多个重载版本,可以根据需要进行使用。主要的两个版本如下:

  1. size_t find(const string& str, size_t pos = 0) const; 这个版本的 find 函数在当前字符串中从位置 pos 开始查找子串 str,并返回找到的子串的第一个字符的位置索引。如果未找到子串,则返回 string::npos

  2. size_t find(const char* s, size_t pos = 0) const; 这个版本的 find 函数与上一个版本类似,但接受一个 C 风格的字符串作为参数,而不是 std::string 对象。

  3. 其他版本:

void string_test2()
{
    std::string str = "Hello, world!";
    std::string substr = "world";

    // 在字符串中查找子串
    size_t found = str.find(substr);
    if (found != std::string::npos) 
    {
        std::cout << "子串 '" << substr << "' 在字符串中的位置索引为: " << found << std::endl;
    }
    else 
    {
        std::cout << "未找到子串 '" << substr << "'." << std::endl;
    }
}

rfind 用于在字符串中从后向前查找子串,并返回找到的子串的第一个字符的位置索引。

  • 主要用法:size_type rfind (const basic_string& str, size_type pos = npos) const;
  • 其他用法:
void string_test2()
{
    std::string str = "Hello, world!";
    std::string substr = "world";

    // 在字符串中查找子串
    size_t found = str.rfind(substr);
    if (found != std::string::npos) 
    {
        std::cout << "子串 '" << substr << "' 在字符串中的位置索引为: " << found << std::endl;
    }
    else 
    {
        std::cout << "未找到子串 '" << substr << "'." << std::endl;
    }
}

1.3 substr()

substr 是 C++ 中 std::string 类的成员函数,用于从字符串中提取子串:

  • basic_string substr (size_type pos = 0, size_type len = npos) const;
  • 其他用法:

这里是这两个重载函数的参数说明:

  • pos:指定子串的起始位置,即要提取的子串在原始字符串中的起始索引。
  • len:可选参数,指定要提取的字符数目。如果不指定或者指定为 npos,则提取从 pos 开始直到字符串末尾的所有字符。
void string_test3()
{
    std::string str = "Hello, world!";

    // 提取子串,从索引位置 7 开始,包括 5 个字符
    std::string sub1 = str.substr(7, 5);
    std::cout << "子串1: " << sub1 << std::endl;

    // 提取子串,从索引位置 7 开始,直到字符串末尾的所有字符
    std::string sub2 = str.substr(7);
    std::cout << "子串2: " << sub2 << std::endl;

}

1.4 打印网址的协议域名等

  • 我们可以使用 find() 和 substr() 分开打印网址的协议和域名
void string_test4()
{
    string url1("https://legacy.cplusplus.com/reference/string/basic_string/substr/");
    string url("https://mp.csdn.net/mp_blog/creation/editor?spm=1000.2115.3001.4503");
    string protocol;
    string domain;
    string port;
    size_t pos = 0;
    pos = url.find(":");
    if (pos != string::npos)
    {
        protocol = url.substr(0, pos);
    }
    int pos1 = pos + 3;
    pos = url.find('/', pos1);
    if (pos1 != string::npos)
    {
        domain = url.substr(pos1, pos - pos1);
    }
    int pos2 = pos + 1;
    port = url.substr(pos2);
    cout << protocol << endl;
    cout << domain << endl;
    cout << port << endl;
}


1.5 find_first_of()

find_first_of 用于在字符串中查找第一个与指定字符序列中的任何一个字符匹配的字符,并返回其位置索引,主要用法如下:

  • size_type find_first_of (const basic_string& str, size_type pos = 0) const;

这里是参数说明:

  • str:指定要搜索的字符序列。
  • pos:可选参数,指定搜索的起始位置。默认值为 0,表示从字符串的开头开始搜索。
  • 其他用法:

用法示例:

void string_test5()
{
    std::string str("PLease, replace the vowels in this sentence by asterisks.");
    std::string::size_type found = str.find_first_of("aeiou");
    while (found != std::string::npos)
    {
        str[found] = '*';
        found = str.find_first_of("aeiou", found + 1);
    }

    std::cout << str << '\n';
}

2. string() 模拟实现

2.1 构造函数的模拟实现

  • 无参构造,切忌不能全都初始化为0,这里会导致未定义的错误。
string()
    :_string(nullptr)
	,_size(0)
	,_capacity(0)
	{}

const char* c_str()
{
	return _string;
}
  • 正确定义方法如下:
string()
    :_string(new char[1])
    ,_size(0)
    ,_capacity()
    {
	    _string[0] = '\0';
    }
  • 但一般不会这样定义,一般定义全缺省的形式:
string(const char* ch = "")
		:_size(strlen(ch))
		{
			_capacity = _size;
            // 多开一个空间用来存放 '\0'
			_str = new char[_capacity + 1];
			strcpy(_str, ch);
		}

2.2 operator[] 和 iterator 的模拟实现

  • operator[] 是一个运算符重载,返回第pos个位置的值,代码如下:
size_t size() const
{
	return _size;
}

// 注意这里是传引用返回,不是传值返回,传值就不能改变_str() 里面的值了
char& operator[](size_t pos)
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}



void string_test2()
{
	string str1("hello world");
	for (size_t i = 0; i < str1.size(); ++i)
	{
		cout << str1[i] << " ";
	}
	cout << endl;
}
  • 如果对于const的成员,就要用const来修饰函数,并且要求返回值不能修改,那么返回值也要加上const来修饰:
const char& operator[](size_t pos) const
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}
  • 测试代码
    void string_test2()
	{
		string str1("hello world");
        // const 类型对象
		const string str2("hello handsome boy");
		for (size_t i = 0; i < str1.size(); ++i)
		{
			cout << str1[i] << " ";
		}
		cout << endl;
        // 可以进行修改
		for (size_t i = 0; i < str1.size(); ++i)
		{
			str1[i]++;
		}
		cout << endl;
		for (size_t i = 0; i < str1.size(); ++i)
		{
			cout << str1[i] << " ";
		}
		cout << endl;
		for (size_t i = 0; i < str2.size(); ++i)
		{
			cout << str2[i] << " ";
		}
		cout << endl;

		// 会报错,不能进行修改
		/*for (size_t i = 0; i < str2.size(); ++i)
		{
			str2[i]++;
		}*/

	}

  • iterator 在上一篇文章将结果,是类似于指针的形式,但是实际上可以拿指针来实现,也可以不用指针实现,以下是用指针实现的模式。
  • 首先要有begin() 和 end() 两个函数,如下:
    typedef char* iterator;
    iterator begin()
    {
    	return &_str[0];
    }
    
    iterator end()
    {
    	return &_str[_size];
    }
  •  对于const类型与非const构成重载:
    typedef const char* const_iterator;
    const_iterator begin() const
    {
    	return &_str[0];
    }
    
    const_iterator end() const
    {
    	return &_str[_size];
    }
  •  测试代码
        void string_test3()
    	{
    		string str1("hello world");
    		const string str2("hello handsome boy");
    		string::iterator it = str1.begin();
    		while (it != str1.end())
    		{
    			cout << *it << " ";
    			++it;
    		}
    		cout << endl;
    		it = str1.begin();
    		while (it != str1.end())
    		{
    			++(*it);
    			++it;
    		}
    		while (it != str1.end())
    		{
    			cout << *it << " ";
    			++it;
    		}
    		cout << endl;
    		string::const_iterator itt = str2.begin();
    		while (itt != str2.end())
    		{
    			cout << *itt << " ";
    			++itt;
    		}
    		// 会报错
    		/*itt = str2.begin();
    		while (itt != str2.end())
    		{
    			++(*itt);
    			++itt;
    		}*/
    	}


2.3 push_back() & append() & += 的模拟实现

  • 既然要插入字符串,那么肯定是需要扩容的,在库中可以使用 reserve() 进行扩容,我们也可以手动实现一个 reserve() 函数。

实现思路如下:

  1. 先new一块大小为n的空间;
  2. 然后把_str中的内容拷贝到tmp中;
  3. 接着释放掉_str指向的空间;
  4. 最后再让_str指向tmp。
void reserve(size_t n = 0)
{
	if (n > _capacity)
	{
		char* tmp = new char[n];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
        _capacity = n;
	}
}

push_back的实现思路:

  1. 首先检查_size 和 _capacity 相不相等,如果相等就需要扩容;
  2. 在_size的位置插入字符,然后_size++;
  3. 在_size的位置插入'\0'。
void push_back(char ch)
{
	if (_size == _capacity)
	{
		int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(newcapacity);
	}
	_str[_size++] = ch;
	_str[_size] = '\0';
}

append插入的实现思路:

  1. 首先计算要插入的字符串的长度len;
  2. 如果_size + len > _capacity 就需要扩容;
  3. 再用strcpy() 把字符串拷贝进_str。
string& append(const char* ch)
{
	size_t len = strlen(ch);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	strcpy(_str + _size, ch);
	_size = _size + len;
	return *this;
}

operator+=的实现就直接调用append 和 push_back 就可以了

string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

string& operator+=(const char* ch)
{
	append(ch);
	return *this;
}
  • 测试代码
    void string_test4()
	{
		string str1("hello world");
		str1.push_back(' ');
		str1.push_back('h');
		str1.push_back('e');
		str1.push_back('l');
		str1.push_back('l');
		str1.push_back('o');
		str1.push_back(' ');
		str1.push_back('h');
		str1.push_back('a');
		str1.push_back('n');
		str1.push_back('d');
		str1.push_back('s');
		str1.push_back('o');
		str1.push_back('m');
		str1.push_back('e');
		str1.push_back(' ');
		str1.push_back('b');
		str1.push_back('o');
		str1.push_back('y');

		str1.append(" hello handsome boy");

		for (size_t i = 0; i < str1.size(); ++i)
		{
			cout << str1[i] << " ";
		}
		cout << endl;
		cout << str1.size() << endl;
	}
    void string_test5()
	{
		string str1("hello world");
		str1 += ' ';
		str1 += "hello handsome boy";
		for (size_t i = 0; i < str1.size(); ++i)
		{
			cout << str1[i] << " ";
		}
		cout << endl;
		cout << str1.size() << endl;
	}

2.4 insert() & erase() 的模拟实现

insert() 就是在某个位置插入字符或者字符串,首先讲一下插入字符思路如下:

  1. 判断pos是否合法;
  2. 检查是否需要扩容;
  3. 将pos位置之后的字符全都向后移动一位;
  4. 在pos位置插入字符;
  5. _size++;

        注意这里的end要给_size + 1,如果给_size,当pos == 0的时候,最后一轮进循环end会变为-1,然而end是size_t类型的,-1就会变为整形的最大值,就会死循环。

        就算把end改为int类型也不行,因为括号内比较的类型是end和pos,而pos是size_t类型的,会发生类型转换,还是会把end转换成size_t类型,因此还是会死循环。

string& insert(size_t pos, char ch)
{
	assert(pos >= 0 && pos <= _size);
	if (_size == _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(newcapacity);
	}
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = ch;
	++_size;
	return *this;
}

insert() 插入字符串类型:

  1. 计算是否需要扩容以及pos位置是否合法;
  2. 将第pos位置之后的元素移动len个字符;
  3. 在pos位置插入字符串;
  4. _size加上len;

这里的移动有两种思路,一种是每次移动一次,移动len次

另一种是直接移动len次

先看一下每次移动一次的,时间复杂度为 O(N^2) , 比较容易理解:

string& insert(size_t pos, const char* ch)
{
	assert(pos >= 0 && pos <= _size);
	size_t len = strlen(ch);
	if (len + _size > _capacity)
	{
		reserve(len + _size);
	}
	size_t pos1 = pos;
	while (len--)
	{
		size_t end = _size + 1;
		while (end > pos1)
		{
			_str[end] = _str[end - 1];
			--end;
		}
		_str[pos1] = *ch;
		++ch;
		++pos1;
		++_size;
	}
	return *this;
}

一次直接移动len次,以下是动图图解:

string& insert(size_t pos, const char* ch)
{
	assert(pos >= 0 && pos <= _size);
	size_t len = strlen(ch);
	if (len + _size > _capacity)
	{
		reserve(len + _size);
	}
	int end = _size + len;
	while (end >= pos + len)
	{
		_str[end] = _str[end - len];
		--end;
	}
	strncpy(_str + pos, ch, len);
	_size += len;
	return *this;
}

erase()就是删除某个位置的元素,较为容易,直接上代码:

string erase(size_t pos = 0, size_t len = npos)
{
	assert(pos >= 0 && pos < _size);
	if (len >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + 1);
		_size = _size - len;
	}
	return *this;
			
}
  • 测试代码
    void string_test6()
	{
		string str1("hello world");
		str1.insert(0, 'x');
		for (size_t i = 0; i < str1.size(); ++i)
		{
			cout << str1[i] << " ";
		}
		cout << endl;
		cout << str1.size() << endl;
		str1.insert(12, 'y');
		for (size_t i = 0; i < str1.size(); ++i)
		{
			cout << str1[i] << " ";
		}
		cout << endl;
		cout << str1.size() << endl;
		str1.insert(0, "zzz");
		for (size_t i = 0; i < str1.size(); ++i)
		{
			cout << str1[i] << " ";
		}
		cout << endl;
		cout << str1.size() << endl;
		str1.insert(16, "dddddddddddd");
		for (size_t i = 0; i < str1.size(); ++i)
		{
			cout << str1[i] << " ";
		}
		cout << endl;
		cout << str1.size() << endl;
		str1.erase(16,12);
		for (size_t i = 0; i < str1.size(); ++i)
		{
			cout << str1[i] << " ";
		}
		cout << endl;
		cout << str1.size() << endl;
	}

2.5 resize() 模拟实现

之前讲过 resize() 的三种形式,这里就不详细讲解了,有兴趣的可以看上一篇文章:

STL —— string(2)-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_68617301/article/details/136954250

 这里直接讲模拟实现方式:

void resize(size_t n, char ch = '\0')
{
	if (n < _size)
	{
		_str[n] = '\0';
	}
	else if (n >= _size)
	{
		reserve(n);
		for (size_t i = _size; i < n; i++)
		{
			_str[i] = ch;
		}
		_str[n] = '\0';
		_size = n;
	}
	return *this;
}
  •  测试代码
    void string_test7()
	{
		string str1("hello world");
		str1.resize(20, 'x');
		for (size_t i = 0; i < str1.size(); ++i)
		{
			cout << str1[i] << " ";
		}
		cout << endl;
		cout << str1.size() << endl;

	}

2.6 析构函数和复制重载模拟

  • 这里的析构函数同样也涉及到深浅拷贝的问题,因为这里也同样有一个在堆上开辟的空间,如果调用编译器自己生成的析构函数,就会造成对同一块空间重复释放的问题,需要我们自己编写析构函数:
~string()
{
	_size = _capacity = 0;
	if (_str != nullptr)
	{
		delete[] _str;
	}
}

string& operator=(string& str)
{
	if (this != &str)
	{
		_size = str._size;
		_capacity = str._capacity;
		char* tmp = new char[_capacity];
		delete[] _str;
		strcpy(tmp, str._str);
		_str = tmp;
	}
			
	return *this;
}

 

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

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

相关文章

C语言运算符优先级介绍

1. 引言 什么是运算符 运算符是编程中用于执行算术、比较和逻辑操作的符号。它们是构建表达式的基本工具&#xff0c;类似于数学中的加、减、乘和除。 程序片段示例: 简单的算术运算符使用 #include <stdio.h>int main() {int a 5, b 2;int sum a b; // 使用加法…

发车,易安联签约某新能源汽车领军品牌,为科技创新保驾护航

近日&#xff0c;易安联成功签约某新能源汽车领军品牌&#xff0c;为其 数十万终端用户 建立一个全新的 安全、便捷、高效一体化的零信任终端安全办公平台。 随着新能源汽车行业的高速发展&#xff0c;战略布局的不断扩大&#xff0c;技术创新不断引领其市场价值走向高点&am…

计算机网络——数据链路层(差错控制)

计算机网络——数据链路层&#xff08;差错控制&#xff09; 差错从何而来数据链路层的差错控制检错编码奇偶校验码循环冗余校验&#xff08;CRC&#xff09;FCS 纠错编码海明码海明距离纠错流程确定校验码的位数r确定校验码和数据位置 求出校验码的值检错并纠错 我们今年天来继…

C#打印50*30条码标签

示例图&#xff1a; 源码下载地址&#xff1a;https://download.csdn.net/download/tiegenZ/89035407?spm1001.2014.3001.5503

【Java程序设计】【C00379】基于(JavaWeb)Springboot的旅游服务平台(有论文)

【C00379】基于&#xff08;JavaWeb&#xff09;Springboot的旅游服务平台&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;已经做了六年的毕业设计程序开发&#xff0c…

STM32F103 CubeMX 使用USB生成键盘设备

STM32F103 CubeMX 使用USB生成键盘设备 基础信息HID8个数组各自的功能 生成代码代码编写添加申明信息main 函数编写HID 修改1. 修改报文描述符2 修改 "usbd_hid.h" 中的申明文件 基础信息 软件版本&#xff1a; stm32cubmx&#xff1a;6.2 keil 5 硬件&#xff1a;…

【剑指offer】顺时针打印矩阵

题目链接 acwing leetcode 题目描述 输入一个矩阵&#xff0c;按照从外向里以顺时针的顺序依次打印出每一个数字。 数据范围矩阵中元素数量 [0,400]。 输入&#xff1a; [ [1, 2, 3, 4], [5, 6, 7, 8], [9,10,11,12] ] 输出&#xff1a;[1,2,3,4,8,12,11,10,9,5,6,7] 解题 …

MySQL【三】操作数据库基础

库、表、记录的概念 库&#xff08;Database&#xff09;&#xff1a;库是一个容器&#xff0c;用于存储表和其他对象&#xff08;如视图、存储过程等&#xff09; 表&#xff08;Table&#xff09;&#xff1a;表是一个由列和行组成的矩阵&#xff0c;其中每列都定义了表中的…

MQTT.fx和MQTTX 链接ONENET物联网提示账户或者密码错误

参考MQTT.fx和MQTTX 链接ONENET物联网开发平台避坑细节干货。_mqttx和mqttfx-CSDN博客 在输入password和username后还是提示错误&#xff0c;是因为在使用token的时候&#xff0c;key填写错误&#xff0c;将设备的密钥填入key中

webpack练习之手写loader

手写一个style-loader来把样式文件插入head里面&#xff0c;准备工作 vue webpack就自己弄了&#xff0c;webpack的一些配置也自己配置好 一、创建index.css文件 .box{width: 100px;height: 100px;background-color: red; }然后在vue的main.js文件中引入它 二、创建自定义l…

MyBatis-Plus分页接口实现教程:Spring Boot中如何编写分页查询

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

港大新工作 HiGPT:一个模型,任意关系类型 !

论文标题&#xff1a; HiGPT: Heterogeneous Graph Language Model 论文链接&#xff1a; https://arxiv.org/abs/2402.16024 代码链接&#xff1a; https://github.com/HKUDS/HiGPT 项目网站&#xff1a; https://higpt-hku.github.io/ 1. 导读 异质图在各种领域&#xf…

云原生靶场kebernetesGoat、Metarget

靶场 文章目录 靶场kebernetesGoat靶场安装Docker in DockerSSRF漏洞容器逃逸到主系统Docker CIS 基线分析Kubernetes CIS 安全基线分析分析被部署挖矿软件的容器镜像获取环境信息Hidden in layersRBAC最低权限配置错误使用 Sysdig Falco 进行运行时安全监控和检测 Metarget ke…

UE5、CesiumForUnreal实现海量POI撒点显示与聚合功能

1.实现目标 POI是UE+GIS三维场景中经常需要展示的要素,在UE中常用的表示POI方法有两种。一种是Mesh,即空间的方式;另一种是Widget,即屏幕上的方式,本文这里使用的是Widget屏幕展示的形式来表示POI。 本文这里使用的POI点位数量共3.3w+,采用直接网格聚合算法,并进行性能优…

ROS机器人入门第四课:话题通信

文章目录 ROS机器人入门第四课&#xff1a;话题通信一、话题通信概述&#xff08;一&#xff09;概念&#xff08;二&#xff09;作用 二、话题通信基本操作需求:分析:流程:&#xff08;一&#xff09;发布方解释一些关键的ROS函数和概念&#xff1a; &#xff08;二&#xff0…

2024年福建事业单位招聘详细流程

2024年福建事业单位招聘详细流程&#xff0c;速速查收&#xff01;

湖北汽车工业学院 实验一 关系数据库标准语言SQL

头歌 实验一 关系数据库标准语言SQL 制作不易&#xff01;点个关注呗&#xff01;为大家创造更多的价值&#xff01; 目录 头歌 实验一 关系数据库标准语言SQL**制作不易&#xff01;点个关注呗&#xff01;为大家创造更多的价值&#xff01;** 第一关&#xff1a;创建数据库第…

简单服务器通信、IO多路复用(select、poll、epoll)以及reactor模式。

网络编程学习 简单服务器通信TCP三次握手和四次挥手三次握手&#xff08;如下图&#xff09;常见问题&#xff1f; 四次挥手 client和server通信写法server端client端 通信双方建立连接到断开连接的状态转换怎么应对多用户连接&#xff1f;缺点 IO多路复用select优缺点 pollpol…

算法---动态规划练习-5(下降路径最小和)

下降路径最小和 1. 题目解析2. 讲解算法原理方法一方法二 3. 编写代码法一法二 1. 题目解析 题目地址&#xff1a;点这里 2. 讲解算法原理 方法一 首先&#xff0c;通过matrix的大小确定矩阵的行数m和列数n。 创建一个大小为(m1) (n2)的二维动态规划数组dp&#xff0c;其中d…

OC 技术 苹果内购

一直觉得自己写的不是技术&#xff0c;而是情怀&#xff0c;一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的&#xff0c;希望我的这条路能让你们少走弯路&#xff0c;希望我能帮你们抹去知识的蒙尘&#xff0c;希望我能帮你们理清知识的脉络&#xff0…