STL好难(2):string类的使用

【本节目标】

  • 1. 标准库中的string类
  • 2. string类的模拟实现
  • 3. 扩展阅读

目录

【本节目标】

1.标准库中的string类

2. string类对象的常见构造

🍉无参构造

🍉带参构造

🍉拷贝构造

🍉用n字符 # 去初始化

🍉用字符串的前n个字符去初始化

🍉从一个 string 对象的 pos 为值开始,拿 len 个字符去初始化

3. string类对象的容量操作

🍉size

🍉length

🍉max_size

🍉capacity

🍉reserve

🍉resize

🍉clear

🍉empty

4. string类对象的访问及遍历操作

🍉[ ] + 下标

🍉at

🍉begin + end(迭代器 iterator)

🍉rbegin 和 rend(反向迭代器 reserve_iterator)

🍉const 迭代器、const反迭代器(const iterator)、(const_reserve_iterator)

🍉范围for

5. string类对象的操作

🍉operator+=

🍉aappend

🍉push_back

🍉pop_back

🍉insert

🍉erase

🍉replace

🍉swap

6.string类运算符函数

🍉operator= 

🍉operator+=

🍉operator+

🍉operator>> 和 operator<<

🍉relational operators (string)


前言:我们为什么要学string类

C语言中的字符串

C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。

string文档介绍

我们下面的学习,都会以文档内容为标准

string类 就类比数据结构的  字符类型的顺序表  来学习能更好的理解

1.标准库中的string类

1. 字符串是表示字符序列的类
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

编码的本质就是:内存当中存的全是 0 和 1,那么如果我要把它显示成对应的文字,比如:英文、中文、日文、韩文等等,只能通过编码显示出来

在显示之前,会通过一张表去对照着查,这个表就是需要显示值的映射表,比如ASCII表

总结:

  • 1. string是表示字符串的字符串类
  • 2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  • 3. string在底层实际是:basic_string模板类的别名,
    typedef basic_string<char, char_traits, allocator> string;
  • 4. 不能操作多字节或者变长字符的序列

在使用string类时,必须包含#include头文件以及using namespace std;

2. string类对象的常见构造

🍉无参构造

构造函数的string类对象,即空字符串

string();

int main()
{
	string s1;
	
	return 0;
}

🍉带参构造

用常量字符串来构造string对象

string (const char* s);

int main()
{
	string s2("hello world");

	return 0;
}

🍉拷贝构造

拿一个已经存在的对象去初始化另一个未存在的对象(加引用是为了减少拷贝)

string(const string& str);

int main()
{
	string s2("hello world");

	string s3(s2);

	return 0;
}

🍉用n字符 # 去初始化

string 类对象内会有n个字符#

string(size_t n, char c);

int main()
{
	// 用9个¥去初始化s4对象
	string s4(9, '¥');

	return 0;
}

🍉用字符串的前n个字符去初始化

string(const char* s, size_t n);

int main()
{
	// 用字符串的前5个字符去初始化s5对象
	string s5("hello world", 5);

	cout << s5 << endl;

	return 0;
}

string存放的是前5个字符

🍉从一个 string 对象的 pos 为值开始,拿 len 个字符去初始化

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

int main()
{
	string s6("STL_is_so_hard");

	// 从s6对象的第0个位置开始(包括第0个位置的字符),往后数7个字符,去初始化s7对象
	string s7(s6, 0, 7);

	cout << s7 << endl;

	return 0;
}

可以看到,len 里面给了个缺省值 nops,那么这个 nops 是多少嘞?

查看文档可以看到:

 npos 其实是 string 类里面的一个静态成员变量,给的值是 -1

但是 size_t 是用无符号来修饰的,也就是说无符号的 -1,它的补码全是1,也就是整形的最大值,所以他的意思就是:从 pos 位置开始,往后取 42 亿多个字符。

也就是代表着如果不给 len 值,那么就是有多少取多少

3. string类对象的容量操作

我们需要学习的以下函数接口:

函数名称功能说明
size(重点)返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty(重点)检测字符串释放为空串,是返回true,否则返回false
clear(重点)清空有效字符
reserve(重点)为字符串预留空间**
resize(重点)将有效字符的个数该成n个,多出的空间用字符c填充

🍉size

返回字符串有效长度

size_t size() const;

int main()
{
	string s1("hello world");

	cout << s1.size() << endl;

	return 0;
}

🍉length

返回字符串有效字符长度(和 size 一样)

size_t length() const;

int main()
{
	string s2("hello world");

	cout << s2.length() << endl;

	return 0;
}

