【C++】STL——string类的模拟实现

news2025/7/22 8:50:44

文章目录

  • 👉string类👈
    • 📕 概念
    • 📕 成员变量
    • 📕 构造函数、析构函数
    • 📕 size() 、getstr() 函数
    • 📕 拷贝构造
    • 📕 赋值重载
    • 📕 迭代器
    • 📕 运算符重载
    • 📕 尾插
    • 📕 insert() 、erase() 函数
    • 📕 流插入、流提取
  • 👉源代码👈

👉string类👈

📕 概念

string是C++标准库的一个重要的部分,主要用于字符串处理。该类提供了上百个成员函数,本文并不逐一实现,只是实现部分 function 的功能。

📕 成员变量

如下,_str 标识字符串的指针,_size标识的是字符串长度,_capacity表示当前对象的最大存储容量,npos是在erase函数(删除单个字符或者字符串)里使用的,和 string 类的标准用法一样。

	private:
		char* _str;
		int _size;      // 目前的长度,不包括 \0
		int _capacity;  // 最大容量,不包括最后一个 \0
		static size_t npos; 

📕 构造函数、析构函数

如下,要使用初始化列表。同时,在这里需要注意几个点。

  • 传参,使用缺省参数,如果无参数则为 “”
  • 初始化列表只初始化 _size ,如果同时初始化 _capacity,根据初始化列表的规则,是按照成员变量声明的顺序来初始化的,所以如果成员变量的声明顺序改变,会导致一些错误(某个变量为随机值等等),所以在函数体内部对其他的变量进行赋值。
  • _str 要开辟 _capacity+1 的空间,因为要拿多出的一个空间来存储 ‘\0’ 。

同时,_str 开辟 _capacity+1 长度的空间还有一个原因,就是为了方便析构,因为析构是使用delete[ ] 的,如果 _str = new char; 会导致 new 和 delete 不匹配。

		string(const char* s = "")
			:_size(strlen(s))    
		{
			_capacity = _size == 0 ? 3 : _size + 1;
			_str = new char[_capacity+1];
			strcpy(_str, s);
		}

析构函数应该不需要过多解释,如下。

		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

📕 size() 、getstr() 函数

这两个函数要使用 const 修饰成员函数,一方面,该函数不需要改变对象里的任何值;另一方面,如果静态成员函数内部 需要调用该函数,要保证可以被调用(静态成员函数内部 不可以 调用非静态的成员函数和成员变量)。

		int size()const
		{
			return _size;
		}

		char* getstr()const
		{
			return _str;
		}

📕 拷贝构造

如下,strcpy 会自动复制 \0 ,所以不需要担心最后的 \0 问题。

		string(const string& s)
			:_size(s._size)
			, _capacity(s._capacity)
		{
			_str = new char[_capacity + 1];
			strcpy(s._str, _str);
		}

📕 赋值重载

赋值重载的注意点和代码如下。

  • 要先排除赋值重载某个对象本身的情况,用 if 判断即可。
  • 不能一开始就直接删除原来的数据,因为如果new失败,会导致错误。
		string operator=(const string& s)
		{
			if (_str != s._str)
			{
				//delete[] _str;   // 这样写并不好,可能会new失败
				//_str = new char[s._capacity];
				//_size = s._size;
				//_capacity = s._capacity + 1;

				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity + 1;
			}
			return *this;
		}

📕 迭代器

如下,迭代器的设计可以使用函数重载,分别是静态和非静态。反向迭代器设计较为复杂,这里先不做介绍。

		typedef char* iterator;
		typedef const char* const_iterator;

		const_iterator begin()const
		{
			return _str;
		}

		const_iterator end()const
		{
			return _str + _size;
		}

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

与此同时,迭代器中的 begin 设计完成之后,就可以使用范围 for 。范围 for 这个语法糖,必须要在类里面实现 begin ,函数名不能有错,也不能出现大写。

for (auto ch : s2) 
	{
		cout << ch << " ";
	}

📕 运算符重载

如下,对于直接使用方括号取值,可以重载,因为涉及到能改变和不能改变所取的值。

