深入篇【C++】string类的常用接口介绍:标准库中的string类 【万字总结】

Ⅰ.string类介绍

string类文档介绍----cplusplus
在这里插入图片描述

1.C语言中,strxxx 是系列库函数而在C++中string是一个类。
2.string是管理字符数组的类
3.标准的字符串类提供了对此类对象的字符类型,其接口类似于标准字符容器的接口,但添加了专门操作单字节字符字符串的设计特性。
4.string类其实是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数。
5.这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字节的序列,这个类的所有成员以及它的迭代器,将仍然按照字节来操作,而不是实际编码的字符。

总结:
1.string其实表示字符串的字符串类
2.string在底层其实是basic_string模板的实例化。

typedef basic_string<char, char_traits, allocator> string;

3.在使用string类时必须要包含头文件和using namespace std;

Ⅱ.string类的常用接口

①.string类对象的常用构造

在这里插入图片描述

1.string()

无参构造函数,不需要参数就可以构造出一个对象,该对象其实就是一个空字符串。
所以可以用空字符串来构造string对象。

int main()
{
	//无参构造函数
	string s1;
	cout << s1 << endl;
}

在这里插入图片描述

2.string(const char*ch)

该构造函数的参数是一个字符串。这说明我们可以用一个字符串来构造string对象。

int main()
{
	string s2("小陶");//用字符串来构造对象
	string s3("hello world");
	cout << s2 << endl;
	cout << s3 << endl;
}

在这里插入图片描述

3.string(const string& str)

该构造函数的参数是string类对象,这说明我们可以用一个string对象来构造string对象。

int main()
{
	string s2("小陶");
	string s3("hello world");
	string s4(s2);
	cout << s4 << endl;
	string s5(s3);
	cout << s5 << endl;
}

在这里插入图片描述

4.string(size_t n,char c)

该构造函数的参数有两个,一个是正数n一个是字符c。
该构造函数实现的功能是用n个字符c构造对象。这说明我们可以用n个字符c来构造string对象。

int main()
{
	
	string s4(10, '*');
	//用10个'*'来构造对象s4
	cout << s4 << endl;

	string s5(10, 'x');
	//用10个'x'来构造对象s4
	cout << s5 << endl;
}

在这里插入图片描述

5.string(const string& str,size_t pos,size_t len=npos)

从参数上我们就可以推断出该构造函数的功能是什么了。
从字符串str第pos位置上拷贝长度为len的字符串用来构造对象
注意最后一个参数给了缺省值npos,npos是一个很大的数值,当我们给定长度时,就从pos位置截取len长度,当我们不给定长度时,就默认从pos位置一直往后截取完。

int main()
{
	string s3("hello world");//字符串
	
    string s6(s3, 6, 5);///从某个字符串某个位置拷贝n各字符
	//从字符串s3第六个位置往后拷贝五个字符
	cout << s6 << endl;

	//当给定长度为5时,就截取5个字符,当不给定长度时,从pos位置一直往后截取
	//因为缺省参数是一个很大的数值。
	
	string s7(s3, 6);
	cout << s6 << endl;
	//
}

在这里插入图片描述

【总结】
常见构造函数功能
string()用空来构造string对象,即空字符串
string(const char* ch)用C-string来构造string对象
string(const string& str)用string类对象来构造string对象,即拷贝构造函数
string (size_t n,char c)对象中包含n个字符c,即用n个字符c来构造对象
string(const stirng&str,size_t pos,size_t len=npos)从对象str的pos位置上截取长度为len的字符串来构造

②.string类对象的容量操作

在这里插入图片描述

1.size

用来计算对象的有效长度。它是string类的成员函数,所以调用它只需要对象即可。
C++中其实还有一个成员函数length也是用来计算对象长度的,与size功能是完全一样,只是名字不一样。
类似的成员函数还有max_size,这是用来计算对象可以拥有的最大长度。
在这里插入图片描述


int main()
{
	string s1("hello xiaotao");
	cout << s1.size() << endl;
	cout << s1.length() << endl;
	cout << s1.max_size() << endl;
}