size length 方法底层实现原理完全相同,引入 size 的原因是为了与其他容器的接口保持一致,所以一般情况下基本都是用 size

🍉max_size

返回字符串可以达到的最大长度

size_t max_size() const;

int main()
{
	string s3("hello world");

	cout << s3.max_size() << endl;

	return 0;
}

这个实际上就是告诉你,字符串最大能开多长,但是这个函数在实际中并没有什么太大的用处。大家可以试试运行下看看最大能开多大

🍉capacity

 返回分配的存储空间容量大小

size_t capacity() const;

int main()
{
	string s4("hello world");

	cout << s4.capacity() << endl;

	return 0;
}

可以看到,容量的大小默认为 15 ,那么 string类 是怎样扩容的?

可以看到,VS下,capacity第一次是扩2倍,之后是进行1.5倍的扩容

在Linux平台下进行编译会发现又是2倍来进行扩容的,这是因为他们版本不同。VS是PJ版,它是微软进行维护的;Linux下的g++是开源社区维护的。也正是STL并没有明确的扩容机制,所以每个社区或平台上的是不一样的。

另外扩容也是有代价的,需要开新的空间,那么有什么方法能够减少扩容呢?
这就需要我们的 reserve 函数

🍉reserve

为字符串预留空间

void reserve(size_t n = 0);

int main()
{
	string s;
	s.reserve(1000); // 提前开好空间
	size_t sz = s.capacity();
	cout << "扩容" << endl;
	cout << "capacity changed:" << sz << endl;

	// 因为前面开了1000个空间,现在往小的阔,编译器并不会执行,而是保持之前的大小
	for (int i = 0; i < 1000; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "往小的阔_capacity changed: " << sz << endl;;
		}
	}
	return 0;
}

当使用 reserve 改变当前对象的容量大小,并提前开辟空间时,
注意:

  • 当 n 大于对象当前的 capacity 时,将 capacity 扩大到 n 或大于 n。
  • 当 n 小于对象当前的 capacity 时,什么也不做。
  • 此函数对字符串的 size 没有影响,并且无法更改其内容。

如下测试:

void TestString() 
{
	string s("HelloWorld");

	cout << s << endl; //HelloWorld
	cout << s.size() << endl; //10
	cout << s.capacity() << endl; //15

	cout << endl;

	//reverse(n)当n大于对象当前的capacity时,将当前对象的capacity扩大为n或大于n
	s.reserve(20);
	cout << s << endl;
	cout << s.size() << endl; //10
	cout << s.capacity() << endl; //31

	cout << endl;

	//reverse(n)当n小于对象当前的capacity时,什么也不做
	s.reserve(2);
	cout << s << endl;
	cout << s.size() << endl; //4
	cout << s.capacity() << endl; //31
}

结果如下: 

总结:

void reserve (size_t n = 0):为 string 预留空间,不改变有效元素个数,当 reserve 的参数小于 string 的底层空间总大小时,reserver 不会改变容量大小。

🍉resize

将有效字符的个数改成 n 个,多出的空间用字符 c 填充

void resize(size_t n);
void resize(size_t n, char c);

使用 resize 改变当前对象的有效字符个数

  1. 当 n 大于对象当前的 size 时,将 size 扩大到 n,扩大的字符为 c,若 c 未给出,则默认为 0
  2. 当 n 小于对象当前的 size 时,将 size 缩小到 n
void TestString()
{
	string s1("HelloWorld");
	//resize(n)n大于对象当前的size时,将size扩大到n,扩大的字符默认为'\0'
	s1.resize(20);
	cout << s1 << endl; //HelloWorld
	cout << s1.size() << endl; //20
	cout << s1.capacity() << endl; //31

	cout << endl;

	string s2("HelloWorld");
	//resize(n, char)n大于对象当前的size时,将size扩大到n,扩大的字符为char
	s2.resize(20, 'x');
	cout << s2 << endl; //HelloWorldxxxxxxxxxxxxxxxx
	cout << s2.size() << endl; //20
	cout << s2.capacity() << endl; //31

	cout << endl;

	string s3("HelloWorld");
	//resize(n)n小于对象当前的size时,将size缩小到n
	s3.resize(2);
	cout << s3 << endl; //He
	cout << s3.size() << endl; //2
	cout << s3.capacity() << endl; //15
}

结果:

相比于 reserve 只是用来开空间resize 的作用就是 开空间+初始化

关于 reserve resize,它们在 VS 下都不会缩容量

