【C++】string的模拟实现

请添加图片描述

前言

  前面我们已经学习了解了string重要接口的使用:【C++】string的使用
  对于C++,这是偏向底层的语言,所以我们来模拟实现一下。
  虽然string本质上是一个模版类,但是我们在使用时更多的只是使用basic_string< char>,因此我们模拟实现就简单实现一个string就好了。

总体结构

  由于string在标准库中已经定义好了,因此我们在模拟实现时使用命名空间进行隔离,防止命名冲突。

namespace bit//使用命名空间与标准库中的string进行隔离
{
	class string
	{
	private:
		char* _str;  //指向字符串的指针
		size_t _size;  //大小
		size_t _capacity;  //容量
		const static size_t npos ;//静态成员变量在类里面声明,在类外初始化
		//类里面的静态成员变量就相当于全局变量
	};
	//const size_t string::npos = -1;错误
	//error LNK1169: 找到一个或多个多重定义的符号
	//因为此时在string.cpp和test.cpp中都包含了,也就是它被定义了两次,一链接就会报错。
}

//string.cpp
const size_t string::npos = -1;
//此时应该在string.cpp中定义

但是对于const修饰的静态成员变量,对于整型可以直接设置初始值

private:
		const static size_t npos  = -1;

但是只有整型家族可以使用,其余的像double的则不行。

  我们在模拟实现时,同样是进行声明和定义分离。因此我们用三个文件:string.hstring.cpptest.cpp
  在实现各种函数前,同样我们先把能打印出来的函数给写了,即c_str,也比较简单,返回指针即可:

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

默认成员函数

构造函数

  对于构造函数,分为无参和带参的我们建议将两者合为一起,写成一个全缺省的。

//string.h文件
string(const char* str = " ");
//string.cpp文件
string::string(const char* str)
	:_size(strlen(str))
{
	_str = new char[_size + 1]; //多开一个给'\0'存放
	_capacity = _size;
	strcpy(_str, str);
}

  缺省值要放在函数声明的地方。
  我们用初始化列表用strlen给大小进行初始化,后面直接用大小初始化剩余的值。

析构函数

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

注意:要使用delete[]。

拷贝构造

  string类涉及到空间的申请和销毁,应当使用深拷贝来拷贝构造,否则原对象s1和拷贝后的对象s2都指向同一块空间,在析构时会崩溃。

//s2(s1),s1就是s,s2就是*this
string::string(const string& s)
{
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}

赋值重载

对于赋值重载也同样有空间的问题,也要进行深拷贝
在这里插入图片描述

string& string::operator=(const string& s)
{
	if (this != &s)//避免自己给自己赋值,减少消耗
	{
		char* p = new char[s._capacity + 1];//开一块新空间,多开一个给'\0'
		strcpy(p, s._str);//拷贝数据
		delete[] _str;//释放旧空间
		_str = p;//修改指针指向
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;//有返回值可以进行像a=b=c一样的连续赋值
}

string类对象的访问遍历操作

_size和operator[]

访问遍历数据最典型的就是通过下标+[]使用for循环进行访问。

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

来测试一下:

void TestString1()
{
	bit::string s1("hello world");

	for (int i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << " ";
	}
	cout << endl;
}

在这里插入图片描述

迭代器

  迭代器是STL的六大组件之一,它适用于所有容器,因此它的类型是不能确定的,但是前面我们说过,它是一个像指针一样的东西,因此我们可以在这里获取原生指针的方式作为迭代器。

//string.h
typedef char* iterator;//迭代器是一种像指针一样的东西,所以我们可以使用最简单的指针来尝试实现
typedef const char* const_iterator;
iterator begin();
iterator end();

//string.cpp
string::iterator string::begin()
//迭代器需要指定类域进行使用,所以iterator前面还加上了string::
{
	return _str;
}
string::iterator string::end()
{
	return _str + _size;
}

  我们知道,范围for的底层其实就是迭代器,当我们实现了迭代器,范围for也可以同时使用,可以测试实现一下:

bit::string s1("hello world");
string::iterator it1 = s1.begin();//迭代器遍历
while (it1 != s1.end())
{
	cout << *it1 << " ";
	it1++;
}
cout << endl;
for (auto e : s1)//范围for遍历
{
	cout << e << " ";
}
cout << endl;

在这里插入图片描述

注意
我们使用typedef char* iterator;是代码封装的一种体现
我把迭代器的真实类型进行typedef,因为迭代器真实的类型并不一定是指针,

string::iterator it1 = s1.begin();//迭代器遍历
while (it1 != s1.end())
{
	cout << *it1 << " ";
	it1++;
}

这样子写无论iterator是什么类型,都可以直接这样使用,这是提供了一种简单通用访问容器的方式,屏蔽了底层的实现细节。

同样还存在const迭代器,它指向的内容不可修改。

//string.h
typedef const char* const_iterator;
const_iterator begin() const;
const_iterator end() const;

//string.cpp
string::const_iterator string::begin()const
{
	return _str;
}
string::const_iterator string::end()const
{
	return _str + _size;
}

有const迭代器,则也存在const operator[]:

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

string类对象的修改操作

reserve

  我们想要插入字符或者字符串,往往需要修改空间大小,因此先来实现reserve扩容

void string::reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];//多开一个留个\0,虽然它不算在_size和_capacity中
		strcpy(tmp, _str);
		delete[] _str;

		_str = tmp;
		_capacity = n;
	}
}