在这里插入图片描述

2.capacity

用来计算对象的容量大小,它是string类的成员函数,对象可以直接调用。

int main()
{
	string s1("hello xiaotao");
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

	string s2;
	cout << s2.size() << endl;
	cout << s2.capacity() << endl;

}

在这里插入图片描述

我们可以发现对象s1和对象s2的容量都为15,这说明对象的起始容量就为15。
当一个对象被创建时,系统就会给它申请15大小的空间。而当我们的对象的长度大于对象本身容量的大小时,就需要对象扩容。
那string类对象的扩容方式是如何的呢?每次对象扩容多少呢?

int main()
{
	string s1;
	cout <<"大小:"<< s1.size() << endl;

	cout <<"起始容量:"<< s1.capacity() << endl;

	size_t old = s1.capacity();
	for (size_t i = 0; i < 100; i++)
	{
		s1 += 'x';
		if (old != s1.capacity())
		{
			cout << "扩容" << s1.capacity() << endl;
			old = s1.capacity();
		}
	}
}

在这里插入图片描述
我们可以看到在VS下string对象的每次扩容申请的空间都是不一样的,比较奇怪。
第一次扩容16大小,第二次扩容26大小,第三次扩容23大小,第4次扩容35大小。

3.clear

用来清空对象的有效字符
要注意clear只是清空了有效字符的大小(size)变成0,而容量空间大小(capacity)是不变的。

int main()
{
	string s1("hello xiaotao");
    cout << s1.size() << endl;
    cout << s1.capacity() << endl;
     s1.clear();
     //清理数据,但内存空间还在
     cout << s1.size() << endl;
     cout << s1.capacity() << endl;
}

在这里插入图片描述

4.empty

用来检查字符串是否被释放成空串,如果是空串则返回true、否则返回false。

int main()
{
	string s1("hello xiaotao");
    
	cout << s1.empty() << endl;
     s1.clear();
     //清理数据,但内存空间还在
     
	 cout << s1.empty() << endl;
}

在这里插入图片描述

5.reserve

用来为对象预留空间。
可以提前为对象申请n大小的空间。
当n比原空间(capacity)大时,就会进行扩容。
当n比原空间(capacity)小时,不一定会缩容,这取决于不同的平台。

而reserve的好处就是当我们知道需要多少空间时,就可以提前将空间开好,这样就可以避免不断扩容,就不存在扩容问题了。

int main()
{
	string s1("hello world");
	//开空间
	s1.reserve(100);
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
}

在这里插入图片描述
用reserve提前开辟的空间不一定准确,可能会开大点,但不可能开小的。就比如reserve(100)提前申请100个空间,系统分配了111个空间给它。

void TestPushBackReserve()
{
	string s;
	s.reserve(100);//提前开辟100大小空间
	size_t sz = s.capacity();
	cout << "now capacity:" << sz << '\n';

	cout << "making a grow:'\n";
	for (int i = 0; i < 100; i++)
	{
		s.push_back('c');
		if (sz != s.capacity())
		cout << "capacity changed:" << sz << '\n';
	}
	
	cout << "later capacity:" << sz << '\n';
	s.reserve(10);
	cout << s.capacity() << endl;
}

在这里插入图片描述
可以通过上面的代码知道reserve确实为对象开辟的空间,并且当开辟空间小于原空间大小时,原空间大小是不变的。

6.resize

在这里插入图片描述

将对象原来的有效字符的个数改成n个,多出来的空间用字符c来填充。
该成员函数有两种重载形式,一种是resize(size_t n)一种是resize(size_t n,char c);
这两个区别在于多出来的空间,resize(size_t n)是用0来填充
resize(size_t n,char c)是用字符c来填充。

int main()
{
	string s1("hello world");
	s1.resize(20);
	cout << s1 << endl;

	string s2("hello world");
	s2.resize(20,'x');
	cout << s2 << endl;
}

