STL+泛型算法 学习总结

C++标准库


前言

本文记录个人学习 c++primer 标准库的一些总结,书中是按照容器的类别分成两大类进行总结,介绍所有容器相同的操作以及每种容器特有的操作。

本文按照每种容器进行总结,将每种容器的操作(尽量全的)分别做总结。持续更新……

一、容器概述

容器 可以理解为封装好的一个用来存储给定类型对象的模板类型。

除 array 以外的所有容器都提供高效的动态内存管理。我们不需要担心元素存储在哪里,容器负责管理自身的容器。

每个容器都有定义其 构造函数添加删除元素、确定容器大小操作、返回指向特定元素的迭代器的操作。而基于容器的排序和搜索等,并不是由容器类型定义的,而是由标准库算法实现的。

二、容器的分类

顺序容器

  • array 固定大小数组。
  • vector 可变大小数组。
  • string 专门存储字符的字符串容器。
  • deque 双端队列。
  • list 双向链表。
  • forward_list 单向链表。

关联容器

  • set 只保存关键字的容器。
  • multiset 关键字可以重复出现的set。
  • unordered_set 哈希表,用哈希函数组织的set。
  • unordered_multiset 关键字可以重复出现的 哈希set。
  • map
  • multimap
  • unordered_map
  • unordered_multimap

一个容器就是一些特定类型对象的集合。
顺序容器 为程序提供了

二、容器的操作

每种容器我从 定义和初始化添加元素删除元素访问元素赋值操作容器大小操作其他成员函数操作几个部分进行总结。

迭代器 鉴于所有容器的迭代器都基本相同(forward_list另外说),迭代器的总结放在开头做一个总结。

定义和初始化 整体可以分为 默认初始化列表的拷贝初始化另一个对象的整体/部分拷贝初始化n个相同元素初始化 4种。根据容器的特点又有所不同,array 不支持 范围迭代器拷贝 和 给大小参数的初始化方式;

容器大小操作 shrink_to_fit 只适用于 vector,string,deque。deque虽然不需要预留空间,但也可以将容器空间大小调整为容器中元素数量大小。
capacity 和 reserve 只适用于 vector,string,也就是说只有类似于单端数组的容器才需要动态扩容或者说预留空间。

赋值操作 assign 操作不适用与 关联容器和 array

交换操作 只有 string 会在交换后使指向容器的迭代器、引用、指针失效。因为除 array 外,交换容器 元素并不会被移动。 array 是会唯一一个在交换时会交换其元素的容器,但其迭代器、引用、指针锁绑定元素操持不变。


迭代器

//迭代器类型别名
vector<int>::iterator;   //正向容器迭代器类型,是一个类型,可以定义迭代器对象
const_iterator   //常量迭代器类型,指向的元素不能修改,只能读取
size_type  //无符号整数类型,定义一个变量来保存容器存储元素的值的大小。可以类比int来理解。
difference_type  //带符号整数类型,足够保存两个迭代器之间的距离。   定义一个变量来存储两个迭代器之间的距离
value_type   //元素类型,相当于用<>中的类型定义一个变量来存储容器中某个对象
reference    //元素类型的引用,定义一个变量来绑定容器中的某个对象
const_reference  //不能修改被绑定元素值

//获取 container 容器迭代器,下边简写为 c
c.begin();   //返回 指向首元素位置的迭代器
c.end();     //返回 指向尾后元素位置的迭代器
c.cbegin();		//返回指向首元素位置的迭代器,只能读取指向元素,不能修改
c.cend();    	//返回 const_iterator

//以下反向容器迭代器 不支持 forward_list
reverse_iterator    	//按逆序寻址元素迭代器类型
const_reverse_iterator	//不能修改元素的逆序迭代器类型
c.rbegin();		//返回指向容器的尾元素位置(反向首元素位置)的迭代器类型
c.rend();		//返回指向容器首前元素位置的迭代器类型
c.crbegin();	//返回 const_reverse_iterator,和 .rbegin() 区别仅在于只能读取不能修改指向元素
c.crend();

顺序容器

array

固定大小数组。
支持快速随机访问。
不能添加或删除元素。