注意:只有在当传入的容量n大于_capacity时才需要扩容,其余都不变。

push_back、append和operator+=

   push_back也就是尾插操作,我们在数据结构用的也比较多,也比较简单,但是在插入前还是要判断空间是否足够再进行插入。

void string::push_back(char ch)//尾插字符
{
   if (_size == _capacity)//判断空间是否足够
   {
   	size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
   	reserve(newcapacity);
   }
   _str[_size] = ch;
   _str[_size + 1] = '\0';
   _size++;
}

注意:要在末尾加上’\0’,因为加进去的字符占用的是’\0’的位置。


append是追加的意思,其实也就是尾插

void string::append(const char* str)//尾插字符串
{
	size_t len = strlen(str);//需要多少扩多少
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	//strcpy(_str, str);//它要找到前面字符串的'\0',然后在后面追加
	strcpy(_str + _size, str);//我知道'\0'的位置,直接拷贝连接过来即可
	_size = _size + len;
}

   其实push_back和append使用的并不是很多,因为使用operator+=尾插字符串/字符要方便快捷很多。

//都直接覆用即可
string& string::operator+=(char ch)
{
   push_back(ch);
   return *this;
}
string& string::operator+=(const char* str)
{
   append(str);
   return *this;
}

可以来测试实现一下:

bit::string s1("hello world");
s1.push_back('!');
cout << s1.c_str() << endl;

s1.append(" aaaaaaa");
cout << s1.c_str() << endl;

s1 += " bbbbbb";
cout << s1.c_str() << endl;

在这里插入图片描述
这样三个尾插都可以实现。

insert和erase

//string.h文件
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);//在pos位置删除长度为len的字符串,不写长度默认全删

insert是插入字符/字符串,挪动数据后插入即可。

void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(newcapacity);
	}
	size_t end = _size + 1;
	//要注意pos等于0时的越界问题
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = ch;
	_size++;
}

对于插入字符串,挪动数据结束循环的条件要搞清楚。

void string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t 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;
	}
	memcpy(_str + pos, str, len);
	_size += len;
}

在这里插入图片描述


erase在任意位置删除长度为len的字符串。
长度为len又有两种情况:

  1. len大于后面字符的个数时,有多少删多少
  2. 不大于时,就需要把后面pos+len处的字符串挪动到pos处
void string::erase(size_t pos, size_t len)
{
	assert(pos <= _size);
	if (len >= _size - pos)//len大于后面字符的个数时,有多少删多少
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}

删除并不是真删除,只要合理的调整 '\0' 位置和 _size 值,使其访问不到后续元素即可。

find

find是找到字符/小字符串在原字符串中的位置。
查找字符:
  传入字符,直接遍历字符串,查找在其中的位置,如果有则返回其位置,没有就返回npos。
  它是可以指定位置进行查找,若不指定,则从缺省值pos = 0处开始。

//string.h
size_t find(char ch, size_t pos = 0);

//string.cpp
size_t string::find(char ch, size_t pos)
{
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
			return i;
	}
	return npos;//找不到就返回整数的最大值
}

查找字符串:
  有一个算法叫KMP算法,匹配字符串查找算法,大家可以去了解了解。我们这里直接使用strstr足矣。