reserve和resize都是可以操纵空间大小的,那它们有什么区别呢?
1.reserve是单纯开空间
2.resize是开空间+填值初始化
3.reserve是只能影响到capacity容量大小,影响不到size的大小。
而resize既能影响capacity容量大小,又能影响size的大小。
因为当resize中的n大于size时,则会扩大size的大小至n,并且容量capacity也会跟着变化。
但当resize中的n小于size时,则只会缩数据个数,不会缩空间大小,也就是只缩小size大小,不改变capacity的大小。

int main()
{
	string s1("hello world");
	//单纯开空间
	s1.reserve(100);
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

	//开空间+填值初始化
	s1.resize(200, 'x');
	cout << s1 << endl;
	//当resize在改变元素个数时,如果元素的个数增多,可能会改变
	//底层容量的大小,如果是将元素个数减少。底层空间总大小不变
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

	s1.resize(20);
	cout << s1.size() << endl;//这个数据大小改变
	cout << s1.capacity() << endl;//但容量不会改变
}
【总结】
成员函数功能
size计算字符串的字符长度
capacity计算空间总大小
clear清空对象数据
empty判断字符串是否被清空
reserve为字符串预留空间
resize将字符串的大小更改成n

1.size()和length()底层实现原理是一样的,出现size()原因是为了于其他容器的接口保持一致,一般都用size().
2.clear()只是清空数据的个数,不会改变capacity的大小的。
3.resize(n)和resize(n,char c)都是用来更改字符大小的。不同的是,当n大于size时,多出来的空间rezie(n)用0初始化,resize(n,char c)用字符c来初始化。当n小于size时,size的大小会改变,但capacity的大小不会改变
4.想单纯的开空间就使用reserve、要想即开空间又初始化,那就用resize。

③.string类的对象的访问与遍历

1.opeartor[]

该成员函数其实就是对[]进行运算符重载。返回pos位置的字符,使用方法跟数组是一样的。都是根据下标来进行访问。

int main()
{
	char ch[] = "hello world";
	cout << ch[2] << endl;
	string s1("hellor world");
	cout << s1[2] << endl;
}

在这里插入图片描述
虽然看起来很像,但它们的底层实现是不一样的,不能混成一块。
char ch[2]的底层实现的原理是*(ch+2).
而s1[2]底层实现的原理是s1.operator[](2).

//如何遍历string对象呢?
  cout << s1.size() << endl;
	for (int i = 0; i < s1.size(); i++)
	{
		s1[i]++;
	}
	
	s1[0]--;//可以根据下标一样来修改string对象
	//下标+[]
	for (int i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << endl;;
	}

注意:下标+[ ]这种访问方式只能用于string和vector,list是无法使用的,因为只有顺序表类型的才可以用这种方式访问,而链表类型的是无法使用该方式访问。但是使用迭代器访问是通用的,什么类型都可以使用迭代器进行访问。

这样还要注意一点,当使用下标访问时,如果访问越界了,即非法访问了。它是会断言报错的。

int main()
{
	string s1("hello world");
	
	for (int i = 0; i < 20; i++)
	{
		s1[i]++;
	}
	
}

在这里插入图片描述

int main()
{
	try {
		string s1("hello world");
		s1.at(0) = 'x';//在某个位置修改
		cout << s1 << endl;
		s1[0] = 'h';
		cout << s1 << endl;
		//s1[15];//如果越界非法访问了,这样会assert警告的。
		///暴力处理
		s1.at(15);//温和的错误处理
	}//会抛异常
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}
2.begin/end

begin可以获得第一个字符的迭代器,end可以获得最后一个字符下一个位置的迭代器。
什么是迭代器呢?
迭代器(iterator)就像一个指针,我们可以暂且将它看作指针类型,但有时候不一定是指针类型。
使用迭代器的方法:

int main()
{
	string s1("hello xiaotao");
	//迭代器--->指针
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		//写
		(*it)--;
		++it;//类似于指针
	}
	while (it != s1.end())
	{
		//读
		cout << *it << " ";
		++it;
	}
}