定义和初始化
array 的定义需要同时指明 存储元素类型存储空间大小
初始化方式可以通过 默认初始化列表初始化整体拷贝另一个对象

#include <array>
using namespace std;
//须要同时指定 存放类型和数组容器大小
//默认初始化,会生成 size个type类型默认值
array<type, size> arr;    

//列表初始化。列表中的元素类型要是 type类的;②元素个数少于等于 size 
array<type, size> arr2{a, b, c...};  
array<type, size> arr2 = {a, b, c ...};   

//拷贝另一个对象初始化时,两个容器的 元素类型及元素个数 要相同
array<type, size> arr3(arr2);
array<type, size> arr3 = arr2;   

添加、删除元素
array是固定大小的数组容器。在运行时不可以动态的添加或删除元素来改变容器的大小。

访问元素
在内存中是连续存储的容器,在访问元素时都可以通过 下标法[]at()函数 来实现,也可以通过解引用迭代器的方式来访问元素,对于连续存储容器 (array, vector, string, deque) 的迭代器除可以++ --的前移后移外,还可以进行跨越若干步的运算操作。

//通过直接操作成员函数来访问(front, back, at, [])
arr.front();    //返回arr数组首元素的引用。若arr为空,函数行为未定义
arr.back();   //返回数组尾元素的引用
arr[index];   //返回数组中下标为 index的元素的引用
arr.at(index);  //同arr[index].区别是 at会在小标越界的时候抛出异常,可以确保下标是合法的

//解引用迭代器法

容器大小操作

arr.empty();    //判读容器是否为空。   返回bool类型,空返回true,非空返回false
arr.size();     //返回容器中存在元素的个数
arr.max_size();  //返回容器最大能够存储的元素数

赋值和交换
assign 赋值操作 不适用于关联容器和 array。
swap 操作交换两个相同类型容器的内容, swap 两个 array 会真正的交换他们的元素,交换两个 array的时间与 array中的元素个数成正比。交换之后的迭代器所绑定的元素保持不变,但元素值已经和对应元素值发生了交换。

arr = arr1;    // = 号赋值

arr.swap(arr1);   //两个 array类型必须相同。

vector

可变大小数组。
支持快速随机访问。
尾插常数时间,除此之外的位置插入删除元素可能很慢。

定义和初始化 vector 对象

定义初始化方式比较灵活,几种方式都可以。

#include <vector>
using namespace std;

//默认初始化,v 不含任何元素,是一个空 vector
vector<type> v;     

//列表初始化
vector<type> v2{a, b, c ……};    //直接初始化(推荐)
vector<type> v2 = {a, b, c ……}; //赋值初始化(会比直接初始化慢点?)

//拷贝另一个对象(全部 \ 迭代器范围内元素)
vector<type> v3(v);     //v2中包含 v中所有元素的副本
vector<type> v3 = v;
vector<type> v4(beg, end);   //用 迭代器范围 内的元素初始化对象 

容器初始化为 n个val
vector<type> v5(n, val);  
vector<type> v5(n);  //初始化为 n个默认值,默认值取决于 type的类型

添加元素

单端数组,不支持 push_frong()。尾插元素O(1)时间。 在指定迭代器位置添加元素需要移动其后续元素,指向插入位置之后的迭代器、引用、指针会失效。

向一个 vector 和 string 添加元素可能会引起整个对象存储空间的重新分配。容器在每次刷新存储空间时会预留一定的空间,只有当预留空间被填满之后才会进行存储空间的重新分配,重新分配存储空间会使原来的迭代器、引用、指针全部失效。

emplacepush 的区别:调用emplace时,传递参数即可,根据传递的参数在容器管理的内存空间中直接创建对象;而调用push会创建一个局部临时变量,并将其压入容器中,传递的是一个对象,这些对象被拷贝到容器中。

v.push_back(val);  //val 由<type>决定
v.emplace_back(args);

//上边两个在尾部添加元素无返回值。在迭代器指向位置添加元素后会  返回一个迭代器,指向新添加的元素或新添加元素列表的第一个元素
v.emplace(pos, args);   //在迭代器指向元素前添加一个元素,该元素是通过拷贝参数在数组空间内直接创建对象
v.insert(pos, val);   
v.insert(pos, n, val);
v.insert(pos, {val1, val2, ...});
v.insert(pos, beg, end);