同时,对于比较运算符的重载,可以直接复用。
但是也要用 const 修饰成员函数,可以看 >= 重载的函数体内部和注释。

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		const char& operator[](size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}

		bool operator>(const string& s)const
		{
			return strcmp(_str, s._str) > 0;
		}

		bool operator==(const string& s)const
		{
			return strcmp(_str, s._str) == 0;
		}

		bool operator>=(const string& s)const
		{
			return (*this > s) || (s == *this); // 如果不设为const,这样会报错,因为 s 调用 == ,静态成员调用非静态成员函数
		}

		bool operator<(const string& s)const
		{
			return !(*this >= s);
		}

		bool operator<=(const string& s)const
		{
			return !(*this > s);
		}

📕 尾插

首先,reserve 函数是将对象进行扩容,使其可以存储更多的数据,但是并不会更改原有的数据,所以其函数内部不涉及 _size 的修改。
pushback,尾插一个字符。
append,尾插字符串,先将一部分字符串后移,再插入。

当然了,我们更喜欢使用的是 += ,可以直接调用 pushback 和 append。

		void reserve(int n) // 扩容,不改变数据
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}

		void pushback(char ch)
		{
			if (_size + 1 > _capacity)
			{
				reserve(_capacity*2);
			}

			_str[_size] = ch;
			_size++;
			_str[_size] = '\0';
		}

		void append(const char* str)
		{
			int len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, str);  // 如果使用strcat,这个函数需要自己去找 \0 ,比较浪费时间
			_size += len;
		}

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

		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

📕 insert() 、erase() 函数

insert 函数重载,在任意位置插入单个字符或者字符串。
需要注意的是,insert是通过下标来访问和插入的,同时,存储下标的数据类型是 size_t ,无符号整形,该类型数据不会有负数,当取到理论上的 -1 时,实际上时它所能存储的最大正整数。如下图。
所以在 insert() 函数的第一个 while 循环语句上面,size_t end = _size +1 ; 这样子的话,最后end 的位置就是在下标为 1 的地方,不会变成 ffff (十六进制)而导致死循环。
那么为什么不直接用 int 类型的数据呢?因为参数 pos 是 size_t (STL标准里面也是这样),end 和 pos 比较,会把 end 当成 size_t 的类型和 pos 比,这里旨在模拟 STL 实现 string,所以要尽可能地贴合标准库。

请添加图片描述

erase 函数是在任意的位置删除任意长度的字符。

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

			_str[pos] = ch;
			_size++;

			return *this;
		}

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

			return *this;
		}

		string& erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);
			// 要删除的内容大于等于pos之后的长度
			if (len == npos || pos + len >= _size) // 要判断 len == npos ,否则如果 pos+len 溢出就麻烦了
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else  // 要删除的内容没有超过最后一个字符
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

📕 流插入、流提取

在流插入的时候,不可以直接 in>>ch; 要当成一个字符一个字符输入,因为 cin 会把空格和换行当作是字符串之间的分割符,不会将其当作一个完整的字符串,所以要使用 istream 类里面的 get 函数,该函数可以拿到空格和换行,不会将其当作间隔处理 。

此外,如果输入字符串过长,每一次都 += ,可能会需要频繁扩容,空间消耗比较大,所以可以设计一个缓冲区,存储满之后将缓冲区里面的数据放到 s 里面。当然,while 循环结束后也要判断缓冲区是否为空,因为可能最后一次没有将缓冲区填满,而导致没有将最后一段字符串放到 s 里面。

istream& operator>>(istream& in, string& s)
	{
		s.clear(); // 要输入之前先清空

		char ch = in.get();   
		char buff[128];       // 缓冲区
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)   // 满了,最后一个位置放 \0 ,然后将缓冲区的数据放到s里面
			{
				buff[127] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();  
		}

		if (i)  // 缓冲区还有数据没有存到s里面的情况
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}

	ostream& operator<<(ostream& out, const string& s)
	{
	    // 自己写了迭代器,就可以使用范围 for 了,可以不用友元函数
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}

