[C++随笔录] string模拟实现

这篇具有很好参考价值的文章主要介绍了[C++随笔录] string模拟实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

放在前面:
我们实现string类, 并不是跟源码一模一样(并不是造一个新的轮子), 只是了解每个接口的实现过程 ⇒ 我们以后也用的放心(比如时间复杂度, 空间复杂度等等)

基本结构

private:
	size_t _size; // 实际长度
	size_t _capacity; // 空间
	char* _str;

习惯把不可能为负数的值的类型 定义为 size_t

天选之子

构造函数

  1. 考虑到 无参调用和有参调用 && 只有一个参数 ⇒ 我们可以采用 全缺省的形式
  2. 传参类型应该为 常量字符串 ⇒ const char* ⇐ 一般用于初始化, 咋们给的值都是常量
  3. 缺省值初始化为 ""(空字符串) ⇐ 常量字符串默认就会有 \0, 即 “” (空字符串) 里面就是一个 \0
  4. _size 和 _capacity 的大小不包括 \0 ⇒ 所以, 我们初始化长度的时候, 用 strlen(str)
  5. _str要先开空间

👇👇👇

string(const char* str = "")
		:_size(strlen(str))
		,_capacity(_size)
		,_str(new char[_capacity+1])
	{
		memcpy(_str, str, _capacity+1);
	}

注意:

  1. 初始化的顺序是按照 声明的顺序来的 ⇒ 我们尽量保持 初始化和声明的顺序一致, 要不然就会出现问题
  2. 由于 _size 和 _capacity不包括 \0 的长度 ⇒ 我们_str开空间的时候要多开一个, 给 \0

🗨️为啥要用 memcpy函数? 为啥不用strcpy函数呢?

  • 1. memcpy函数 和 strcpy函数的 区别 : memcpy函数是 逐个字节进行copy的, 而strcpy是 遇到 \0就停止 copy
    2 我们标准库中 string类的输出是按照 _size 来的. 即遇到下面的情况, strcpy 和 strcpy的区别就体现出来了👇👇👇
	// 字符串如下:
	// hello 
	// world!
	// 来做以下几组实验

	// 1. 库里的string
	std::string str1 = "hello";
	str1 += "\0";
	str1 += "world!";
	cout << str1 << endl;

	// 2. 用strcpy来实现
	// ...
	//..

	// 3. 用memcpy来实现
	// ...
	// ...

*****
1. helloworld!
2. hello
3. helloworld!
*****
  1. memcpy默认是不会copy \0, 所以memcpy函数里面的长度 传的是 _capacity+1

析构函数

~string()
{
	delete[] _str; // 清理动态申请空间
	// 置零(置空)
	_str = nullptr;
	_size = _capacity = 0;
}

拷贝构造函数

  1. 我们不写构造函数, 系统自动生成的是一种 浅拷贝 ⇒ 对有动态资源申请的对象来说, 会对同一块空间析构两次
  2. 我们写的是 深拷贝 ⇒ 找一块新的空间给 this->_str, 然后将 s的内容 copy过去, 更新 _capacity 和 _size
String(const string& s)
{
	_str = new char[s._capacity + 1];
	memcpy(_str, s._str, s._capacity + 1);
	_capacity = s._capacity;
	_size = s._size;
}

空间

size()函数

size_t size()const
{
	return _size;
}

capacity()函数

size_t capacity()const
{
	return _capacity;
}

clear()函数

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

clear()函数 并不是用来清理空间的, 而是让空间置空(置零)

empty()函数

bool empty()const 
{
	if(_size == 0)
		return true;
	return false;
}

reverse()函数

void reverse(size_t n)
{
	assert(n >= 0);

	// 扩容逻辑 -- 一般我们不进行缩容
	if(n > _capacity)
	{
		char* tem = new char[n+1];
		memcpy(tem._str, _str, _capacity+1);
		delete[] _str;
		_str = tem;
		_capacity = n;
	}	
}

resize()函数

void resize(size_t n, char ch = '\0')
{
	assert(n >= 0);
	
	if(n <= _size)
	{
		_size = n;
		_str[_size] = '\0';
	}
	else
	{
		reverse(n);
		for(int i = _size; i < _size+n; i++)
		{
			_str[i] = ch;
		}
		
		_size = _size + n;
		_str[_size] = '\0';
	}
}