删除元素

同样的只能从尾部进行删除操作,常数时间。或者删除指定位置或指定范围的元素。

v.pop_back();    //尾删。无返回值

//删除指定位置\指定范围元素。 返回一个迭代器,迭代器测试返回为 pos\beg
v.erase(pos);
v.erase(beg, end);

v.clear();  //删除所有元素。无返回值

访问元素

迭代器的解引用访问也可以,底层是连续存储的顺序容器的 迭代器都是比较灵活的,可以跳转迭代器运算。

//通过直接操作成员函数来访问(front, back, at, [])
v.front();    //返回 数组v首元素的引用。若数组为空,函数行为未定义
v.back();   //返回数组尾元素的引用
v[index];   //返回数组中下标为 index的元素的引用
v.at(index);  //同v[index].区别是 at会在小标越界的时候抛出异常,可以确保下标是合法的

//也通过解引用迭代器来访问元素

容器大小操作

v.empty();   	//容器是否为空?空 返回true;非空 返回false
v.size();		//返回容器中存储的元素数量
v.capacity();  	//返回容器现在 在不扩容的基础上最多能存储的元素数量
v.shrink_to_fit();  //请将 capacity() 减少为和 size() 相同大小,但不一定成功。
v.max_size();   //返回容器最大能容纳的元素数量。capacity() 不断扩容最后能到达的最大值

v.resize(n);		//调整容器的大小为 n 个元素。n<size,则删除多余的元素;n>size,新添加的元素按指定值初始化,否则按类型默认初始化。
v.resize(n, val);
v.reserve(n);      //空间预留。分配至少能容纳 n 个元素的内存空间

赋值和交换

assign 赋值操作 适用于 vector, string, deque, list, forward_list.

v.assign({...});     //将容器 v中元素 替换为 初始化列表中的元素
v.assign(beg, end);  //替换为 另一个对象的范围迭代器中的部分
v.assign(n, val);      //替换为 n个值为 val 的元素
v = v1;       //替换为 v1中元素的拷贝。
v = {...};    

//除 array以外,swap 不对任何元素进行拷贝操作,可以保证在常数时间内完成。交换之后原有的迭代器不会失效,不过指向的容器发生交换。
v.swap(v1);	 	//交换的容器类型必须相同。
swap(v, v1);   //适用非成员版本的 swap 是一个好习惯。通常比从v1向v拷贝元素快的多。


string

与 vector 相似的容器,但专门用于存储字符。
支持快速随机访问。
尾部插入/删除常数时间,剩余位置插入删除可能很慢。

定义和初始化 string 对象
string 不适用 只指定n的构造方式,想要用大小参数来初始化对象,必须同时指明 n个ch。

#include <string>
using namespace std;

//默认初始化,str1 是一个空字符串
string str;     

//列表初始化
string str2("hello world");
string str2 = "hello world";

//拷贝另一个对象(整体/迭代器范围内的元素))
string str3(str);
string str3 = str;
string str4(beg, end);  //用 迭代器范围 内的元素来初始化

//n个初始化为 ch 的元素 来初始化string对象
string str5(n, 'ch');   //初始化为连续 n个字符 ch组成的串

添加元素
会改变容器的大小,string 不支持 push_front, emplace_front。只能从串尾追加字符或字符串。

//在字符串 str的尾部插入一个字符,ch 不可以是字符串。 无返回值
str.push_back(ch);     
str.emplace_back(args);    

//在迭代器 pos位置的元素前插入元素。 会使所有指向容器的迭代器、引用、指针失效。
//返回一个迭代器,指向新添加的元素。如果添加的不只一个元素,则指向新添加所有元素的第一个元素。
str.emplace(pos, args);
str.insert(pos, ch);   //插入一个字符
str.insert(pos, n, ch);    //插入 n个字符
str.insert(pos, beg, end);  //插入 迭代器范围内的元素,迭代器不能是自身迭代器
str.insert(pos, {ch, ch, ……}); //插入一个 元素值列表

