string的简单模拟即相关问题

分布解析 :

string的构造,析构

namespace zephyr
{
	class string
	{
	public:
		/*string()
			:str(new char[1]{'\0'})
			,size(0)
			,capacity(0)
		{}*/


		//string(const char* s=nullptr)  错误示范
		//string(const char* s="\0")     错误示范
		string(const char* s="")
			:_size(strlen(s))
			//:str(new char[strlen(s) + 1] )
			//,size(strlen(s))  //多次调用了strlen,牺牲了效率
			//,capacity(strlen(s))
		{
			if (s == nullptr)  //构造类对象时,如果传递nullptr,可认为程序非法
			{
				assert(false);
				return;
			}
			_capacity=_size;
			_str = new char[_size + 1];
			//strcpy(_str, s);  //遇到\0会终止
			memcpy(_str, s, _size + 1);
		}
        template<class InputIterator>
        string(InputIterator first, InputIterator last)  //使用迭代区间,防止中途遇到\0而终止
        {
	        while (first != last)
	        {
	        	push_back(*first);
	        	first++;
	        }
        }

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

		~string()
		{
			delete[]str;
			str = nullptr;
			size = 0;
			capacity = 0;
		}
	private:
		char* str;
		//计算时都不包含结尾的'\0'
		size_t size;
		size_t capacity;
	};
}
int main()
{
	zephyr::string s;
	cout << s.c_str() << endl;
}

拷贝构造 :

//传统写法:
/*string(const string& s)
{
	_str = new char[s._capacity + 1];
	memcpy(_str, s._str, s._size+1);
	_size = s._size;
	_capacity = s._capacity;
}*/
//现代写法:
string(const string& s)
{
	//错误写法:string tmp(s._str);  //中间有\0会导致提前终止
	string tmp(s.begin(), s.end());//reserve中一定要判断原字符串是否为空
	swap(tmp);
}
 构造函数和拷贝构造函数的区别:

 增操作(push_back、append、insert、operator+=)

void push_back(char ch)
{
	if (_size == _capacity)
	{
		reserve(_capacity==0?4:2 * _capacity);
	}
	_str[_size++] = ch;
	_str[_size] = '\0';
}
void append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(max(_size + len, 2 * _capacity));
	}
	memcpy(_str + _size, str, len+1);//相当于加完字符串后,把最后的'\0'也加上了
	_size += len;
	//_str[_size] = '\0';  前面是memcpy(_str + _size, str, len)时才要加这个'\0'
}

注意:记得判断是否后面要手动加\0

void insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	size_t end = _size;//指向'\0' 即使写成int下面比较的时候会被 提升
	while (end >= pos)//size_t 类型 无负数故而会导致头插有问题
	{
		_str[end + 1] = _str[end];
		if (end == 0)break;    //直接使用memmove便不用考虑该问题
		end--;
	}
	_str[pos] = ch;
	_size++;
	
}
void insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	int len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(max(_size + len, 2 * _capacity));
	}
	memmove(_str + pos + len, _str + pos, _size + 1 - pos);
	_size += len;
	for (int i = 0; i < len; i++)
		_str[pos + i] = str[i];

}

注意:insert 头插时判断是否会因为比较大小而出现问题(size_t类型无负数,会导致0下一个仍为正数而出现死循环等)eg:if (end == 0)break;    就是解决方法之一

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

 直接使用push_back和append来实现

 扩容操作:(reserve)

void reserve(size_t n)
	//只做扩容处理
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];
		
		//检查是否旧空间为空
		if(_str)
		{
			//错误示范:strcpy拷贝数据的话遇到'\0'就停止了,但字符串中可能中间包含'\0'
			memcpy(tmp, _str, _size + 1);//_size个字符 外加 最后的'\0'
			delete[]_str;
		}

		_str = tmp;
		_capacity = n;
	}
}

注意:拷贝时使用memcpy 

一定要检查旧的空间是否为空,防止程序崩溃

迭代器的实现:

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

 删操作(erase)

void erase(size_t pos, size_t len = npos)//从pos开始往后去删
{
	assert(pos < _size);
	if (len == npos || pos + len > _size)//pos后全删
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else//删部分
	{
		memmove(_str + pos, _str + pos + len, _size + 1 - (pos + len));//将末尾的\0也一起移
	}
}

注意分类讨论