👉源代码👈

如下,是 string 类的源代码,将它放到一个命名空间里面,为了防止和标准库里面的某些名字冲突。


#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<iostream>
#include<algorithm>
#include<assert.h>

namespace simulate
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;


		const_iterator begin()const
		{
			return _str;
		}

		const_iterator end()const
		{
			return _str + _size;
		}

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		// string(const char* s = nullptr) 是不可以的,传参 '\0' 也不可以。因为cout对于char*类型的,是一直读到 \0 为止
		string(const char* s = "")
			:_size(strlen(s))    // 如果同时初始化_capacity,会导致按照申明的顺序初始化,顺序改动会造成随机值
		{
			_capacity = _size == 0 ? 3 : _size + 1;
			_str = new char[_capacity+1];
			strcpy(_str, s);
		}

		string(const string& s)
			:_size(s._size)
			, _capacity(s._capacity)
		{
			_str = new char[_capacity + 1];
			strcpy(s._str, _str);
		}

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

		char* c_str()const
		{
			return _str;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		const char& operator[](size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}

		int size()const
		{
			return _size;
		}

		bool operator>(const string& s)const
		{
			return strcmp(_str, s._str) > 0;
		}

		bool operator==(const string& s)const
		{
			return strcmp(_str, s._str) == 0;
		}

		bool operator>=(const string& s)const
		{
			return (*this > s) || (s == *this); // 如果不设为const,这样会报错,因为 s 调用 == ,静态成员调用非静态成员函数
		}

		bool operator<(const string& s)const
		{
			return !(*this >= s);
		}

		bool operator<=(const string& s)const
		{
			return !(*this > s);
		}

		void resize(size_t n,char ch='\0')
		{
			// 删除数据,保留前n个
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else if (n > _size)  // 不用考虑 n = _size 的情况,因为这种情况不需要改变任何值
			{
				if (n > _capacity)
				{
					reserve(n);
				}
				size_t i = _size;
				while (i < n)
				{
					_str[i] = ch;
					i++;
				}
				_size = n;
				_str[_size] = '\0';
			}
		}

		void reserve(size_t n) // 扩容,不改变数据
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}

		void pushback(char ch)
		{
			if (_size + 1 > _capacity)
			{
				reserve(_capacity*2);
			}

			_str[_size] = ch;
			_size++;
			_str[_size] = '\0';
		}

		void append(const char* str)
		{
			int len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, str);  // 如果使用strcat,这个函数需要自己去找 \0 ,比较浪费时间
			_size += len;
		}

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

		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

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

			_str[pos] = ch;
			_size++;

			return *this;
		}

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

			return *this;
		}

		string& erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);
			// 要删除的内容大于等于pos之后的长度
			if (len == npos || pos + len >= _size) // 要判断 len == npos ,否则如果 pos+len 溢出就麻烦了
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else  // 要删除的内容没有超过最后一个字符
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		size_t find(const char ch,size_t pos=0)
		{
			assert(pos <= _size);
			for (int i = pos; i < _size; i++)
			{
				if (_str[ch] = i) return i;
			}
			return npos;
		}

		size_t find(const char* str, size_t pos = 0)
		{
			assert(pos < _size);
			char* p = strstr(_str, str);
			if (p == nullptr)
			{
				return npos;
			}
			return p - _str;
		}

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

	private:
		char* _str;
		int _size;      // 目前的长度,不包括 \0
		int _capacity;  // 最大容量,不包括最后一个 \0

		static const size_t npos; 
	};

	const size_t string::npos = -1;

	istream& operator>>(istream& in, string& s)
	{
		s.clear(); // 要输入之前先清空

		char ch = in.get();   
		char buff[128];       // 防止频繁扩容,缓冲区
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)   // 满了,最后一个位置放 \0 ,然后将缓冲区的数据放到s里面
			{
				buff[127] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get(); 
		}

		if (i)  // 缓冲区还有数据没有存到s里面的情况
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}

	ostream& operator<<(ostream& out, const string& s)
	{
	    // 自己写了迭代器,就可以使用范围 for 了,可以不用友元函数
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}


}

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

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

