c++面经整理(四)STL

 1 面向对象编程和泛型编程

OOP将datas和methods关联在一起

GP将datas和methods分开来 

 2 操作符重载和模板泛化

2.1 操作符重载

十个 C++ 运算符重载示例

 2.2 泛化,全特化,偏特化

template<typename T,typename C>
struct A
{
    A(){cout<<"泛化版本构造函数"<<endl;}
    void func()
    {
        cout<<"泛化版本"<<endl;    
    }
};

//特化
template<>
struct A<int,int>
{
   A(){cout<<"int,int特化版本构造函数"<<endl;}
   void func()
   {
       cout<<"int,int特化版本"<<endl;    
   } 
};
 
template<>
struct A<double,double>
{
   A(){cout<<"double,double特化版本构造函数"<<endl;}
   void func()
   {
       cout<<"double,double特化版本"<<endl;    
   } 
};


int main()
{
    A<int,double> a;//调用泛化构造函数
    a.func();//调用泛化版本函数
    A<int,int> a;//调用int,int特化构造函数
    a.func();//调用int,int特化版本函数
    A<double,double> a;//调用double,double特化构造函数
    a.func();//调用double,double特化版本函数
}

 template<>中为空,代表所有类型都在下面特殊化处理,上面相当于对int,int和double,double两种类型进行了分别的处理,其他类型依然是泛化版本。

对类中的某个成员函数进行特化处理

还是以上面给的例子为基础,特化func()成员函数,当A的模板参数为<int,double>时,调用特化版的func()。

template<>
void A<int,double>::func
{
    cout<<"int,double特化版本函数"<<endl;
}
 
int main()
{
    A<int,double> a;//调用泛化版本构造函数
    a.func();//调用int,double特化版本函数
}

类模板的偏特化

类模板偏特化(局部特化):顾名思义,只特殊化几个参数或者一定的参数范围

template<typename T,typename C,typename D>
struct A
{
	void func()
	{
		cout << "泛化版本" << endl;
	}
};
 
template<typename C>
struct A<int,C,int>
{
	void func()
	{
		cout << "int,C,int偏特化版本" << endl;
	}
};
 
template<typename C>
struct A<double, C, double>
{
	void func()
	{
		cout << "double,C,double偏特化版本" << endl;
	}
};
int main()
{
	A<double, int, int> a;
	a.func();//调用泛化版本
	A<int, char, int> b;
	b.func();//调用int,C,int偏特化版本
	A<double, int, double> c;
	c.func();//调用double,C,double偏特化版本
}

范围偏特化

注意范围二字,比如const int属于int的一个小范围,int *和const int*属于int的一个小范围,int&属于int的一个小范围,int&&属于int的一个小范围

template<typename T>
struct A
{
	void func()
	{
		cout << "泛化版本" << endl;
	}
};
 
template<typename T>
struct A<const T>
{
	void func()
	{
		cout << "const T版本" << endl;
	}
};
 
template<typename T>
struct A<T*>
{
	void func()
	{
		cout << "T*版本" << endl;
	}
};
 
template<typename T>
struct A<T&>
{
	void func()
	{
		cout << "T&版本" << endl;
	}
};
 
template<typename T>
struct A<T&&>
{
	void func()
	{
		cout << "T&&版本" << endl;
	}
};
int main()
{
	A<int> a;
	a.func();
	A<const int> b;
	b.func();
	A<int *> c;
	c.func();
	A<const int *> d;
	d.func();
	A<int&> e;
	e.func();
	A<int&&> f;
	f.func();
}

3 分配器allocator

分配器(配置器,allocator)是STL中六大组件(容器、算法、迭代器、仿函数、适配器、分配器)之一,用于分配管理内存空间。其实我们可以把allocator看成一个简易的内存池,其主要适用于在使用容器时,对内存空间的动态分配,如果是我们平常要申请一块动态内存时,不推荐使用allocator,应该使用new-delete(malloc-free),主要原因是allocator不好用(使用不方便,容器例外),在内存释放的时候还需要提供对象的个数,因为我们在动态分配内存时候基本上都是对指针所指向的内存空间进行操作,而不会去记录空间中构造了多少个对象。

    allocator的特点是将内存分配(allocate)与对象构造(construct)分离,这样使得对内存的管理更加灵活,性能较高。

3.1 简单的使用