str += str1;   //在一个字符串后拼接一个字符或字符串。

访问元素

//通过直接操作成员函数来访问(front, back, at, [])
str.front();    //返回 数组str首元素的引用。若数组为空,函数行为未定义
str.back();   //返回数组尾元素的引用
str[index];   //返回数组中下标为 index的元素的引用
str.at(index);  //同str[index].区别是 at会在小标越界的时候抛出异常,可以确保下标是合法的

//也通过解引用迭代器来访问元素

读写操作

//字符串的读写操作
os << str;    //os 是一个输出流对象,将 str写到输出流 os中。返回 os
is >> str;    //is 是一个输入流对象,从 is中读取字符串赋值给 str,读取字符串时以空格为一个字符串的结束。返回 is
getline(is, str);  //从 is中读取一行复制给 str。返回 is

处理 string 对象中的 字符

isalpha(ch);   //字符ch 是字母时为真
isdigit(ch);   // ch 是数字时为真
isalnum(ch);   // ch 是字母或数字时为真
islower(ch);   // ch 是小写字母时为真
isupper(ch);   // ch 是大写字母时为真
tolower(ch);   //把大写字母变为小写字母
toupper(ch);   //把小写字母变为大写字母

iscntrl(ch);   //ch 是控制字符时为真
isspace(ch);   //当 ch 是空白(空格、制表符、回车符、换行符、进纸符)时为真
isprint(ch);   //ch 是可打印字符(空格或 ch具有可视形式)时为真
isgraph(ch);   //ch 不是空格但可以打印时为真
ispunct(ch);   //ch 是标点符号时为真(不能是数字、字母、控制字符、可打印空白)
isxdigit(ch);  //ch 是十六进制数字时为真

list

双向链表。
只能双向顺序访问
任何位置插入/删除元素都常数时间,很快。

定义和初始化

也比较灵活,几种构造方式都可以。

#include<list>
using namespace std;

//默认初始化
list<type> lst;

//列表初始化
list<type> lst2{...};
list<type> lst2 = {...};

//拷贝其他对象的整体或局部
list<type> lst3(lst2);
list<type> lst3 = lst2;
list<type> lst3(beg, end);

//接受大小参数的初始化方式
list<type> lst4(n);
list<type> lst4(n, val);

添加元素

双向链表,头部尾部都可添加元素,中间任意位置也可以常数时间内添加元素。

lst.push_back(val);
lst.push_front(val);
lst.emplace_back(args);
lst.emplace_front(args);

lst.emplace(pos, args);
lst.insert(pos, val);
lst.insert(pos, n, val);
lst.insert({...});
lst.insert(pos, beg, end);

删除元素

lst.pop_back();
lst.pop_front();

lst.erase(pos);
lst.erase(beg, end);

lst.remove(val);    //直接在链表中删除指定元素,如果存在的话。

lst.clear();

访问元素

//通过直接操作成员函数来访问(front, back)。  at, []不适用不支持快速访问的容器。且迭代器只能++ --操作
list.front();    //返回 数组list首元素的引用。若数组为空,函数行为未定义
list.back();   //返回数组尾元素的引用

//也通过解引用迭代器来访问元素

容器大小操作

lst.empty();
lst.size();
lst.max_size();

lst.resize(n);
lst.resize(n, val);

赋值和交换

lst.assign(n, val);
lst.assign({...});
lst.assign(beg, end);
lst = lst2;
lst = {...};

lst.swap(lst2);
swap(lst, lst2);

deque

双端队列(双端数组)。
支持快速随机访问。
头部和尾部 插入/删除都很快,此外位置可能很慢。

定义并初始化

定义和初始化方式同样比较灵活。

基本和 vector 类似,唯一区别就是因为头部开了口而导致的一系列区别。

#include <deque>
using namespace std;

//默认初始化
deque<int> deq;

//列表初始化
deque<int> deq2{ 1, 2, 3 };
deque<int> deq2 = { 1, 2, 3 };

//拷贝其他对象初始化(整体拷贝\部分拷贝-通过迭代器范围)
deque<int> deq3(deq2);
deque<int> deq3 = deq2;
deque<int> deq4(beg, end);