//string.h
size_t find(const char* str, size_t pos = 0);

//string.cpp
size_t string::find(const char* str, size_t pos)
{
	const char* p = strstr(_str + pos, str);//返回对应位置的指针
	if (p == nullptr)
		return npos;
	else
		return p - _str;
}

指针-指针即可找到下标位置。

swap

  swap,对于交换,其实可以使用库里面的swap即可,但是这样会进行深拷贝调用拷贝构造的次数比较多,代价比较大,但是可以将这三个变量分别调用库中的函数交换,可以减少拷贝的代价。

//s1.swap(s3);
void string::swap(string& s)
{
	std::swap(_str, s._str);//直接交换指针指向
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

substr

substr是从pos位置开始,截取n个字符并返回。

string string::substr(size_t pos, size_t len)
{
	if (len > _size - pos)//len大于后面剩余的字符,有多少取多少
	{
		string sub(_str + pos);
		return sub;
	}
	else
	{
		string sub;
		sub.reserve(len);
		for (size_t i = 0; i < len; i++)
		{
			sub += _str[pos + i];
		}
		return sub;
	}
}

大小比较

string的大小比较取决于ASCII码值,直接用strcmp即可。
我们只需要实现小于等于的判断,其余直接复用即可。

bool string::operator<(const string& s) const
{
	return strcmp(_str, s._str) < 0;
}
bool string::operator>(const string& s) const
{
	return !(*this < s);
}
bool string::operator<=(const string& s) const
{
	return *this < s || *this == s;
}
bool string::operator>=(const string& s) const
{
	return !(*this < s);
}
bool string::operator==(const string& s) const
{
	return strcmp(_str, s._str) == 0;
}
bool string::operator!=(const string& s) const
{
	return !(*this == s);
}

输入输出

流插入重载

使string类可以直接通过cout来输出。
直接遍历输出每个字符即可:

ostream& operator<<(ostream& os, const string& s)
{
	for (auto e : s)
	{
		os << e;
	}
	return os;
}

流提取重载

void string::clear()
{
	_str[0] = '\0';
	_size = 0;
}
istream& operator>>(istream& is, string& str)
{
	//char ch;//把一个一个字符提取出来
	//is >> ch;//cin拿不到空格,会自动忽略掉空格或换行
	//还要清空以前的字符串。
	str.clear();
	char ch = is.get();
	while (ch != ' ' && ch != '\n')//等于空格或换行就结束,空格或者换行被认为是多个值之间的分割
	{
		str += ch;//提取出来的字符连接到str上,
		ch = is.get();
	}
	
	return is;
}
  • 第一次ch = is.get(),用于从输入流 is 中读取第一个字符,并将其存储在变量 ch 中。
  • 第二次ch = is.get(),用于在每次循环迭代结束时从输入流中读取下一个字符。这个新读取的字符将用于下一次循环迭代的条件检查。

全部代码

string.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
using namespace std;

namespace bit
{
	class string
	{
	public:
		typedef char* iterator;//迭代器是一种像指针一样的东西,所以我们可以使用最简单的指针来尝试实现
		typedef const char* const_iterator;
		iterator begin();
		iterator end();

		const_iterator begin() const;
		const_iterator end() const;
		string(const char* str = " ");
		string(const string& s);
		string& operator=(const string& s);
		~string();

		const char* c_str() const;
		size_t size() const;
		char& operator[](size_t pos);
		const char& operator[](size_t pos) const;

		void reserve(size_t n);//扩容
		void push_back(char ch);
		void append(const char* str);

		string& operator+=(char ch);
		string& operator+=(const char* str);

		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		void erase(size_t pos, size_t len = npos);//在pos位置删除长度为len的字符串,不写长度默认全删

		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);

		void swap(string& s);
		string substr(size_t pos = 0, size_t len = npos);

		bool operator<(const string& s) const;
		bool operator>(const string& s) const;
		bool operator<=(const string& s) const;
		bool operator>=(const string& s) const;
		bool operator==(const string& s) const;
		bool operator!=(const string& s) const;

		void clear();

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
		const static size_t npos;//静态成员变量在类里面声明,在类外初始化
		//类里面的静态成员变量就相当于全局变量
	};

	//const size_t string::npos = -1; 错误
	istream& operator>>(istream& is, string& str);
	ostream& operator<<(ostream& os, const string& s);
}