#include <iostream>
#include <memory>
using namespace std;
//先熟悉一下提供的allocator用法
int main(int argc, char const *argv[])
{
    allocator<int> a;
    int *ptr=a.allocate(5);
    a.construct(ptr,3);
    a.construct(ptr+1,-3);
    a.construct(ptr+2,3);
    a.construct(ptr+3,-3);
    a.construct(ptr+4,3);
    for(int i=0;i<5;i++)
    {
        cout<<*(ptr+i)<<" ";
        a.destroy(ptr+i);
    }
    a.deallocate(ptr,5);
    return 0;
}

一个分配器中一定要有allocate、deallocate、construct、destroy四个函数,分别表示内存分配,内存释放、对象构造、对象析构。在一般的allocator中allocate直接是调用::operator new(),deallocate直接调用::operator delete(),没有什么性能上的优化。

4 list

4.1 list的介绍

list是序列容器,允许在序列中的任何位置执行固定O(1)时间复杂度的插入和删除操作,并在两个方向进行迭代。
list容器使用双链表实现;双链表将每个元素存储在不同的位置,每个节点通过next,prev指针链接成顺序表。
list与其他标准序列容器(array,vector和deque)相比,list通常可以在容器内的任何位置插入、提取和移动元素。
list与其他标准序列容器(array,vector和deque)相比,list和forward_list(单链表实现)的主要缺点是他们不能通过位置直接访问元素;例如,要访问列表中的第五个元素,必须从已知位置(开始或结束)迭代到该位置,需要哦线性时间开销。
存储密度低,list要使用一些额外的内容空间(next,prev)来保持与每个元素相关联(前后续的线性)的链接信息,从而导致存储小元素类型(如char,short,int等)的列表的存储密度低。

4.2 构造函数

#include <iostream>
#include <list>

using namespace std;

template<class T>
void Print(const list<T>& my)
{
	typename list<T>::const_iterator it = my.begin();
	for (; it != my.end(); it++)
	{
		cout << *it << "\t";
	}

	cout << endl;
}

int main()
{
	list<int> list1 = { 12,23,34 };
	list<int> list2(3, 11);
	list<int> list3(list2);

	list<string> list4 = { "This","is","windows" };
	list<string> list5;
	list<string> list6;

	list5 = list4;

	list6.assign(3, "This");

	Print(list1);
	Print(list2);
	Print(list3);
	Print(list4);
	Print(list5);
	Print(list6);

	return 0;
}

output:
12      23      34
11      11      11
11      11      11
This    is      windows
This    is      windows
This    This    This

 4.3 常用API操作

#include <iostream>
#include <list>

using namespace std;

template<class T>
void Print(const list<T>& my)
{
	typename list<T>::const_iterator it = my.begin();
	for (; it != my.end(); it++)
	{
		cout << *it << "\t";
	}

	cout << endl;
}

int main()
{
	list<int> list1 = { 12,23,34 };
	list<int> list2 = { 1,2,3,4,5 };
	
	Print(list1);
	Print(list2);
	auto it = list1.begin();
	list1.insert(it, 55);
	Print(list1);
	it++;
	list1.insert(it, 3, 55);
	Print(list1);

	list1.erase(it);
	Print(list1);

	list1.swap(list2);
	Print(list1);
	Print(list2);


	return 0;
}
output:
12      23      34
1       2       3       4       5
55      12      23      34
55      12      55      55      55      23      34
55      12      55      55      55      34
1       2       3       4       5
55      12      55      55      55      34

5 vector

5.1  vector 介绍

vector的数据安排以及操作方式,与array非常相似,两者的唯一差别在于空间的运用的灵活性。

Array是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间,可以,一切琐碎得由自己来,首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间

Vector是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。因此vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必害怕空间不足而一开始就要求一个大块头的array了。

Vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率,一旦vector旧空间满了,如果客户每新增一个元素,vector内部只是扩充一个元素的空间,实为不智,因为所谓的扩充空间(不论多大),一如刚所说,是”配置新空间-数据移动-释放旧空间”的大工程,时间成本很高,应该加入某种未雨绸缪的考虑,稍后我们便可以看到vector的空间配置策略。

为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求大一些,以备将来可能的扩充,这边是容量的概念。换句话说,一个vector的容量永远大于或等于其大小,一旦容量等于大小,便是满载,下次再有新增元素,整个vector容器就得另觅居所。