相关文章

如何管控第三方软件,保护企业数据安全?

日前&#xff0c;密码管理供应商LastPass公布了关于其数据泄露事件的调查新进展。据其透露&#xff0c;这是一起“二次协同攻击”事件。LastPass在2022年8月、12月先后披露的两起违规事件&#xff0c;这两起事件的攻击链有关联。在此次攻击事件中&#xff0c;LastPass发现恶意黑…

王道计算机组成原理课代表 - 考研计算机 第五章 中央处理器 究极精华总结笔记

本篇博客是考研期间学习王道课程 传送门 的笔记&#xff0c;以及一整年里对 计算机组成 知识点的理解的总结。希望对新一届的计算机考研人提供帮助&#xff01;&#xff01;&#xff01; 关于对 “中央处理器” 章节知识点总结的十分全面&#xff0c;涵括了《计算机组成原理》课…

C++基础——C++相比C语言的新特性梳理总结(C++新特性、输入输出方式、命名空间namespace)

【系列专栏】&#xff1a;博主结合工作实践输出的&#xff0c;解决实际问题的专栏&#xff0c;朋友们看过来&#xff01; 《QT开发实战》 《嵌入式通用开发实战》 《从0到1学习嵌入式Linux开发》 《Android开发实战》 《实用硬件方案设计》 长期持续带来更多案例与技术文章分享…

论文翻译 | Momentum Contrast for Unsupervised Visual Representation Learning(前三章)

前言&#xff1a; 上一次读恺明大神的文章还是两年前&#xff0c;被ResNet的设计折服得不行&#xff0c;两年过去了&#xff0c;我已经被卷死在沙滩上 Momentum Contrast for Unsupervised Visual Representation Learning 摘要 我们提出了针对无监督表征学习的方法MOCO,利用…

上门按摩预约APP源码-东郊到家源码(开发,PHP,平台搭建)

一、什么是上门按摩预约APP源码&#xff1f; 上门按摩预约APP源码是一款家政服务类型的APP&#xff0c;可以帮忙用户在家就能享受按摩的服务。APP源码分两端&#xff0c;一端是用户端&#xff0c;另外一端是技师端。采用的技术&#xff0c;前端是安卓IOS&#xff0c;后端是PHP&…

java_Day004

1.二维数组 二维数组的创建与初始化&#xff08;java是支持规则数组和不规则数组的&#xff09; 例&#xff1a;int[][] array {{1,2},{2,3}{3,4,5}}; 结构如下&#xff1a; 二维数组的遍历&#xff1a; 例子&#xff1a; Testpublic void test21(){int[][] array new int[…

C++学习记录——십이 vector

文章目录1、vector介绍和使用2、vector模拟实现insert和erase和迭代器失效补齐其他函数深浅拷贝难点思考1、vector介绍和使用 vector可以管理任意类型的数组&#xff0c;是一个表示可变大小数组的序列容器。 通过vector文档来看它的使用。 #include <iostream> #inclu…

集群、分布式的理解

一、单机模式小型系统相对简单&#xff0c;所有的业务全部写在一个项目中&#xff0c;部署服务到一台服务器上&#xff0c;所有的请求业务都由这台服务器处理&#xff0c;这就是单机模式。显然&#xff0c;当业务增长到一定程度的时候&#xff0c;服务器的硬件会无法满足业务需…

强化学习 | 课堂笔记 | 第三课 MP的便利性,随机逼近方法

一、回顾 一、值函数、贝尔曼方程、贝尔曼最优方程 二、最优值函数 三、ADP 3.1 VI 3.2 PI 四、ADP可以使用的条件 五、Q函数 六、解决问题的方案 &#xff08;指的是解决“四 ADP可以使用的条件”中的三个问题&#xff09; 二、期望的计算 一、Markov过程的便利性 1…

新搭建Gitlab代码仓代码如何导入