string.cpp

#include"string.h"
namespace bit
{
	const size_t string::npos = -1;
	string::string(const char* str)
		:_size(strlen(str))
	{
		_str = new char[_size + 1]; //多开一个给'\0'存放
		_capacity = _size;
		strcpy(_str, str);
	}
	//s2(s1),s1就是s,s2就是*this
	string::string(const string& s)
	{
		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;

	}
	string& string::operator=(const string& s)
	{
		if (this != &s)//避免自己给自己赋值,减少消耗
		{
			char* p = new char[s._capacity + 1];//开一块新空间,多开一个给'\0'
			strcpy(p, s._str);//拷贝数据
			delete[] _str;//释放旧空间
			_str = p;//修改指针指向
			_size = s._size;
			_capacity = s._capacity;
		}
		return *this;//有返回值可以进行像a=b=c一样的连续赋值
	}
	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = _capacity = 0;
	}
	const char* string::c_str() const
	{
		return _str;
	}
	size_t string::size() const
	{
		return _size;
	}
	char& string::operator[](size_t pos)
	{
		assert(pos < _size);
		return _str[pos];
	}
	string::iterator string::begin()//迭代器需要指定类域进行使用,所以iterator前面还加上了string::
	{
		return _str;
	}
	string::iterator string::end()
	{
		return _str + _size;
	}
	string::const_iterator string::begin()const
	{
		return _str;
	}
	string::const_iterator string::end()const
	{
		return _str + _size;
	}
	const char& string::operator[](size_t pos) const
	{
		assert(pos < _size);
		return _str[pos];
	}
	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];//多开一个留个\0,虽然它不算在_size和_capacity中
			strcpy(tmp, _str);
			delete[] _str;

			_str = tmp;
			_capacity = n;
		}
	}
	void string::push_back(char ch)//尾插字符
	{
		if (_size == _capacity)
		{
			size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			reserve(newcapacity);
		}
		_str[_size] = ch;
		_str[_size + 1] = '\0';
		_size++;
	}
	void string::append(const char* str)//尾插字符串
	{
		size_t len = strlen(str);//需要多少扩多少
		if (_size + len > _capacity)
		{
			reserve(_size + len);
		}
		//strcpy(_str, str);//它要找到前面字符串的'\0',然后在后面追加
		strcpy(_str + _size, str);//我知道'\0'的位置,直接拷贝连接过来即可
		_size = _size + len;
	}
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}
	void string::insert(size_t pos, char ch)
	{
		assert(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++;
	}
	void string::insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		size_t 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;
		}
		memcpy(_str + pos, str, len);
		_size += len;
	}
	void string::erase(size_t pos, size_t len)
	{
		assert(pos <= _size);
		if (len >= _size - pos)//len大于后面字符的个数时,有多少删多少
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			strcpy(_str + pos, _str + pos + len);
			_size -= len;
		}
	}
	size_t string::find(char ch, size_t pos)
	{
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
				return i;
		}
		return npos;//找不到就返回整数的最大值
	}
	size_t string::find(const char* str, size_t pos)
	{
		const char* p = strstr(_str + pos, str);//返回对应位置的指针
		if (p == nullptr)
			return npos;
		else
			return p - _str;
	}
	//s1.swap(s3);
	void string::swap(string& s)
	{
		std::swap(_str, s._str);//直接交换指针指向
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}
	string string::substr(size_t pos, size_t len)
	{
		if (len > _size - pos)//len大于后面剩余的字符,有多少取多少
		{
			string sub(_str + pos);
			return sub;
		}
		else
		{
			string sub;
			sub.reserve(len);
			for (size_t i = 0; i < len; i++)
			{
				sub += _str[pos + i];
			}
			return sub;
		}
	}
	bool string::operator<(const string& s) const
	{
		return strcmp(_str, s._str) < 0;
	}
	bool string::operator>(const string& s) const
	{
		return !(*this < s);
	}
	bool string::operator<=(const string& s) const
	{
		return *this < s || *this == s;
	}
	bool string::operator>=(const string& s) const
	{
		return !(*this < s);
	}
	bool string::operator==(const string& s) const
	{
		return strcmp(_str, s._str) == 0;
	}
	bool string::operator!=(const string& s) const
	{
		return !(*this == s);
	}
	void string::clear()
	{
		_str[0] = '\0';
		_size = 0;
	}
	istream& operator>>(istream& is, string& str)
	{
		//char ch;//把一个一个字符提取出来
		//is >> ch;//cin拿不到空格,会自动忽略掉空格或换行
		//还要清空以前的字符串。
		str.clear();
		char ch = is.get();
		while (ch != ' ' && ch != '\n')//等于空格或换行就结束,空格或者换行被认为是多个值之间的分割
		{
			str += ch;//提取出来的字符连接到str上,
			ch = is.get();
		}
		
		return is;
	}
	ostream& operator<<(ostream& os, const string& s)
	{
		for (auto e : s)
		{
			os << e;
		}
		return os;
	}
}