所谓动态增加大小,并不是在原空间之后续接新空间(因为无法保证原空间之后尚有可配置的空间),而是一块更大的内存空间,然后将原数据拷贝新空间,并释放原空间。因此,对vector的任何操作,一旦引起空间的重新配置,指向原vector的所有迭代器就都失效了。这是程序员容易犯的一个错误,务必小心。

5.2 构造函数

vector<T> v; //采用模板实现类实现,默认构造函数
vector(v.begin(), v.end());//将v[begin(), end())区间中的元素拷贝给本身。
vector(n, elem);//构造函数将n个elem拷贝给本身。
vector(const vector &vec);//拷贝构造函数。

5.3 常用API操作

at(int idx); //返回索引idx所指的数据,如果idx越界,抛出out_of_range异常。
operator[];//返回索引idx所指的数据,越界时,运行直接报错
front();//返回容器中第一个数据元素
back();//返回容器中最后一个数据元素 


insert(const_iterator pos, int count,ele);//迭代器指向位置pos插入count个元素ele.
push_back(ele); //尾部插入元素ele
pop_back();//删除最后一个元素
erase(const_iterator start, const_iterator end);//删除迭代器从start到end之间的元素
erase(const_iterator pos);//删除迭代器指向的元素
clear();//删除容器中所有元素


size();//返回容器中元素的个数
empty();//判断容器是否为空
resize(int num);//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
resize(int num, elem);//重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长>度的元素被删除。
capacity();//容器的容量
reserve(int len);//容器预留len个元素长度,预留位置不初始化,元素不可访问。

6 deque

6.1 deque 介绍

Vector容器是单向开口的连续内存空间,deque则是一种双向开口的连续线性空间。

所谓的双向开口,意思是可以在头尾两端分别做元素的插入和删除操作,当然,vector容器也可以在头尾两端插入元素,但是在其头部操作效率奇差,无法被接受。

Deque容器和vector容器最大的差异,一在于deque允许使用常数项时间对头端进行元素的插入和删除操作。二在于deque没有容量的概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,换句话说,像vector那样,”旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间”这样的事情在deque身上是不会发生的。也因此,deque没有必须要提供所谓的空间保留(reserve)功能.

Deque容器是连续的空间,至少逻辑上看来如此,连续现行空间总是令我们联想到array和vector,array无法成长,vector虽可成长,却只能向尾端成长,而且其成长其实是一个假象,事实上(1) 申请更大空间 (2)原数据复制新空间 (3)释放原空间 三步骤,如果不是vector每次配置新的空间时都留有余裕,其成长假象所带来的代价是非常昂贵的。

Deque是由一段一段的定量的连续空间构成。一旦有必要在deque前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在deque的头端或者尾端。Deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。

既然deque是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。Deque代码的实现远比vector或list都多得多。

Deque采取一块所谓的map(注意,不是STL的map容器)作为主控,这里所谓的map是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque的存储空间的主体。

6.2 构造函数

deque<T> deqT;//默认构造形式
deque(beg, end);//构造函数将[beg, end)区间中的元素拷贝给本身。
deque(n, elem);//构造函数将n个elem拷贝给本身。
deque(const deque &deq);//拷贝构造函数。

 6.3 常用API操作

deque赋值操作
assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将n个elem拷贝赋值给本身。
deque& operator=(const deque &deq); //重载等号操作符 
swap(deq);// 将deq与本身的元素互换

deque大小操作
deque.size();//返回容器中元素的个数
deque.empty();//判断容器是否为空
deque.resize(num);//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
deque.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置,如果容器变短,则末尾超出容器长度的元素被删除。

deque双端插入和删除操作
push_back(elem);//在容器尾部添加一个数据
push_front(elem);//在容器头部插入一个数据
pop_back();//删除容器最后一个数据
pop_front();//删除容器第一个数据