查找操作(find):

		size_t find(char ch, size_t pos=0)
		{
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == pos)return i;
			}
		}
		size_t find(const char* sub, size_t pos=0)
		{
			const char* p = strstr(_str, sub);//返回_str中第一次出现sub的位置的指针
			if (p == nullptr)
			{
				return npos;
			}
			else 
			{
				return p - _str;//求下标
			}
		}

 获取子串(substr):

string substr(size_t pos, size_t len=npos)
{
	assert(pos < _size);
	string ret;
	//判断是否需要更新len
	if (pos + len > _size || len == npos)//或者直接写成len>_size-pos
	{
		len = _size - pos;
	}

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

比较大小 :

最初比较时使用的是strcmp()--按字典序比较

 注意:但是其遇到\0就会停止

 memcmp()需要第三个参数(比较多少个字节),也并不能一步到位

bool operator<(const string& s)const
{
	//错误案例:return strcmp(_str,s._str)<0; //有坑 遇到\0就停止了
	//memcmp 需要第三个参数size

	//此比较方法为:先比长度,再进行字典序比较
	if (_size < s._size)return true;
	else if (_size == s._size)
	{
		for (int i = 0; i < _size; i++)
		{
			if (_str[i] < s._str[i])return true;
			else if (_str[i] > s._str[i])return false;
		}
	}
	else return false;
}
bool operator<=(const string& s)const
{
	return (*this < s || *this == s);
}
bool operator>(const string& s)const
{
	return !(*this < s || *this == s);
}
bool operator>=(const string& s)const
{
	return !(*this < s);
}
bool operator==(const string& s)const
{
	if (_size == s._size)
	{
		for (int i = 0; i < _size; i++)
		{
			if (_str[i] != s._str[i])return false;
		}
		return true;
	}
	return false;
}
bool operator!=(const string& s)const
{
	return !(*this == s);
}

此处写得比较大小与库(字典序)的无关 

 operator=

string& operator=(const string& s)
{
	if(this!=&s)//this和s的地址进行比较
	{
        //1.
		char* tmp = new char[s._capacity + 1];
		memcpy(tmp, s._str, s._size + 1);//s的_size
		delete[] _str;
		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;

		//另一种写法:2.
		//string tmp(s);
		//swap();交换后tmp的空间会自行析构带走
	}
    return *this;
}
//更为简洁的写法:3.(但无法判断s1=s1这种自己给自己赋值)不过这本就是非正常场景
string& operator=(string tmp)//传形参,调用拷贝构造
{
	swap(tmp);
	return *this;
}
int main()
{
	zephyr::string s1("hello world");
	zephyr::string s2 = s1.substr(6);//初始化时调用的是拷贝构造函数
	cout << s2.c_str() << endl;
	zephyr::string s3(s2);//不写拷贝构造函数就是浅拷贝
	cout << s3.c_str() << endl;
	s2 = s1;//没写operator=的话浅拷贝,指向同一块地址,释放两次而程序崩溃
	cout << s2.c_str() << endl;
}

上述我认为2.最简介合适 

注意:

1、上述代码s2 = s1;(两个已经存在的变量)调用的是operator=,如若为自行定义(默认浅拷贝)导致后续指向同一块空间,最后被释放两次而导致程序崩溃

2、一定要做地址检查防止(s1=s1)时造的成错误和消耗。

流插入和流提取:(不需要写为友元函数)

std::ostream& operator<<(std:: ostream& os, const zephyr::string& s)  //记得写zephyr::域
{
	for (auto x : s)
	{
		os << x;
	}
	return os;
}
std::istream& operator>>(istream& is, zephyr::string& s)
{
	s.clear();//清空之前的数据
	char tmp[256];//解决输入长字符串、短字符串的折中方法,减少reserev开空间次数造成的消耗
	size_t i = 0;

	char ch;
	//错误示范:is >> ch; //cin输入 空格换行 被认为是 多项值之间的分割 故而不会读入

	ch=is.get();
	while (ch != ' ' && ch != '\n')
	{
		//s += ch;//不会停止
		tmp[i++] = ch;
		if (i == 255)
		{
			tmp[255] = '\0';
			s += tmp;

			i = 0;//重置为零
		}
		//is >> ch;//不会读入空格换行 故而不会停止
		ch = is.get();

	}
	if (i > 0)
	{
		tmp[i] = '\0';
		s += tmp;
	}

	return is;//返回输入流是为了支持连续的输入
}

注意:

1.获取字符时不能写 is >> ch; (原因:cin输入 空格换行 被认为是 多项值之间的分割 故而不会读入空格和换行,故而不会停止),故而采取get函数

2.为解决输入长字符串时reserve次数过多而造成的空间消耗,可以先reserve一个较大的空间,但这样如若遇到的是短字符串会造成空间浪费(reserve后的容量不可能缩小)。

故而采取在栈区上开一个tmp数组(根据实际自行安排大小)先进行存储,达到上限\输入结束后 再进行s+=来减少reserve次数

 c_str:(与cou<<还是有区别的)

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

 

如图所示:c_str遇到'\0'便会停止 

交换函数swap():效率陷阱

1.模拟实现算法库的swap模板:(消耗过大)
template <class T>
void swap(T& a, T& b)
{
	T c(a);
	a = b;
	b = c;
}
int main()
{
	zephyr::string s1("hello world");
	zephyr::string s2("zephyr");
	swap(s1, s2);
	std::cout << s1 << std::endl;
	std::cout << s2 << std::endl;
	return 0;
}
 算法库的交换会多次深拷贝,如上面写的swap模板,多次开空间释放空间和拷贝

 2.直接交换指针指向位置,即个数和容量(有效减少消耗)
void swap(string& s)//这是在类中定义的
{
	//算法库的交换会多次深拷贝,如上面写的swap模板,多次开空间释放空间和拷贝
	//直接交换指针指向位置,即个数和容量
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

void swap(string& s1,string& s2)//类外定义
{
    s1.swap(s2);
}
int main()
{
	zephyr::string s1("hello world");
	zephyr::string s2("zephyr");
	s1.swap(s2);//类外定义后swap(s1,s2);的实质便是前面的
	std::cout << s1 << std::endl;
	std::cout << s2 << std::endl;
	return 0;
}

 这也是为何许多类中有单独自己的swap的原因

下述为完整代码 :

template <class T>
void swap(T& a, T& b)
{
	T c(a);
	a = b;
	b = c;
}

namespace zephyr
{
	// 错误案例:const size_t string:: npos = -1; 此时string还未声明//类里面声明,类外面定义
	class string
	{
	public:
		static const size_t npos;//特殊处理(因为是const静态,且得是整型)
		//static const double x = 1.1;   错误案例 const后给值仅针对整型,其他类型会报错
		//static size_t x = 1;   错误案例 报错是因为给缺省值用于初始化列表,但静态成员变量并不走初始化列表

		typedef char* iterator;//此处只是把迭代器用指针实现,并非迭代器一定是指针
		typedef const char* const_iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size; 
		}
		const_iterator begin()const
		{
			return _str;
		}
		const_iterator end()const
		{
			return _str + _size;
		}
		/*string()
			:str(new char[1]{'\0'})
			,size(0)
			,capacity(0)
		{}*/


		//string(const char* s=nullptr)  错误示范
		//string(const char* s="\0")     错误示范
		string(const char* s="")
			:_size(strlen(s))
			//:str(new char[strlen(s) + 1] )
			//,size(strlen(s))  //多次调用了strlen,牺牲了效率
			//,capacity(strlen(s))
		{
			if (s == nullptr)  //构造类对象时,如果传递nullptr,可认为程序非法
			{
				assert(false);
				return;
			}
			_capacity=_size;
			_str = new char[_size + 1];
			//strcpy(_str, s);
			memcpy(_str, s, _size + 1);
		}
		template<class InputIterator>
		string(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}
		//传统写法:
		/*string(const string& s)
		{
			_str = new char[s._capacity + 1];
			memcpy(_str, s._str, s._size+1);
			_size = s._size;
			_capacity = s._capacity;
		}*/
		//现代写法:
		string(const string& s)
		{
			//错误写法:string tmp(s._str);  //中间有\0会导致提前终止
			string tmp(s.begin(), s.end());
			swap(tmp);
		}
		const char* c_str()const
		{
			return _str;
		}
		~string()
		{
			if(_str)
			{
				delete[]_str;
				_str = nullptr;
				_size = 0;
				_capacity = 0;
			}
		}

		size_t size()const
		{
			return _size;
		}
		size_t capacity()const
		{
			return _capacity;
		}
		char& operator[](size_t i)
		{
			assert(i < _size);
			return _str[i];
		}
		const char& operator[](size_t i)const
		{
			assert(i < _size);
			return _str[i];
		}

		void reserve(size_t n)
			//只做扩容处理
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				
				//检查是否旧空间为空
				if(_str)
				{
					//错误示范:strcpy拷贝数据的话遇到'\0'就停止了,但字符串中可能中间包含'\0'
					memcpy(tmp, _str, _size + 1);//_size个字符 外加 最后的'\0'
					delete[]_str;
				}

				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				reserve(_capacity==0?4:2 * _capacity);
			}
			_str[_size++] = ch;
			_str[_size] = '\0';
		}
		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(std::max(_size + len, 2 * _capacity));
			}
			memcpy(_str + _size, str, len+1);//相当于加完字符串后,把最后的'\0'也加上了
			_size += len;
			//_str[_size] = '\0';  前面是memcpy(_str + _size, str, len)时才要加这个'\0'
		}
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
		string& operator=(const string& s)
		{
			if(this!=&s)//this和s的地址进行比较
			{
				char* tmp = new char[s._capacity + 1];
				memcpy(tmp, s._str, s._size + 1);//s的_size
				delete[] _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;

				//另一种写法:
				//string tmp(s);
				//swap();交换后tmp的空间会自行析构带走
				
			}
			return *this;
		}
		//更为简洁的写法:
		string& operator=(string tmp)//传形参,调用拷贝构造
		{
			swap(tmp);
			return *this;
		}
		void insert(size_t pos, char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			size_t end = _size;//指向'\0' 即使写成int下面比较的时候会被 提升
			while (end >= pos)//size_t 类型 无负数故而会导致头插有问题
			{
				_str[end + 1] = _str[end];
				if (end == 0)break;    //直接使用memmove便不用考虑该问题
				end--;
			}
			_str[pos] = ch;
			_size++;
			
		}
		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			int len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(std::max(_size + len, 2 * _capacity));
			}
			memmove(_str + pos + len, _str + pos, _size + 1 - pos);
			_size += len;
			for (int i = 0; i < len; i++)
				_str[pos + i] = str[i];

		}
		void erase(size_t pos, size_t len = npos)//从pos开始往后去删
		{
			assert(pos < _size);
			if (len == npos || pos + len > _size)//pos后全删
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else//删部分
			{
				memmove(_str + pos, _str + pos + len, _size + 1 - (pos + len));//将末尾的\0也一起移
			}
		}

		size_t find(char ch, size_t pos=0)const
		{
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == pos)return i;
			}
		}
		size_t find(const char* sub, size_t pos=0)const
		{
			const char* p = strstr(_str, sub);//返回_str中第一次出现sub的位置的指针
			if (p == nullptr)
			{
				return npos;
			}
			else 
			{
				return p - _str;//求下标
			}
		}
		string substr(size_t pos, size_t len=npos)const
		{
			assert(pos < _size);
			string ret;
			//判断是否需要更新len
			if (pos + len > _size || len == npos)//或者直接写成len>_size-pos
			{
				len = _size - pos;
			}

			for (size_t i = 0; i < len; i++)
			{
				ret += _str[pos + i];
			}
			return ret;
		}
		
		bool operator<(const string& s)const
		{
			//错误案例:return strcmp(_str,s._str)<0; //有坑 遇到\0就停止了
			//memcmp 需要第三个参数size

			//此比较方法为:先比长度,再进行字典序比较
			if (_size < s._size)return true;
			else if (_size == s._size)
			{
				for (int i = 0; i < _size; i++)
				{
					if (_str[i] < s._str[i])return true;
					else if (_str[i] > s._str[i])return false;
				}
			}
			else return false;
		}
		bool operator<=(const string& s)const
		{
			return (*this < s || *this == s);
		}
		bool operator>(const string& s)const
		{
			return !(*this < s || *this == s);
		}
		bool operator>=(const string& s)const
		{
			return !(*this < s);
		}
		bool operator==(const string& s)const
		{
			if (_size == s._size)
			{
				for (int i = 0; i < _size; i++)
				{
					if (_str[i] != s._str[i])return false;
				}
				return true;
			}
			return false;
		}
		bool operator!=(const string& s)const
		{
			return !(*this == s);
		}
		//friend  ostream& operator>>(ostream& os, const string& str);已经给了适配接口无需使用友元访问

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}
		void swap(string& s)
		{
			//算法库的交换会多次深拷贝,如上面写的swap模板,多次开空间释放空间和拷贝
			//直接交换指针指向位置,即个数和容量
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
	private:
		//char _buf[16];
		char* _str=nullptr;
		//计算时都不包含结尾的'\0'
		size_t _size=0;
		size_t _capacity=0;
	};
	const size_t string::npos = -1;//类里面声明,类外面定义
}
std::ostream& operator<<(std:: ostream& os, const zephyr::string& s)  //记得写zephyr::域
{
	for (auto x : s)
	{
		os << x;
	}
	return os;
}
std::istream& operator>>(std::istream& is, zephyr::string& s)
{
	s.clear();//清空之前的数据
	char tmp[256];//解决输入长字符串、短字符串的折中方法,减少reserev开空间次数造成的消耗
	size_t i = 0;

	char ch;
	//错误示范:is >> ch; //cin输入 空格换行 被认为是 多项值之间的分割 故而不会读入

	ch=is.get();
	while (ch != ' ' && ch != '\n')
	{
		//s += ch;//不会停止
		tmp[i++] = ch;
		if (i == 255)
		{
			tmp[255] = '\0';
			s += tmp;

			i = 0;//重置为零
		}
		//is >> ch;//不会读入空格换行 故而不会停止
		ch = is.get();

	}
	if (i > 0)
	{
		tmp[i] = '\0';
		s += tmp;
	}

	return is;//返回输入流是为了支持连续的输入
}