test.cpp

#include"string.h"

namespace bit
{
	void TestString1()
	{
		bit::string s1("hello world");

		for (int i = 0; i < s1.size(); i++)
		{
			cout << s1[i] << " ";
		}
		cout << endl;
		string::iterator it1 = s1.begin();
		while (it1 != s1.end())
		{
			cout << *it1 << " ";
			it1++;
		}
		cout << endl;
		for (auto e : s1)
		{
			cout << e << " ";
		}
		cout << endl;

		const bit::string s2("aaaaaaaaa");
		string::const_iterator it2 = s2.begin();
		while (it2 != s2.end())
		{
			//*it2 = 'y';不能修改
			cout << *it2 << " ";
			it2++;
		}
		cout << endl;
		for (int i = 0; i < s2.size(); i++)
		{
			//s2[i]++; 不能修改
			cout << s2[i] << " ";
		}
		cout << endl;
	}

	void TestString2()
	{
		bit::string s1("hello world");
		/*s1.push_back('!');
		cout << s1.c_str() << endl;

		s1.append(" aaaaaaa");
		cout << s1.c_str() << endl;

		s1 += " bbbbbb";
		cout << s1.c_str() << endl;*/

		s1.insert(0, 'a');
		cout << s1.c_str() << endl;

		s1.insert(0, "bbbbbb");
		cout << s1.c_str() << endl;

		s1.erase(5, 3);
		cout << s1.c_str() << endl;

		s1.erase(3, 20);
		cout << s1.c_str() << endl;

	}
	void TestString3()
	{
		bit::string s1("hello world");
		cout << s1.find("hell") << endl;
		cout << s1.find("abc") << endl;
	}
	void TestString4()
	{
		bit::string s1("hello world");
		bit::string s2(s1);
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
	}
	void TestString5()
	{
		bit::string s1("hello world");
		bit::string s2("aaaaaaaaaaa");
		s1.swap(s2);
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
	}
	void TestString6()
	{
		bit::string s("https://legacy.cplusplus.com/reference/string/string/substr/");
		cout << "链接为:" << s.c_str() << endl;
		size_t pos1 = s.find(':');//找到冒号,并返回它所在的位置
		bit::string s1 = s.substr(0, pos1 - 0);//从下标0开始,想要截取pos1-0个长度的字符串
		//左闭右开区间下标相减就是长度
		cout << "协议为:" << s1.c_str() << endl;

		size_t pos2 = s.find('/', pos1 + 3);//从pos1+3的位置即字符c这个位置开始往后找'/'
		bit::string s2 = s.substr(pos1 + 3, pos2 - (pos1 + 3));
		cout << "域名为:" << s2.c_str() << endl;

		bit::string s3 = s.substr(pos2 + 1);//从pos2+1开始直到最后
		//只有一个参数,截取长度为缺省值npos,为最大整数,因此直接取到最后
		cout << "路径为:" << s3.c_str() << endl;
	}
	void TestString7()
	{
		bit::string s1("hello");
		bit::string s2("abcd");
		cout << (s1 < s2) << endl;
	}
	void TestString8()
	{
		bit::string s1("hello");
		cout << s1 << endl;


		cin >> s1;
		cout << s1 << endl;
	}
}


int main()
{
	//bit::TestString1();
	//bit::TestString2();
	//bit::TestString3();
	//bit::TestString4();
	//bit::TestString5();
	//bit::TestString6();
	bit::TestString8();
	
	return 0;
}

感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。
请添加图片描述

  • 42
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 34
    评论
评论 34
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值