begin和end又称为正向迭代器,是从开头到结尾进行访问的。

3.rbegin/rend

rbegin是获取最后一个字符的迭代器,rend是获取第一个字符前一个位置的迭起器。
rbegin和rend可以用来进行从后往前遍历,所以又称为反向迭代器。

int main()
{
	string s1("hello world");
	string::reverse_iterator rit = s1.rbegin();//反向迭代器
	while (rit != s1.rend())
	{
		
		cout << (*rit) << endl;//反向遍历
		++rit;
	}
}

在这里插入图片描述

4.范围for

范围for是C++11支持更简洁的新遍历方式。

int main()
{
	string s1("hello xiaotao");
	for (auto ch : s1)
	{
		cout << ch << endl;
	}
}

它会依次将s1中的字符赋给ch,ch会自动识别类型。

1.范围for的底层本质上其实就是替换成迭代器。如果不支持迭代器的用法就不能支持范围for。
为什么范围for可以使用呢?
因为任何容器都支持迭代器的用法,并且用法都是类似的。

	任何容器都支持迭代器,并且用法是类似的。
	vector<int> v;
	vector<int>::iterator vit = v.begin();
	while (vit != v.end())
	{
		cout << *vit << endl;
		++vit;
	}
	
	list<int> lit;
	list<int>::iterator lit = lit.beign();
	while (lit != lit.end())
	{
		cout << (*lit) << endl;
		++lit;
	}

2.所以迭代器(iterator)提供了一种统一的方式进行访问和修改容器的数据。
3.迭代器可以跟算法进行配合。
因为数据封装在容器里面。算法是无法对容器进行修改的,所以要利用迭代器。这样算法就可以通过迭代器去处理容器中的数据,比如reverse和sort。

	//迭代器跟算法进行配合
	reverse(s1.begin(), s1.end());
	sort(s1.beign(), s1.end());

4.迭代器的类型有多种。
比如const修饰的对象就无法使用普通的迭代器来遍历。
而需要使用const迭代器。