这里写目录标题一级目录1.本地代码如何导入新Gitlab2.怎么将旧Gitlab代码导入新Gitlab一级目录 1.本地代码如何导入新Gitlab 修改本地代码 .git 目录下面的config 文件&#xff0c;主要是url参数&#xff0c;将url指向新的Gitlab仓库地址 [core]repositoryformatversion 0f…

【1096. 花括号展开 II】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 如果你熟悉 Shell 编程&#xff0c;那么一定了解过花括号展开&#xff0c;它可以用来生成任意字符串。 花括号展开的表达式可以看作一个由 花括号、逗号 和 小写英文字母 组成的字符串&#xff0c;定…

E900V21C(S905L-armbian)安装armbian-Ubuntu(WiFi)

基本上是s905L芯片的刷机都是如此&#xff0c;包括Q7等 在网上寻找好多的教程关于e900v21c的刷机包和教程都少的可怜&#xff0c;唯一的就是这个&#xff1a;山东联通版创维E900V21C盒子刷入Armbiam并安装宝塔和Docker&#xff0c;但他是不能用WiFi和蓝牙的然后就是寻找s90l的…

C++基础了解-01-基础语法

基础语法 一、基础语法 C 程序可以定义为对象的集合&#xff0c;这些对象通过调用彼此的方法进行交互。现在让我们简要地看一下什么是类、对象&#xff0c;方法、即时变量。 对象 - 对象具有状态和行为。例如&#xff1a;一只狗的状态 - 颜色、名称、品种&#xff0c;行为 -…

【LeetCode每日一题】——334.递增的三元子序列

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【题目进阶】九【时间频度】十【代码实现】十一【提交结果】一【题目类别】 贪心算法 二【题目难度】 中等 三【题目编号】 334.递增的三元子序列 四【题…

Vue3视频播放组件(Video)

Vue2视频播放组件 可自定义设置以下属性&#xff1a; 视频文件url&#xff08;videoUrl&#xff09;&#xff0c;必传&#xff0c;支持网络地址https和相对地址 视频封面url&#xff08;videoCover&#xff09;&#xff0c;默认为null&#xff0c;支持网络地址https和相对地…

【nacos2.2.1本地启动】

nacos2.2.1本地启动填坑之行 下载nacos代码 nacos文档地址&#xff1a;https://nacos.io/zh-cn/docs/quick-start-spring.html github地址下载代码&#xff1a;https://github.com/alibaba/nacos.git appllo文章&#xff1a;https://blog.51cto.com/muxiaonong/3933418 下…

UEFI学习(三)-创建一个dxe driver-UDK2017

创建一个dxe driver 创建UEFI DXE driver DXE驱动的运行阶段 DXE驱动创建 创建UEFI DXE driver 在edk2中&#xff0c;我们可以了解到它有非常多种类的模块&#xff0c;每种模块运行于不同阶段&#xff0c;上一阶段&#xff0c;我们尝试了一下标准应用程序的工程模块&#xff0c…

Centos7超详细安装教程

Centos 7适合初入门的带图形化的界面系统安装 本文是基于VMware虚拟机&#xff0c;centos7 64位安装教学 文章目录Centos 7适合初入门的带图形化的界面系统安装一、软件准备二、VMware新建适配虚拟机三、Centos 安装四、基础检查一、软件准备 VMware 虚拟机安装 官网下载链接&…

Redis 做延迟消息队列

背景 看到消息队列&#xff0c;我们肯定会想到各种MQ&#xff0c;比如&#xff1a;RabbitMQ&#xff0c;acivityMQ、RocketMQ、Kafka等。 但是&#xff0c;当我们需要使用消息中间件的时候&#xff0c;并非每次都需要非常专业的消息中间件&#xff0c;假如我们只有一个消息队…

问一下ChatGPT:DIKW金字塔模型

经常看到这张DIKW金字塔模型图&#xff0c;还看到感觉有点过份解读的图&#xff0c;后面又加上了insight&#xff0c;impact等内容。 Data&#xff1a;是数据&#xff0c;零散的、无规则的呈现到人们眼前&#xff0c;如果你只看到这些数字&#xff0c;如果没有强大的知识背景&a…