但是可以看到我们扩容的时候,明明是申请的 20 个空间,为什么打印出来有 31 个呢?这是因为 capacity 的内存对齐

这里可以简单解释一下:
系统去申请内存,需按照整数倍去对齐,就算我们不对齐,那么系统也会自动去对齐的;


比如这里的31,加上最后的 ' \0 '  就是32,刚好时4个字节


为什么要申请对齐呢?
是因为和内存的效率以及内存碎片有关。

🍉clear

擦除字符串的内容,该字符串变为空字符串(长度为 0 个字符)
使用 clear 删除对象的内容,删除后对象变为空字符串,但是对象的容量不会被清理掉

void clear();

int main()
{
	string s1("hello world");
	cout << s1 << endl; //HelloWorld
	cout << s1.size() << endl; //11
	cout << s1.capacity() << endl; //15

	cout << endl;

	s1.clear();
	cout << s1 << endl; //空
	cout << s1.size() << endl; //0
	cout << s1.capacity() << endl; //15

	return 0;
}

clear 只是将 string 对象中有效字符清空,不改变底层空间大小。

🍉empty

判断字符串是否为空,如果字符串长度为 0,则为 true,否则为 false

bool empty() const;

int main()
{
	string s1("hello world");
	string s2;

	// 字符串不为空,返回0
	cout << s1.empty() << endl;

	// 字符串为空,返回1
	cout << s2.empty() << endl;

	return 0;
}

4. string类对象的访问及遍历操作

函数名称功能说明
operator[ ](中点)返回pos位置的字符,const string类对象调用
at返回pos位置的字符,const string类对象调用
begin  +  endbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
rbegin rend

begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器

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

🍉[ ] + 下标

string 类 对 [ ] 运算符做了重载,所以我们可以直接使用  [ ] + 下标 访问对象中的元素

并且,重载用的是 引用返回 ,所以我们可以直接通过  [ ] + 下标 来修改对于位置元素

char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;


int main()
{
	string s1("hello world!");

	//1.使用下标访问对象元素
	for (size_t i = 0; i < s1.size(); ++i) 
	{
		cout << s1[i];
	}

	cout << endl;

	//2.使用下标修改对象元素
	for (size_t i = 0; i < s1.size(); ++i) 
	{
		s1[i] = 'x';
	}
	cout << s1 << endl;
}

  [ ] + 下标 也有个const版本,加上const就不能用下标进行修改了

🍉at

at 和 operator[ ]  很相似,区别是:当他们出错时编译器会有不同的警告

int main()
{
	string s1 = "hello world";

	s1[100];
	s1.at(100);

	return 0;
}

operator[ ]:就是 assert() 那种直接中断的报错警告

at :at是抛异常的警告

抛异常是能被 捕获 到的:

int main()
{
	string s1 = "hello world";

	try
	{
		s1.at(100);
	}
	catch(const exception& a)
	{
		cout << a.what() << endl;
	}

	return 0;
}

结果:程序会继续运行,但会出现提示

实际过程中很少用at

🍉begin + end(迭代器 iterator)

begin 获取第一个字符的迭代器 + end 获取最后一个字符下一个位置的迭代器

void TestString() 
{
	string s1("hello world!");

	//使用迭代器访问对象元素
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//使用迭代器修改对象元素
	string::iterator aaa = s1.begin();
	while (aaa != s1.end())
	{
		*aaa += 1;
		++aaa;
	}
	cout << s1 << endl;
}

对于迭代器,可以理解为像指针一样的东西,或者说就是指针。

🍉rbegin 和 rend(反向迭代器 reserve_iterator)

rbegin 获取之后一个字符的迭代器 + end 获取第一个字符上一个位置的迭代器

rbegin + rend 打印出来的是数组的反串

void TestString() 
{
	string s1("hello world!");

	//使用迭代器访问对象元素
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
}

🍉const 迭代器、const反迭代器
(const iterator)、(const_reserve_iterator)

我们可以在C++文档中可以看到的begin、end、rbegin、rend中都有个const修饰的类型

以begin为例:

普通对象用普通迭代器,const对象用const迭代器

void Func(const string& s)
{
	//const迭代器的使用,遍历和读容器的数据,不能修改
	string::const_iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}
int main()
{
	string s1 = "hello world";
	Func(s1);
	return 0;
}

对应的还有const反迭代器 const_reserve_iterator