迭代器

迭代器是属于类的 ⇐ 我们声明迭代器的时候要声明类域
👇👇👇

std::string str = "hello world";
iterator it = str.begin();

*****
error C2955: “std::iterator”: 使用 类 模板 需要 模板 参数列表
*****

但要在 string类里面,定义一种类型, 有两种方式:

  1. typedef 一个变量
  2. 定义一个内部类 (内部类一般都是自定义类型)

而我们这里iterator其实就是数组_str各个下标对应的地址, 是一种 内置类型 ⇒ 所以, 我们采用typedef的方式来实现 iterator

iterator

typedef char* iterator;

begin()函数

iterator begin()
{
	return _str;
}

end()函数

iterator end()
{
	return _str + _size;
}

const_iterator

typedef const char* const_iterator;

begin()函数

const_iterator begin()const
{
	return _str;
}

end()函数

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

push_back()函数

尾插一个字符的操作:

  1. 是否需要扩容 ⇐ _size == _capacity

  2. 扩容逻辑:

    1. _capacity == 0 ⇒ 传个 4 过去扩容
    2. _capacity > 0 ⇒ 2倍扩容
  3. _size++, _str[_size] = ‘\0’;

void push_back(const char ch)
{
	// 是否扩容
	if (_size == _capacity)
	{
		size_t newcapacity = (_capacity == 0 ? 4 : _capacity * 2);
		reverse(newcapacity);
	}

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

}

append()函数

尾插一个字符串的操作:

  1. 是否需要扩容

  2. 扩容逻辑:
    1. _size + len <= _capacity — — 不需要扩容
    2. _size + len > _capacity — — 扩容(_size + len)

  3. _size = _size + len, _str[_size] = ‘\0’;

void append(const char* ch)
{
	size_t len = strlen(ch);

	// 是否扩容
	if (len + _size > _capacity)
	{
		reverse(len + _size);
	}

	for (size_t i = 0; i < len; i++)
	{
		_str[_size + i] = ch[i];
	}
	_size += len;
	_str[_size] = '\0';

}

operator+=

复用 push_back() 和 append()

void operator+=(const char ch)
{
	push_back(ch);

}

void operator+=(const char* ch)
{
	append(ch);

}

insert()函数

在 下标为pos的位置插入n个字符:

  1. 是否需要扩容

  2. 扩容逻辑:

    1. _size + n <= _capacity — — 不需要扩容
    2. _size + n > _capacity — — 扩容(_size + n)
  3. 挪动数据

  4. _size = _size + n, _str[_size] = ‘\0’;

void insert(size_t pos, const char* ch)
{
	assert(pos >= 0);

	// 是否需要扩容
	size_t len = strlen(ch);
	if (_size + len > _capacity)
	{
		reverse(_size + pos);

	}

	// 挪动数据
	size_t end = _size;
	// 挪动数据时, 下标不能小于0(即不能等于 -1)
	while (end >= pos && end != _nops)
	{
		_str[end + len] = _str[end];
		end--;
	}

	// 插入数据
	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = ch[i];
	}

	_size = _size + len;
}
  • 对了, 这里的 _nops是我么定义的一个静态成员变量
// 类里面的声明
public:
	static size_t _nops;

// 类外面的初始化
size_t muyu::string::_nops = -1; // 这里的muyu是我定义的一个命名空间域

🗨️为啥要定义一个nops? 为啥要初始化为 -1?

  • 前面, 我们有说过: 不可能为负数的, 我们定义成 size_t (无符号整数)
    如果 下标减到 -1 — ---- 由于是 size_t, 变量是不会比 -1 小的
    那么 size_t 类型如何区分开 -1 呢?
    size_t i = -1; ⇒ i 等于 2 ^ 32 -1;
    那么 下标 不等于 nops不就行了~~
    还有就是, 插入函数 和 删除函数中 字符串的长度如果不写, 就是nops

erase()函数

void erase(size_t pos, size_t n = _nops)
{
	assert(pos >= 0);

	// 是否把pos位置后面全部删除
	if (n == _nops || pos + n >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		for (size_t i = pos; i < pos + n; i++)
		{
			_str[i] = _str[i + n];
		}
		_size = _size - n;
	}
}

find()函数

size_t find(size_t pos = 0, const char ch )
{
	assert(pos < _size);

	for (int i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}

	return _nops;
}