//提供大小参数来初始化
deque<int> deq5(3, 1);
deque<int> deq5(3);

添加元素

因为是 双端数组,所以除vector支持的添加操作外,还多了 front 的相关操作。

//从头尾两侧添加元素
deq.push_back(val);			//在容器尾部创建一个置为 val 的元素,val是先创建一个临时变量,在压入到数组中
deq.push_front(val);
deq.emplace_back(args); 	//区别是:通过传递的参数 在容器管理的空间内直接创建元素
deq.emplace_front(args);

//添加到任意位置(位置信息必须给,添加内容也需要)
deq.emplace(pos, args);    //给出指定位置和传递参数即可。
deq.insert(pos, val);     //指定位置添加 val值
deq.insert(pos, n, val);  //指定位置 添加n个val值
deq.insert(pos, {...});   //指定位置添加一个初始化列表
deq.insert(pos, beg, end);   //指定位置添加一个 范围迭代器内包含元素

删除元素

同多了 front 的删除方式。

//删除头尾两侧元素
deq.pop_back();
deq.pop_front();

//删除任意位置元素
deq.erase(pos);     //指定要删除位置即可
deq.erase(bgn, end);  //指定要删除的位置

deq.clear();    //删除容器中所有元素

访问元素

//通过直接操作成员函数来访问(front, back, at, [])
deq.front();    //返回 数组deq首元素的引用。若数组为空,函数行为未定义
deq.back();   //返回数组尾元素的引用
deq[index];   //返回数组中下标为 index的元素的引用
deq.at(index);  //同deq[index].区别是 at会在小标越界的时候抛出异常,可以确保下标是合法的

//也通过解引用迭代器来访问元素

容器大小操作

deque 没有预留空间查循容器空间的操作。但是却可以将容器空间减少为与 size() 相同的大小?

deq.empty();
deq.size();
deq.shrink_to_fit();    //没有 capacity() 的操作,却可以有该操作。
deq.max_size();

deq.resize(n);
deq.resize(n, val);

赋值和交换

deq.assign(n, val);
deq.assign({...});
deq.assign(beg, end);
deq = {...};
deq = deq2;

deq.swap(deq2);
swap(deq, deq2);    //推荐适用

stack

栈的底层可以是 vector,deque,list。对外提供 push, pop, top, empty.


关联容器 - 哈希结构

数组 set map 都用到了哈希函数映射。 哈希结构都是空间换时间,因为需要额外的空间来存储数据,进而实现快速查找。

set / multiset

所有元素在插入时都会被自动排序。
底层是 红黑树。

set 不允许容器中有重复元素,而 multiset 允许容器中有重复元素。
所有 set容器 在插入数据时会进行检测,根据返回值可以判断是否插入成功。multiset, unordered_set 插入数据时不会进行检测。

构造和赋值
set<T> s1; 默认构造
set<T> s2(s1); 拷贝构造

set<T> s3 = s1; 赋值

大小和交换
统计容器大小,交换 set 容器

s1.size(); 返回 s1容器中元素的个数
s1.empty(); 判断 s1容器是否为空
s1.swap(s2); 交换两个集合容器

插入和删除数据
s1.insert(elem); 插入元素

  • set, unorderset 可以通过返回值判断插入成功否。返回值是一个对组 pair<set<T>::iterator, bool>,可以通过返回值对组中的第二个值判断插入是否成功。
  • multiset 是允许重复值存储的,所以不会返回插入成功否的标志。

s1.clear(); 清空容器
s1.erase(pos); 删除迭代器 pos 指向元素,返回下一个元素的迭代器
s1.erase(beg, end); 删除区间内所有元素,返回下一个元素迭代器
s1.erase(val); 删除容器中值为 val 的元素

查找和统计
s1.find(val); 查找 val 是否存在。存在返回该元素的迭代器;不存在返回 set.end().
s1.count(val); 返回 val元素的个数

set 容器排序
可以通过 仿函数 来自定义排序规则。默认是从小到大。

class MySort {
public:
	bool operator() (T obj1, T obj2) {
	 	return obj1 > obj2;
	}
};