//int main()
//{
//	/*zephyr::string s;
//	cout << s.c_str() << endl;
//	zephyr::string s2("hello world");
//	cout << s2.c_str() << endl;*/
//
//	/*for (int i = 0; i < s2.size(); i++)
//	{
//		
//		cout << s2[i] << " ";
//	}*/
//
//	/*zephyr::string s3;
//	zephyr::string s4;
//
//	for (int i = 1; i <= 7; i++)
//	{
//		s3.push_back('x');
//		s4.append("ab");
//	}
//	cout << s3.c_str() << endl;
//	cout << s4.c_str() << endl;
//
//	for (int i = 1; i <= 10; i++)
//	{
//		s3+=('x');
//		s4+=("ab");
//	}
//	cout << s3.c_str() << endl;
//	cout << s4.c_str() << endl;*/
//
//	/*zephyr::string s5("hello world");
//	s5.erase(6);
//	cout << s5.c_str() << endl;
//
//	zephyr::string s6("hello world");
//	s6.erase(6,3);
//	cout << s6.c_str() << endl;
//
//	zephyr::string s7("hello world");
//	s7.insert(0, "my ");
//	cout << s7.c_str() << endl;*/
//
//	return 0;
//}

//int main()
//{
//	zephyr::string s1("hello world");
//	zephyr::string s2 = s1.substr(6);
//	cout << s2.c_str() << endl;
//	zephyr::string s3(s2);//不写拷贝构造函数就是浅拷贝
//	cout << s3.c_str() << endl;
//	s2 = s1;//没写operator=的话浅拷贝,指向同一块地址,释放两次而程序崩溃
//	cout << s2.c_str() << endl;
//}