size_t find(size_t pos = 0, const char* ch )
{
	assert(pos < _size);
	
	// 如果找到返回地址, 否则返回nullptr
	const char* res = strstr(_str, ch);

	if (res)
	{
		return res - _str;
	}
	else
	{
		return _nops;
	}

}

swap()函数

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

operator[]函数

//不是&, 那就返回的是常量临时拷贝
char& operator[](size_t n)
{
	assert(n <= _size);

	return _str[n];
}

const char& operator[](size_t n)const 
{
	assert(n <= _size);

	return _str[n];
}

operator= 函数

//string& operator=(const string& s)
//{
//	// 传统写法 -- 找一块空间, 把s的内容搞过去, 然后和*this交换
//	// 1. 找空间, 移内容;  2. 释放this的空间

//	string tem;
//	tem.reverse(s._capacity + 1);
//	memcpy(tem._str, s._str, s._capacity + 1);
//	tem._size = s._size;

//	swap(tem);

//	return *this;

//}

string& operator=(string s)
{
	swap(s);

	return *this;
}

比较

bool operator==(const string& s)
{
	// 如果_size都不相等, 那么何谈相等
	return _size == s._size &&
		memcmp(_str, s._str, _size) == 0;
}

bool operator>(const string& s)
{
	// 取较小长度进行比较
	size_t size = std::min(_size, s._size);
	int ret = memcmp(_str, s._str, size);
	
	// 由于是取较小长度进行比较, 那么会出现如下几种情况:
	// 1. str1 = hello, str2 = hello
	// 2. str1 = hello\0xxx, str2 = hello
	// 3. str1 = hello, str2 = hello\00xxx
	// 这几种情况都是根据较小长度比较的结果都是 相等
	if (ret == 0)
	{
		if (_size > s._size)
			return true;
		else
			return false;
	}

	return ret > 0;

}

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

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

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

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

流操作

流操作要写在全局位置 ⇐ cout/cin 要抢占第一个参数. 若要是在类中, 第一个参数就默认是this

流插入 <<

ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}

	return out;
}

流提取 >>

istream& operator>>(istream& in, string& s)
{
	// 每一次新的读取要进行清理一下
	// 要不然就会接着读取, 而不是覆盖
	s.clear();

	// get()函数可以读到每一个字符, 包括空格 和 换行
	char ch = in.get();
	// 处理前缓冲区前面的空格或者换行
	while (ch == ' ' || ch == '\n')
	{
		ch = in.get();
	}

	// in >> ch;
	char buff[128]; // buff数组的作用是: 减少开空间的次数
	int i = 0;

	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == 127)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}

		//in >> ch;
		ch = in.get();
	}
	
	// 如果最后 buff数组还有数据, 那么就加到s中
	if (i != 0)
	{
		buff[i] = '\0';
		s += buff;
	}

	return in;
}

C接口

c_str()函数

const char* c_str()const 
{
	return _str;
}

substr()函数

string substr(size_t pos = 0, size_t n = _nops)
{
	assert(pos >= 0);
	
	// 是否需要扩容
	int len = n; // 默认是n
	if (n == _nops || pos + n >= _size)
	{
		len = _size - pos;
	}

	string tem;
	tem.reverse(len);
	//for (size_t i = pos; i < len; i++)
	//{
	//	tem[i] = _str[i + pos];
	//}

	//tem._size = len;
	//tem[_size] = '\0';

	for (size_t i = pos; i < pos + len; i++)
	{
		tem += _str[i];
	}

	return tem;
}

源码

#pragma once