deque数据存取
at(idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range。
operator[];//返回索引idx所指的数据,如果idx越界,不抛出异常,直接出错。
front();//返回第一个数据。
back();//返回最后一个数据

deque插入操作
insert(pos,elem);//在pos位置插入一个elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。

deque删除操作
clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除pos位置的数据,返回下一个数据的位置。

7 stack

7.1 stack介绍

stack是一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口,形式如图所示。stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。
有元素推入栈的操作称为:push,将元素推出stack的操作称为pop.

 7.2 构造函数

stack<T> stkT;//stack采用模板类实现, stack对象的默认构造形式: 
stack(const stack &stk);//拷贝构造函数

7.3 常用API操作

stack赋值操作

stack& operator=(const stack &stk);//重载等号操作符

stack数据存取操作
push(elem);//向栈顶添加元素
pop();//从栈顶移除第一个元素
top();//返回栈顶元素


stack大小操作
empty();//判断堆栈是否为空
size();//返回堆栈的大小

8 queue

8.1 queue 介绍

Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。

8.2 构造函数

queue<T> queT;//queue采用模板类实现,queue对象的默认构造形式:
queue(const queue &que);//拷贝构造函数

8.3 常用API操作

queue存取、插入和删除操作
push(elem);//往队尾添加元素
pop();//从队头移除第一个元素
back();//返回最后一个元素
front();//返回第一个元素


queue赋值操作
queue& operator=(const queue &que);//重载等号操作符

queue大小操作
empty();//判断队列是否为空
size();//返回队列的大小

9 红黑树

9.1 红黑树介绍

详细可以看看

红黑树是一种自平衡的二叉查找树,是一种高效的查找树。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。红黑树是一个二叉搜索树,它在每个节点增加了一个存储位记录节点的颜色,可以是RED,也可以是BLACK;通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍)

它同时满足以下特性:

1 节点是红色或黑色
2 根是黑色
3 叶子节点(外部节点,空节点)都是黑色,这里的叶子节点指的是最底层的空节点(外部节点),下图中的那些null节点才是叶子节点,null节点的父节点在红黑树里不将其看作叶子节点
4 红色节点的子节点都是黑色
        a 红色节点的父节点都是黑色
        b 从根节点到叶子节点的所有路径上不能有 2 个连续的红色节点
5 从任一节点到叶子节点的所有路径都包含相同数目的黑色节点

10 set/multiset

10.1 set/multiset容器介绍

Set的特性是。所有元素都会根据元素的键值自动被排序。Set的元素不像map那样可以同时拥有实值和键值,set的元素即是键值又是实值。Set不允许两个元素有相同的键值。

我们不可以通过set的迭代器改变set元素的值,因为set元素值就是其键值,关系到set元素的排序规则。如果任意改变set元素值,会严重破坏set组织。换句话说,set的iterator是一种const_iterator.

set拥有和list某些相同的性质,当对容器中的元素进行插入操作或者删除操作的时候,操作之前所有的迭代器,在操作完成之后依然有效,被删除的那个元素的迭代器必然是一个例外。

multiset容器基本概念
multiset特性及用法和set完全相同,唯一的差别在于它允许键值重复。set和multiset的底层实现是红黑树。

10.2 构造函数

set<T> st;//set默认构造函数:
mulitset<T> mst; //multiset默认构造函数: 
set(const set &st);//拷贝构造函数

10.3 常用API操作

set赋值操作
set& operator=(const set &st);//重载等号操作符
swap(st);//交换两个集合容器


set大小操作
size();//返回容器中元素的数目
empty();//判断容器是否为空


set插入和删除操作
insert(elem);//在容器中插入元素。
clear();//清除所有元素
erase(pos);//删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg, end);//删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(elem);//删除容器中值为elem的元素。


set查找操作
find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end();
count(key);//查找键key的元素个数 这个数字只能是0(没有找到)或1(找到了)
lower_bound(keyElem);//返回第一个key>=keyElem元素的迭代器。
upper_bound(keyElem);//返回第一个key>keyElem元素的迭代器。
equal_range(keyElem);//返回容器中key与keyElem相等的上下限的两个迭代器。

11 map/multimap

11.1 map/multimap介绍

Map的特性是,所有元素都会根据元素的键值自动排序。

Map所有的元素都是pair,同时拥有实值和键值,pair的第一元素被视为键值,第二元素被视为实值,map不允许两个元素有相同的键值。

我们不可以通过map的迭代器改变map的键值, 因为map的键值关系到map元素的排列规则,任意改变map键值将会严重破坏map组织。如果想要修改元素的实值,那么是可以的。

Map和list拥有相同的某些性质,当对它的容器元素进行新增操作或者删除操作时,操作之前的所有迭代器,在操作完成之后依然有效,当然被删除的那个元素的迭代器必然是个例外。