void Func(const string& s)
{
	string::const_iterator it = s.begin();
	while (it !=s.end())
	{
		cout << *it << endl;
		++it;
	}
void Func(const string& s)
{
	string::const_reverse_iterator rit = s.rbegin();
	auto rit = s.rbegin();
	while (rit != s.rend())
	{
		cout << *rit << endl;
		++rit;
	}
}

在这里插入图片描述

【总结】
成员函数功能
operator[ ]返回pos位置的字符,利用下标进行访问和遍历
begin/end正向迭代器,可以用来遍历和访问
rbegin/rend反向迭代器,可以倒着遍历
范围forC++11支持的更简洁的遍历方式,任何容器都可以使用

④.string类对象查看与修改操作

1.push_back

在字符串后面尾插一个字符
在这里插入图片描述

int main()
{
	//增操作
	string s1("helllo");
	//尾插一个字符
	s1.push_back('6');
}
2.append

在字符串后面尾插一个字符串
在这里插入图片描述

int main()
{
	//增操作
	string s1("helllo");
	//尾插一个字符
	s1.push_back('6');
	//尾插一个字符串
	s1.append("world");
}
3.operator+=

operator+=是对+=运算符重载
它比push_back和append尾插更方便。而且接口很多
可以尾插一个字符,一个字符串,一个stirng对象等。


int main()
{
	//增操作
	string s1("helllo");
	//尾插一个字符
	s1.push_back('6');
	//尾插一个字符串
	s1.append("world");
	//push_back是用来尾插字符的
	//append是用来插入字符串的
	//但是还有一种方法直接尾插,+=
	s1 += '6';
	s1 += "6666";

问题: 如何将整形转化成string类型呢?

int main()
{	//要求将x转化为string对象?
	size_t x = 0;
	cin >> x;

	string xstr;
	while (x)
	{
		size_t val = x % 10;
		xstr += (val + '0');
		x /= 10;
	}
	//逆转
	reverse(xstr.begin(), xstr.end());
	cout << xstr << endl;
}
4.insert

在字符串的头部插入字符/字符串
在这里插入图片描述

int main()
{
	string s1("hello world");
	//往头部插入10个’x
	s1.insert(0, 10, 'x');
	//insert(位置,个数,字符);
	cout << s1 << endl;
	
	//从第五个位置插入world
	s1.insert(5, "world");
	//insert(位置,字符串)
	cout << s1 << endl;
	
	s1.insert(0, 10, 'x');
	//从第十个位置插入10个y
	s1.insert(s1.begin() + 10, 10, 'y');
	cout << s1 << endl;
}
5.erase

删除字符串
在这里插入图片描述
默认从第第一个位置开始删除。
erase(位置,删除的长度),注意删除的长度给了缺省值npos,这说明当不给定长度时,会默认将pos位置后面的字符全部删除掉。
erase也可以使用迭代器进行删除。删除的是迭代器位置上的字符。

int main()
{
	string s1("hello world");
	s1.erase(5, 1);
	//从第5个位置删除1个字符
	cout << s1 << endl;

	
	//从第五个位置往后全部删除
	s1.erase(5);
	cout << s1 << endl;
	//erase(位置,n个字符=nps缺省值很大的数

	string s2("hello world");
	s2.erase(0, 1);//相当于头删了
	cout << s2 << endl;
	
	s2.erase(s2.begin());
	//删除这个迭代器位置上的字符
	cout << s2 << endl;

  
}
6.replace

替换字符串中的字符
replace(替换的位置,要替换的长度,替换成的字符串)
在这里插入图片描述

int main()
{
	//将world替换成xxxxxxxxxxxx
	string s1("hello world hello xiaotao");
	s1.replace(6, 5, "xxxxxxxxxxxxxxxxxxx");
	//replace(位置,替换的个数,替换成的字符串)
	s1.replace(6, 20, "666");
	cout << s1 << endl;

	//将s2中所以空格全部替换成20%
	string s2 = "hello world hello xiaotao";
	string s3;
	for (auto ch : s2)
	{
		if (ch != ' ')
		{
			s3 += ch;
		}
		else
		{
			s3 += "20%";
		}
	}
	s2 = s3;
	cout << s2 << endl;
}
7.c_str

返回C格式的字符串
这个接口有什么用呢?为什么要返回C格式的字符串呢?
因为在做项目时我们要和C的一些接口函数进行配合才可以使用。
比如某个函数必须要使用C格式的字符串,那么就需要将string类型的字符串转化成C格式的字符串

int main()
{
	//适用于一些C的一些接口函数配合
	string filename = "test.cpp";
	filename += ".zip";
	FILE* fout = fopen(filename.c_str(), "r");//
	//调用这个函数必须要C的字符类型
}
8.substr

从字符串pos位置开始截取n个字符,如何将其返回
substr(位置,长度)注意长度参数给了缺省值,所以如果不给定长度,则默认从pos位置往后一直截取字符。

在这里插入图片描述

int main()
{
	string  s1("hello xiaotao");
	string s2, s3;
	s2 = s1.substr(6);
	//不给位置,则默认从该位置往后一直截取字符,直到截取完毕
	cout << s2 << endl;
	s3 = s1.substr(6, 7);
	//从第6个位置截取7个字符
	cout << s3 << endl;

}

在这里插入图片描述

9.find

从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置。
find(字符,位置),如果不给位置,默认从第一个位置开始往后找。

在这里插入图片描述

int main()
{
	string s1("hello xiaotao");
	cout << s1.find('x') << endl;
	//不给位置,则默认从第一个位置开始往后面找

	cout << s1.find('o', 5);
	//从第5个位置往后找字符'o'找到后返回该字符的位置
}

在这里插入图片描述

我们利用一个题目来深入理解find。
【切割字符串】
将任何一个网站的协议,域名,资源名分割处理:


int main()
{
	string url = "ftp://www.baidu.com/?tn=65081411_1_oem_dg";

	//协议  域名  资源名

	size_t pos1 = url.find("://");
	string protocol;
	if (pos1 != string::npos)
	{
		protocol = url.substr(0, pos1);
		//从位置0这个地方截取pos1长度的字符串赋给protocol
	}
	cout << protocol << endl;
	size_t pos2 = url.find('/', pos1 + 3);
	string domain;//域名
	string uri;//资源名
	if (pos2 != string::npos)
	{
		domain = url.substr(pos1 + 3, pos2 - (pos1 + 3));
		uri = url.substr(pos2+1);
	}
	cout << domain << endl;
	cout << uri << endl;
}

在这里插入图片描述

10.rfind

与find类似但不同的是find是从前往后找,而rfind是从后往前找。
从字符串pos位置往前找字符c,找到了返回该字符所在位置。

如果不给位置,默认从最后一位开始往前找。
在这里插入图片描述

int main()
{
	string s1("hello xiaotao");
	cout << s1.rfind(' ') << endl;
	//找到' '返回该字符的位置
}
【总结】
成员函数功能
push_back在字符串后面尾插一个字符
append在字符串后面追加一个字符串
operatro+=在字符串后面追加str
insert头插字符/字符串
erase尾删字符/字符串
replace替换字符串中的字符
c_str返回C格式的zifc
substr从pos位置截取长度为n的字符串
find从pos开始往后查找字符c,返回该字符的位置
rfind从pos位置开始往前查找字符c,返回该字符的位置

⑤.string类非成员函数

非成员函数功能
operator+对运算符+的重载,尽量少用,因为传值返回,深拷贝效率低
operator<<输入运算符符重载,使string类对象可以直接输入
operator>>输出运算符重载,使string类对象可以直接输出
getline可以获取一行字符串,坚持一行不结束,遇到换行才结束
relational operators各种大小比较运算符重载,使string对象可以直接比较
to_string将其他类型转化为字符串类型

在这里插入图片描述

int main()
{
	//将整形转化成字符类型
	string stri = to_string(1234);
	//将浮点型转化成字符类型
	string strd = to_string(6.11);
}

Ⅲ.牛刀小试:练习string类

1.字符串相加

字符串相加—力扣
第一种:头插方式

class Solution {
public:
    string addStrings(string num1, string num2) {
     int end1=num1.size()-1,end2=num2.size()-1;
     int carry=0;
     string strRet;
     while(end1>=0||end2>=0)
     {
        int val1=end1>=0?(num1[end1]-'0'):0;
        int val2=end2>=0?(num2[end2]-'0'):0;

        int ret=val1+val2+carry;
        carry=ret/10;
        ret%=10;

      strRet.insert(strRet.begin(),ret+'0');
       --end1;
       --end2;
     }
     if(carry==1)
     {
         strRet.insert(strRet.begin(),'1');
     }
     return strRet;
    }
};

第二种:尾插+逆转

class Solution {
public:
    string addStrings(string num1, string num2) {
     int end1=num1.size()-1,end2=num2.size()-1;
     int carry=0;
     string strRet;
     while(end1>=0||end2>=0)
     {
        int val1=end1>=0?(num1[end1]-'0'):0;
        int val2=end2>=0?(num2[end2]-'0'):0;

        int ret=val1+val2+carry;
        carry=ret/10;
        ret%=10;

       strRet+=ret+'0';
      
       --end1;
       --end2;
     }
     if(carry==1)
     {
         strRet+='1';
     }
     reverse(strRet.begin(),strRet.end());
     return strRet;
    }
};

2.字符串最后一个单词的长度

字符串最后一个单词的长度—牛客

#include <iostream>
using namespace std;
#include <string>

int main()
{
    string s1;
    while(getline(cin,s1))
    {
       size_t pos=s1.rfind(' ');
       cout<< s1.size()-(pos+1)<<endl;
    }
    return 0;
}

不能使用流输入cin来输入s1,因为cin和scan遇到空格和换行都会结束,无法完全读取成功,还有一部分会留在缓冲区。所以必须要使用getline来获取完整的一行字符串。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小陶来咯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值