#include <string.h>
#include<assert.h>
#include<iostream>

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

		// friend ostream& operator<<(ostream& out, const string& s);


		iterator begin()
		{
			return _str;
		}

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

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

		string(const char* str = "")
			:_size(strlen(str))
			,_capacity(_size)
			,_str(new char[_capacity+1])
		{
			memcpy(_str, str, _capacity+1);
		}

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

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

		const char* c_str()const
		{
			return _str;
		}

		void reverse(size_t n)
		{
			if (n > _capacity)
			{
				char* tem = new char[n + 1];
				memcpy(tem, _str, _capacity + 1);
				_capacity = n;
				delete[] _str;
				_str = tem;
			}
		}

		void resize(size_t n, char ch = '\0')
		{
			if (_size > n)
			{
				_str[n] = '\0';
				_size = n;
			}
			else
			{
				reverse(n); // 不管需不需要扩容,都丢给reverse. reverse内部有判断是否需要扩容
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
				_str[n] = '\0';
			}
		}

		void push_back(const char ch)
		{
			// 是否扩容
			if (_size == _capacity)
			{
				size_t newcapacity = (_capacity == 0 ? 4 : _capacity * 2);
				reverse(newcapacity);
			}

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

		}

		void append(const char* ch)
		{
			size_t len = strlen(ch);

			// 是否扩容
			if (len + _size > _capacity)
			{
				reverse(len + _size);
			}

			for (size_t i = 0; i < len; i++)
			{
				_str[_size + i] = ch[i];
			}
			_size += len;
			_str[_size] = '\0';

		}

		void operator+=(const char ch)
		{
			push_back(ch);

		}

		void operator+=(const char* ch)
		{
			append(ch);

		}

		void insert(size_t pos, const char* ch)
		{
			assert(pos >= 0);

			// 是否需要扩容
			size_t len = strlen(ch);
			if (_size + len > _capacity)
			{
				reverse(_size + pos);

			}

			// 挪动数据
			size_t end = _size;
			// 挪动数据时, 下标不能小于0(即不能等于 -1)
			while (end >= pos && end != _nops)
			{
				_str[end + len] = _str[end];
				end--;
			}

			// 插入数据
			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = ch[i];
			}

			_size = _size + len;
		}

		void erase(size_t pos, size_t n = _nops)
		{
			assert(pos >= 0);

			if (n == _nops || pos + n >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				for (size_t i = pos; i < pos + n; i++)
				{
					_str[i] = _str[i + n];
				}
				_size = _size - n;
			}
		}

		size_t size()const
		{
			return _size;
		}

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

		bool empty()const
		{
			return _size > 0;
		}

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

		 //不是&, 那就返回的是常量临时拷贝
		char& operator[](size_t n)
		{
			assert(n <= _size);

			return _str[n];
		}

		const char& operator[](size_t n)const 
		{
			assert(n <= _size);

			return _str[n];
		}

		string substr(size_t pos = 0, size_t n = _nops)
		{
			assert(pos >= 0);

			int len = n; // 默认是n
			if (n == _nops || pos + n >= _size)
			{
				len = _size - pos;
			}

			string tem;
			tem.reverse(len);
			//for (size_t i = pos; i < len; i++)
			//{
			//	tem[i] = _str[i + pos];
			//}

			//tem._size = len;
			//tem[_size] = '\0';

			for (size_t i = pos; i < pos + len; i++)
			{
				tem += _str[i];
			}

			return tem;
		}

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

		bool operator>(const string& s)
		{
			// 取较小长度进行比较
			size_t size = std::min(_size, s._size);
			int ret = memcmp(_str, s._str, size);

			if (ret == 0)
			{
				if (_size > s._size)
					return true;
				else
					return false;
			}

			return ret > 0;

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

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

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

		size_t find(const char ch, size_t pos = 0)
		{
			assert(pos < _size);

			for (int i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}

			return _nops;
		}

		size_t find(const char* ch, size_t pos = 0)
		{
			assert(pos < _size);

			const char* res = strstr(_str, ch);

			if (res)
			{
				return res - _str;
			}
			else
			{
				return _nops;
			}

		}

		//string& operator=(const string& s)
		//{
		//	// 传统写法 -- 找一块空间, 把s的内容搞过去, 然后和*this交换
		//	// 1. 找空间, 移内容;  2. 释放this的空间

		//	//string tem;
		//	//tem.reverse(s._capacity + 1);
		//	//memcpy(tem._str, s._str, s._capacity + 1);
		//	//tem._size = s._size;

		//	//swap(tem);

		//	//return *this;

		//}

		string& operator=(string s)
		{
			swap(s);

			return *this;
		}


	private:
			size_t _size;
			size_t _capacity;
			char* _str;

	public:
			static size_t _nops;
	};


	size_t string::_nops = -1;

	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		// 每一次新的读取要进行清理一下
		// 要不然就会接着读取, 而不是覆盖
		s.clear();

		// get()函数可以读到每一个字符, 包括空格 和 换行
		char ch = in.get();
		// 处理前缓冲区前面的空格或者换行
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}

		// in >> ch;
		char buff[128]; // buff数组的作用是: 减少开空间的次数
		int i = 0;

		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}

			//in >> ch;
			ch = in.get();
		}

		// 如果最后 buff数组还有数据, 那么就加到s中
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}

}