set<T, MySort> set;
// 此时在在容器中插入数据,就是按照自定义的排序规则进行排序了

map / multimap

map 中所有元素都是 pair. pair 中第一个元素为 key,起到索引的作用;第二个元素是 val 实值。
map 容器可以通过 key 快速找到 value值。

构造和赋值
map<T1, T2> m; 默认构造
map<T1, T2> m2(m); 拷贝构造

map<T1, T2> m3 = m; 赋值

大小和交换
m.size(); 返回容器中元素的个数
m.empty(); 返回bool,判读容器是否为空
m.swap(m1); 交换两个容器

插入和删除
m.insert(pari<T1, T2>(val1, val2)); 返回值和 map 类似

  • map, unordered_map 可以通过返回值判断插入成功否。返回值是一个对组 pair<set<T>::iterator, bool>,可以通过返回值对组中的第二个值判断插入是否成功。
  • multimap 是允许重复值存储的,所以不会返回插入成功否的标志。

m.clear(); 清空容器
m.erase(ite); 删除迭代器 ite所指元素,返回下一个元素的迭代器
m.erase(beg, end); 删除区间元素,返回下一个元素的迭代器
m.erase(key); 删除容器中键为 key 的元素

查找和统计
m.find(key); 查找键 key是否存在。存在,返回其迭代器;不存在,返回 m.end().
m.count(); 统计的是 key 在容器中出现的次数

map 容器排序
和 set一样,也是利用 仿函数 在构造容器的时候改变容器的排序规则。


算法部分

c++ 并没有给标准模板定义太多的操作,上面也做了总结,大体可以分为 容器定义和初始化添加元素删除元素访问元素容器大小操作等。
剩下的 赋值操作 可以变相的理解为是容器的重新初始化,只不过用到了成员函数assign而已;而成员函数 交换操作 是不如直接使用 泛型算法中的swap的。字符串string比较特殊,其成员函数比较多。

而对容器中元素的 查找删除重新排列等操作,c++ 定义了通用的 算法 来实现。

泛型算法 我们可以理解为封装好的一个个函数,有函数就要有参数、返回值。
参数 是 迭代器 或者 谓词(即自定义的操作条件)。迭代器就是提供一个迭代器范围,算法真正操作的也就是迭代器范围内的元素。而谓词需要理解一下。

谓词

算法中有一些是对迭代器范围元素操作时需要满足一定条件,这个条件是自己定义的,定义好之后作为 谓词 传参到算法的参数中作为 操作条件。

根据算法谓词参数部分允许的参数个数,谓词分为 一元谓词二元谓词,即自定义的条件函数中参数的个数。有的算法谓词部分只能是一元谓词,此时如果我们的条件部分需要两个参数才可以实现我们的算法条件,就需要涉及到 lambda表达式

lambda表达式 可以被看作是一个谓词参数而解决上述问题。通过直接捕获局部变量而避免需要定义函数时传多个参数的问题。
但在写程序时写一个代码需要考虑适用适用场景,对于 相同的操作使用在多个地方\一个操作需要多条语句\lambda的捕获列表为空,通常使用函数更好。 而那种只在一两个地方使用的简单操作\对于捕捉局部变量的lambda需要捕获谓词所在定义域的局部变量的 lambda,用函数来替换就不是很容易。

[capture list] (parameter list) -> return type { function body }

auto f = [] { return 0; };   //可以忽略参数列表和返回类型,但必须有捕获列表和函数体

//例子
stable_sort(words.begin(), words.end(), [] (const string &a, const string &b) {
	return a.size() < b.size();}
	);    //排序时会将 迭代器范围内的元素按照 lambda表达式 定义的方式操作。

标准库 bind 函数
对于想要直接把自定义函数作为谓词而存在的话,可以使用 bind函数,通过传参自定义函数,对自定义函数参数的 占位或者给定默认值 处理,将 bind函数看作是一个谓词而存在。

auto newCallable = bind(callable, arg_list);

//例子
ostream &print(ostream &os, const string &s, char c) {
	return os << s << c; 
}
for_each( words.begin(), words.end(), bind(print, ref(os), _1, ' ') );    //ref() 绑定引用参数

总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值