Multimap和map的操作类似,唯一区别multimap键值可重复。Map和multimap都是以红黑树为底层实现机制。

11.2 构造函数

map<T1, T2> mapTT;//map默认构造函数: 
map(const map &mp);//拷贝构造函数

11.3 常用API操作 

map赋值操作
map& operator=(const map &mp);//重载等号操作符
swap(mp);//交换两个集合容器


map大小操作
size();//返回容器中元素的数目
empty();//判断容器是否为空


map插入数据元素操作
map.insert(...); //往容器插入元素,返回pair<iterator,bool>
map<int, string> mapStu;
// 第一种 通过pair的方式插入对象
mapStu.insert(pair<int, string>(3, "小张"));
// 第二种 通过pair的方式插入对象
mapStu.inset(make_pair(-1, "校长"));
// 第三种 通过value_type的方式插入对象
mapStu.insert(map<int, string>::value_type(1, "小李"));
// 第四种 通过数组的方式插入值
mapStu[3] = "小刘";
mapStu[5] = "小王";


map删除操作
clear();//删除所有元素
erase(pos);//删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg,end);//删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(keyElem);//删除容器中key为keyElem的对组。


map查找操作
find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;/若不存在,返回map.end();
count(keyElem);//返回容器中key为keyElem的对组个数。对map来说,要么是0,要么是1。对multimap来说,值可能大于1。
lower_bound(keyElem);//返回第一个key>=keyElem元素的迭代器。
upper_bound(keyElem);//返回第一个key>keyElem元素的迭代器。
equal_range(keyElem);//返回容器中key与keyElem相等的上下限的两个迭代器。

12 hashTable

12.1 hashTable介绍

hashtable 的目的是为了提供任何操作都是常数级别。SGI STL 中, hash table 使用了 开链法 实现的。大致的意思如下图所示:

hash table 内的元素为 桶子(bucket),每个桶子里面有很多节点。其实有点像我们前面整理的 deque(双端队列),双端队列主控是个指向指针的指针,而hash table是一个vector;双端队列缓冲区是一块连续内存,像是array,而hash table 则是一个类似的单向链表。

解决哈希冲突的方式?

  1. 线性探查。该元素的哈希值对应的桶不能存放元素时,循序往后一一查找,直到找到一个空桶为止,在查找时也一样,当哈希值对应位置上的元素与所要寻找的元素不同时,就往后一一查找,直到找到吻合的元素,或者空桶。

  2. 二次探查。该元素的哈希值对应的桶不能存放元素时,就往后寻找1^2,2^2,3^2,4^2.....i^2个位置。

  3. 双散列函数法。当第一个散列函数发生冲突的时候,使用第二个散列函数进行哈希,作为步长。

  4. 开链法。在每一个桶中维护一个链表,由元素哈希值寻找到这个桶,然后将元素插入到对应的链表中,STL的hashtable就是采用这种实现方式。

  5. 建立公共溢出区。当发生冲突时,将所有冲突的数据放在公共溢出区

13 unordered_map系列

13.1 unordered_map

以unordered_map为例

运行效率方面:unordered_map最高,而map效率较低但 提供了稳定效率和有序的序列。

占用内存方面:map内存占用略低,unordered_map内存占用略高,而且是线性成比例的。

需要无序容器,快速查找删除,不担心略高的内存时用unordered_map;有序容器稳定查找删除效率,内存很在意时候用map。

总之:非频繁的查询用map比较稳定;频繁的查询用hash_map效率会高一些

14 priority_queue

14.1 priority_queue介绍 

普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。
在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest
out)的行为特征。
首先要包含头文件#include, 他和queue不同的就在于我们可以自定义其中数据的优先级,让优先级高的排在队列前面,优先出队。

14.2 priority_queue构造函数

默认优先输出大数据:大根堆

大根堆就是把大的元素放在堆顶的堆。优先队列默认实现的就是大根堆,所以大根堆的声明不需要其他的,直接按C++STL的声明规则声明即可。
#include<queue>
priority_queue<int> q;
priority_queue<string> q;
priority_queue<pair<int,int> > q;

优先输出小数据 :小根堆

priority_queue<int,vector<int>,greater<int> >q;

14.3 常用API操作

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值