void Func(const string& s)
{
	string::const_reverse_iterator rit = s.rbegin();
	while (rit != s.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
}
int main()
{
	string s1 = "hello world";
	Func(s1);
	return 0;
}

🍉四种迭代器:

  • iterator
  • reserve_iterator
  • const_iterator
  • const_reserve_iterator

这里我们可以直接用 auto 语法来让编译器自动推导

void Func(const string& s)
{
	//string::const_reverse_iterator rit = s.rbegin();
    
    //用auto自动推导
	auto rit = s.rbegin();
	while (rit != s.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
}

🍉范围for

范围for 的底层实现就是通过迭代器实现的。

如果我们是通过范围 for 来修改对象的元素,那么接收元素的变量 e 的类型必须是引用类型,否则 e 只是对象元素的拷贝,对 e 的修改不会影响到对象的元素。

void TestString() 
{
	string s1("hello world!");

	//使用范围for访问对象元素
	for (auto e : s1) 
	{
		cout << e;
	}
	cout << endl;

	//使用范围for修改对象元素
	for (auto& e : s1)
	{
		e = 'x';
	}
	cout << s1 << endl;
}

5. string类对象的操作

 点击查看string类文档

对象的操作有很多,主要可以分为:插入、删除、查找、拼接

🍉operator+=

string 类中对 += 运算符进行了重载,重载后的 += 运算符支持 string 类的复合赋值、字符串的复合赋值以及字符复合的赋值

 1)插入一个string对象

string& operator+= (const string& str);

int main()
{
	string s1;
	string s2("hello");

	s1 += s2;

	cout << s1 << endl;

	return 0;
}

2)插入一个常量字符串

string& operator+= (const char* s);

int main()
{
	string s1("hello");

	s1 += "world";

	cout << s1 << endl;

	return 0;
}

3)插入一个字符

string& operator+= (char c);

int main()
{
	string s1("hello");

	s1 += "!";

	cout << s1 << endl;

	return 0;
}

总结:

在 string尾插字符时,有以下三种方式

  • s.push_back(c) 
  • s.append(1, c) 
  • s += ' c '

三种的实现方式差不多,一般情况下 string 类的 += 操作用的比较多
+= 操作不仅可以连接单个字符,还可以连接字符串

🍉aappend

 在字符串后追加一个字符串(基本用不到,因为用 += 更简单)

1)插入一个string对象

string& append(const string& str);

int main()
{
	string s1("hello");
	string s2("world");

	//直接把s2对象拼接到s1的后面
	s1.append(s2);

	cout << s1 << endl;

	return 0;
}

2)插入一个常量字符串

 

string& append(const char* s);

int main()
{
	string s1("hello");

	//在s1字符串后面拼接新的字符串
	s1.append("world");

	cout << s1 << endl;

	return 0;
}

3)插入 n 个字符 c

 

string& append(size_t n, char c);

int main()
{
	string s1("hello");

	//将3个字符拼接到s1对象后面
	s1.append(3, '!');

	cout << s1 << endl;

	return 0;
}

🍉push_back

尾插,将字符 c 追加到字符串末尾,将其长度增加1

void push_back(char c);

int main()
{
	string s1;

	s1.push_back('h');
	s1.push_back('e');
	s1.push_back('l');
	s1.push_back('l');
	s1.push_back('o');

	cout << s1 << endl; //hello

	return 0;
}

🍉pop_back

尾删,删除string最后一个字符

void pop_back();
int main()
{
	string s1("hello");

	s1.pop_back();

	cout << s1 << endl;

	return 0;
}

🍉insert

 可以看到insert有很多结构,但并不是每个都会用到,这里我们选几个用的最多的来演示

1)在 pos 位置插入一个常量字符串

string& insert(size_t pos, const string& str);

int main()
{
	string s("hello");

	// 在第0个位置插入字符串xxx
	s.insert(3, "xxx");  

	cout << s << endl;  // helxxxlo 

	return 0;
}

2)在 pos 位置插入 n 个字符串 c

 

string& insert(size_t pos, size_t n, char c);

int main()
{
	string s("hello");

	// 在第3个位置插入5个字符y
	s.insert(3, 5, 'y');

	cout << s << endl; //helyyyyylo

	return 0;
}

3)使用迭代器来进行插入

 

iterator insert(iterator p, char c);

int main()
{
	string s("hello");

	// 从begin开始的5个位置插入字符y
	s.insert(s.begin() + 5, 'y');

	cout << s << endl; //helloy

	return 0;
}

begin(),是s对象的第一个位置的指针

🍉erase

从字符串中删除字符

1)从 pos 位置开始,删除 len 个字符

 

string& erase(size_t pos = 0, size_t len = npos);