//int main()
//{
//	zephyr::string s1("hello");
//	zephyr::string s2("world");
//	cout << (s1 >= s2) << endl;
//	return 0;
//}

//int main()
//{
//	zephyr::string s("hello world");
//	//cout << s << endl;
//
//	s += '\0';
//	s += "xxxxxxxxx";
//	cout << s.c_str() << endl;
//	cout << s << endl;
//
//}

//int main()
//{
//	zephyr::string s1,s2;
//	cin >> s1 >> s2;
//
//	cout << s1 << endl;
//	cout << s1.size() << endl;
//	cout << s1.capacity() << endl;
//
//	cout << s2 << endl;
//	cout << s2.size() << endl;
//	cout << s2.capacity() << endl;
//}

int main()
{
	zephyr::string s1("hello world");
	zephyr::string s2("zephyr");
	s1.swap(s2);
	std::cout << s1 << std::endl;
	std::cout << s2 << std::endl;
	return 0;
}

 

其余问题:

1.为何要typedef const char* const_iterator;而不直接使用const iterator呢?有何区别?

 

 也就是说const iterator相当于char* const指针常量,相当于const修饰的是iterator(char*)类型别名本身,而不是其指向的char内容。

例如:const iterator str;即str不能变,*str可变。

而const_str 才是真正的常量指针,指向内容不可变。

区分:指针常量(指针是常量)  常量指针(指向常量的指针)

2.计算string对象的大小,为何64位平台下不是24字节?

 可以看到除了_ptr外还有一个存储小范围字符串的_buf(如下图所示),这是一种编译器的优化用于提高效率

 3.扩展:g++早期使用的写时拷贝:(先浅拷贝+引用计数+适当时深拷贝)

浅拷贝的问题:

1.析构多次程序崩溃(利用引用计数来解决)

2.一个修改影响另一个(修改时深拷贝)

写时拷贝:设计优势:拷贝后不一定修改对象

写时拷贝在读取是缺陷

面试中string的一种正确写法

STL的string类怎么了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值