持志如心痛. — — 王阳明
译:心在痛上,岂有工夫说闲话、管闲事.
文章来源地址https://www.toymoban.com/news/detail-727558.html

到了这里,关于[C++随笔录] string模拟实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++】模拟实现string

      目录 🌞专栏导读 🌛定义string类  🌛构造函数 🌛拷贝构造函数 🌛赋值函数 🌛析构函数  🌛[]操作符重载  🌛c_str、size、capacity函数  🌛比较运算符重载   🌛resize与reserve函数 🌛push_back、append函数  🌛insert函数  🌛erase函数 🌛find函数  🌛swap函数 🌛clean函数  🌛

    2024年02月14日
    浏览(43)
  • 【C++】string模拟实现

    个人主页🍖:在肯德基吃麻辣烫 本文带你进入string的模拟实现,对于string,是我们深入学习STL的必要途径。 我在模拟实现string时,成员变量如下: 1.1 无参构造(默认构造) 构造时不进行任何初始化,则默认为空字符串 比如: bit::sring s1; 1.2 普通构造 思路: 1.先新申请一块空

    2024年02月16日
    浏览(46)
  • string类的模拟实现

    上一篇博客我们对string类函数进行了讲解,今天我们就对string类进行模拟实现,以便于大家更加深入地了解string类函数的应用 由于C++的库里面本身就有一个string类,所以我们为了不让编译器混淆视听,我们可以首先将我们自己模拟实现的string类放入一个我们自己定义的命名空

    2024年01月21日
    浏览(34)
  • string的模拟实现

    2024年04月12日
    浏览(25)
  • string模拟实现:

    上一篇博客,我们对String类有了一个基本的认识,本篇博客我们来从0~1去模拟实现一个String类,当然我们实现的都是一些常用的接口。 ❓我们这里定义了一个string类型,然后STL标准库里面也有string,两个名字一样我们分不清楚怎么办呢? 为了跟库的string区分开,我们可以定

    2024年02月14日
    浏览(30)
  • string模拟实现

    1、构造函数,析构函数 2、遍历,size(),operator[],迭代器iterator 3、增删查改 push_back   append(串)   插入前需要检查容量,reserve()   再重载+= insert(挪动时,size_t pos小心死循环下标为0,可以引入npos多一步判断)    erase  find  substr     关系运算符重载 4、流插入、流提取

    2024年02月13日
    浏览(37)
  • 操作系统进程调度算法(c语言模拟实现)

            前言: 本文旨在分享如何使用c语言对操作系统中的部分进程调度算法进行模拟实现,以及算法描述的讲解, 完整代码放在文章末尾,欢迎大家自行拷贝调用 目录 常见的调度算法 数据结构 先来先服务调度算法 算法模拟思路: 算法模拟:  最短作业优先调度算法

    2024年02月06日
    浏览(52)
  • 【操作系统原理实验】银行家算法模拟实现

    选择一种高级语言如C/C++等,编写一个银行家算法的模拟实现程序。1) 设计相关数据结构;2) 实现系统资源状态查看、资源请求的输入等模块;3) 实现资源的预分配及确认或回滚程序;4) 实现系统状态安全检查程序;5) 组装各模块成一个完整的模拟系统。 (1)设计思想: 1、

    2024年02月01日
    浏览(43)
  • C++ 模拟实现string类

    目录 一、类的定义 二、初始化销毁 1、构造函数 2、辨析三种定义  3、析构函数 三、赋值  1、拷贝构造函数 2、赋值运算符 四、成员访问  operator[ ] 五、比较大小判断相等 六、容量操作  1、size() 2、reserve 3、push_back 4、append 5、加等运算符  6、C风格 7、insert 插入字符  插入

    2024年02月05日
    浏览(46)
  • 【C++】——string的模拟实现

    前言: 在之前的学习中,我们已经对string类进行了简单的介绍,大家只要能够正常使用即可。但是在面试中,面试官总喜欢让学生自己 来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。因此,接下来我将带领大家手动模拟实现一下

    2024年02月05日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包