int main()
{
	string s1("helloworld");

	从s1下标为5的位置开始,往后删除2个字符
	s1.erase(5, 2); // 删除 w 和 o

	cout << s1 << endl; //hellorld


	return 0;
}

注意:len 参数的缺省值是 npos,不给值会将后面的全删掉,删到' /0 ' 

2)从迭代器的位置删除字符

 

iterator erase(iterator p);

int main()
{
	string s1("helloworld");

	//删除s1字符串第一个位置的元素
	s1.erase(s1.begin());

	cout << s1 << endl; //elloworld

	return 0;
}

3)从迭代器开始的位置,一直删除到迭代器结束的位置

 

iterator erase(iterator first, iterator last);

int main()
{
	string s1("helloworld");

	//从begin+5的位置开始,一直删除到end的位置
	s1.erase(s1.begin() + 5, s1.end());

	cout << s1 << endl; // hello

	return 0;
}

🍉replace

替换字符串的一部分

替换也有很多接口,我们只了解圈出来的两个即可

 1)从 pos 位置开始的第 len 个字符替换为一个字符串

 

string& replace(size_t pos, size_t len, const char* s);

int main()
{
	string s1("helloworld");

	//将第6个位置开始的4个字符替换为字符串xxxyyy
	s1.replace(6, 4, "xxxyyy");

	cout << s1 << endl; //hellowxxxyyy

	return 0;
}

 2)从 pos 位置开始的第 len 个字符替换为 n 个字符 c

string& replace(size_t pos, size_t len, size_t n, char c);

int main()
{
	string s1("helloworld");

	//将第5个位置开始的1个字符替换为3个字符x
	s1.replace(5, 1, 3, 'x');

	cout << s1 << endl; //helloxxxorld

	return 0;
}

🍉swap

交换两个字符串的值

void swap(string& str);

int main()
{
	string s1("hello");
	string s2("world");

	cout << "交换前" << endl;
	cout << "s1:" << s1 << endl;
	cout << "s2:" << s2 << endl;

	//使用string类的成员函数swap交换s1和s2
	s1.swap(s2);

	cout << "交换后" << endl;
	cout << "s1:" << s1 << endl;
	cout << "s2:" << s2 << endl;

	return 0;
}

对于string里面的swap我们还要说一点,我们要知道,在我们全局里面也有一个swap函数,他能完成各种类型的交换,当然也包括string

int main()
{
	string s1("hello");
	string s2("world");

	cout << "交换前" << endl;
	cout << "s1:" << s1 << endl;
	cout << "s2:" << s2 << endl;

	//使用全局swap函数交换s1和s2
	swap(s1, s2);

	cout << "交换后" << endl;
	cout << "s1:" << s1 << endl;
	cout << "s2:" << s2 << endl;

	return 0;
}

那么这两个swap的区别是什么,用哪个实现起来更效果更高呢?

string 里面的 swap 是直接交换指针的指向,而全局的swap是进行拷贝,
所以用 string 里面的 swap 效率高

6.string类运算符函数

🍉operator= 

string类中,对=运算符进行了重载,使其能支持string类的赋值、字符串的赋值以及字符的赋值

1)支持string类对象之间的赋值

 

int main()
{
	string s1;
	string s2("hello world");

	s1 = s2;

	cout << s1 << endl;

	return 0;
}

2)支持字符串赋值

 

int main()
{
	string s1;

	s1 = "hello world";

	cout << s1 << endl;

	return 0;
}

🍉operator+=

上面已经有过介绍、这里就不多赘述了

🍉operator+

string类中,对+运算符进行了重载,使其可以进行+string类、+字符串、字符的操作

 

int main()
{
	string s11;
	string s12;
	string s13;
	string s2("hello");
	string s3("world");
	char str[] = "C++";
	char a = '!';
	//string类 + string类
	s11 = s2 + s3;

	//string类 + 字符串,或者:字符串 + string类
	s12 = s11 + str;	//s12 = str + s11;

	//string类 + 字符,或者:字符 + string类
	s13 = s12 + a;		//s13 = a + s12;

	cout << s11 << endl;
	cout << s12 << endl;
	cout << s13 << endl;


	return 0;
}

结果如下:

🍉operator>> 和 operator<<

string也对>>和<<进行了重载,可以再cin和cout中直接使用string类进行输入和输出

int main()
{
	string s1;

    //流提取
	cin >> s1; 

    //流插入
	cout << s1 << endl; 

	return 0;
}

🍉relational operators (string)

string还对其他一系列操作符进行了重载,分别是:==,!=,<=,>=,<,>

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值