通用容器

 

通用容器
STL 是一个容器集,容器又是对象的集合(它里面持有对象)
1.      容器和迭代器
容器可以根据它里面对象的需要自行扩展,我们在使用容器时,不需要知道它里面要放多少个对象,也不需要知道容器里的处理细节,只需要定义一个容器对象,然后由容器来处理全部细节。不同的容器它们的接口类型和外部行为有所不同,它们处理相同操作所耗费的效率也不同。譬如: vector 遍历元素或访问元素很方便,但是要在其中插入元素就很耗效率;而使用 list 要插入一个元素就很方便,但随机访问一个元素却比 vector 效率低。这些操作依赖于这些序列的底层结构。不同容器的关键不同之处在于它们在内存中存储对象的方式和向用户提供的操作。当我们使用时可以根据不同的需进行选择。
为了灵活的访问容器中的元素,引入迭代器 (iterator) 。迭代器是为了实现通用而做的抽象。使用迭代器可以返回迭代器所指的元素给迭代器的使用者。它可以灵活的遍历容器中的每一个元素。也容许同时存在多重迭代器。通过迭代器,容器可以被看作一个序列。迭代器操作与容器的操作是分离开的,任何一方的变动都不会影响到另一方。通过迭代器可以读取元素,也可以给元素赋值(改变元素值)。
2.      概述
容器分为三大类,每一类有分为几个类型:

分类
容器
序列容器
vector, list, deque
适配器容器
queue, stack, priority_queue
关联式容器
set, map, multiset, multimap

序列容器只是将它们的元素线性地组织起来,是最基本的容器类型。
适配器容器可以在序列容器需要的时候为它们附上某些特殊的属性(如:队列或栈的抽象建立模型)。
关联式容器则是基于关键字来组织它们的数据,并允许快速的检索那些数据。
vector list deque 的区别 :
1)         vector 是一个允许快速随机访问其中元素的线性序列,它随机访问元素的速度快,但是向其中插入元素的速度慢,不支持 push_front() 操作;
list
是一个双向链表,要移动它的元素所需花费的代价很高,但是很容易向其中任何地方插入元素;
deque
是一个双端队列,也可以用几乎和 vector 一样快 ( 但是会比 vector 稍慢 ) 的速度随机访问其中的元素,但是它扩展资源的速度要快很多,而且可以很容易的在它两端加入新元素。
2)         vector deque 允许使用检索操作符 [   ] ,但 list 不允许。
3)         它们都提供了相应的类型的迭代器来访问它的元素。
set 里面存入的是不重复的元素,且放入这些元素时会自动对它们排序。
容器中所持有的是存入对象的拷贝,并根据需要扩展它们的资源。所以这些对象必须有可访问的拷贝构造函数和可访问的赋值操作符。对于在容器里所存对象堆空间的分配管理需要用户自己做。譬如:当容器所持有的对象被销毁之后,容器并不会自动 destroy 它所持有的相应指,而需用户自己去做。对于解决这种问题,最容易和最安全的方法就是使用智能指针 (smart pointer)
同一个对象,可以有不至一个容器里的指针指向它。
1) 字符串容器
2) STL容器继承
继承自容器的类的对象 也具有那个容器的属性和行为。
3.      更多迭代器
<ContainerType>::iterator;
<ContainerType>::const_iterator
如果一个容器是 const (常)容器,那么它的迭代器也是 const( ) 迭代器,也就是不允许更换这些迭代器所指向的元素(因为相应的运算符都是 const 的)。
所有迭代器都++可以前向移动,也可以使用 != == 对迭代器进行比较。
可以通过使用解析运算符 (operator *) 来取得迭代器当前所指向的容器元素。
如果 it 是一个可遍历容器的迭代器,且 f() 是该容器所持有的对象的成员函数,那么可以用 (*it).f() it->f() 访问( ( 容器所包含的类型的 ) 对象的)这个成员函数。但是要注意如下用法:
template <class Cont, class PtrMemFun>
void apply(Cont& c, PtrMemFun f) {
 typename Cont::iterator it = c.begin();
 while(it != c.end())
 {
  //(it->*f)();// 将当成it使用操作符->* ,但是在迭代器类中并没有提供->* 这个操作符。
  //(it->(*f))();//error C2039: 'it' : is not a member of 'Z'
      ((*it).*f)(); // Alternate form
  
      ++it;
 }
}
1) 可逆容器中的迭代器
可逆容器的迭代器可用来反向遍历容器,有成员函数 rbegin() 用于产生一个选择了容器末尾的迭代器 reverse_iterator rend() 用于产生一个选择了容器超越起始的迭代器 reverse_iterator 。对所有的容器调用 rbegin() rend() 可得到 reverse_iterator 对象。如果容器是 const 的,那么 rbegin() rend() 产生的迭代器是 const_reverse_iterator
2) 迭代器的种类
为什么要对迭代器划分种类?因为:一使用某些内置的迭代器类型或创建自己的迭代器时,迭代器的种类就很重要;二使用 STL 算法时,每种算法对其迭代器都有使用场合的要求。在创建用户自己的可重用的算法模板时,这些种类知识尤其重要,因为自定义的算法所需要的迭代器种类决定了该算法的灵活性。如果只要求最基本的迭代器类型(输入或输出迭代器),则这种算法可以适用于任何场合(如 copy() )。
一个迭代器类的种类由一个迭代器的层次结构标记类进行标识,类名和迭代器的种类相符合,且它们之前的派生层次结构反映了它们之间的关系:
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag{} : public input_iterator_tag{};
struct bidirectional _iterator_tag{} : public forward_iterator_tag{};
struct random_access_iterator_tag{} : struct bidirectional_iterator_tag{};
( 注:因为前向迭代器 forward_iterator 需要使用超越末尾的迭代器值,而输出迭代器 output_iterator 总是假定它总是可以解析的,可是保证不把一个超越末尾的迭代器值传给一个需要使输出迭代器的算法中是很重要的,所以, forward_iterator 只从 input_iterator 继承而不继承 output_iterator.)
为了提高效率,有些算法为不同的迭代器提代不同的实现。
下面按行为功能由严格到最强大的顺序来分别讨论看看具体划分为哪些种类以及它们分别有什么性能:
输入迭代器: 只读,一次传递
可以对读操作的结果进行解析(一个值只解析一次),然后前向移动,可以用与超越末尾的值比较判断是否结束。为输入迭代器的预定义实现只有 istream_iterator istreambuf_iterator ,用于从一个输入流 istream 中读取。
输出迭代器: 只写,一次传递
可以对写操作的结果进行解析(一个值只解析一次),然后前向移动,没有使用超越末尾来的值结束的概念。为输出迭代器的预定义实现只有 ostream_iterator ostreambuf_iterator ,用于从一个输出流 ostream 中读取。
前向迭代器: 多次读 /
前向迭代器包括输入 / 输出迭代器的所有功能,不同的是它可以对一个迭代器所指定位置多次解析(也就是可以对迭代器所指向的值多次读写)。它也是只能前向移动。没有专为前向迭代器预定义的迭代器。
双向迭代器: operator--
它具有前向迭代器的所有功能,另外还具有后向移动的功能。由 list 返回的迭代器都是双向的。
随机访问迭代器: 类似于一个指针
它具有双向迭代器的所有工轻盈,再加上一个指针所具有的功能,除了不具有一种空 (null) 迭代器和空指针对应。它可以像一个指针一样进行任何操作,可以使用 operator[ ] 进行索引,可以加若干值指向另一位置(向前 / 后移动),可以运用比较操作符在迭代器之间进行比较。
3) 预定义迭代器
front_inserter() 用一个容器对象作参数,产生一个调用 push_front 进行赋值的迭代器
back_inserter() 用一个容器对象作参数,产生一个调用 push_back 进行赋值的迭代器 .
inserter() 是插入而不是压入一个元素,再次替代 operator=, 它在插入之前需要知道要插入位置的迭代器
如: int a[] = { 1, 3, 5, 7, 11, 13, 17, 19, 23 };
deque<int> di;
vector<int> vi;// 不支持 push_front push_back.
copy(a, a + sizeof(a)/sizeof(deque), front_inserter (di));   //di{ 23, 19,17, 13, 11, 7, 5, 3, 1}
di.clear();// 清空 di
copy(a, a + sizeof(a)/sizeof(deque),back_inserter(di));    //di{1, 3, 5, 7, 11, 13, 17, 19, 23 }
copy(a,a + sizeof(a)/(sizeof(Cont::value_type)*2),inserter(ci,it));   //di{1,3,5,1,3,5,7,7,11,13,17,19,23}
l     更多的流迭代器
输入流迭代器 istream_iterator 有两个构造函数,一个获得输入流 istream 并且产生一个实际读取的迭代器对象,另一个是默认构造函数,用于产生一个作为超越末尾标记的迭代器对象。
输入流迭代器会丢失一些空格、回车、 tab 之类的空白字符,所以平时使用更多的是输入输出流缓冲迭代器,除非你不用考虑空白字符是否会丢失的问题。另外, istream::operator>> 每次操作会增加较多开销。
l     操作从未初始化的存储区
raw_storage_iterator <tn1, tn2> 是一个输出迭代器,可以用来操作操作未初始化的存储区。
它有两个模板参数: tn1 是输出迭代器类型, tn2 是所存储对象类型,输出迭代器指向这块未初始化的存储区。它提供的算法使结果存放到未经初始化的内存区。
接口:它的构造函数有一个指向原始(某未初始化)内存储区的迭代器(典型的指针), operator= 将一个对象分配给那个未初始化的原始内存。
注意:原始内存区类型必须与所创建的对象类型相同(可以在创建时进行强制类型转换)。必须显示调用析构函数进行清理工作,也允许在操作容器期间每次删除一个对象。 Delete 表达式中的静态指针类型必须与 new 表达式中分配的类型相同。
4.      基本序列容器:vector、list和deque
所有基本序列容器都是完全按照存入去时的顺序持有对象。但是,不同的容器进行某些操作时的效率是不同,所以,我们在使用时要根据不同的操作需要选择合适的容器。
1) 基本序列容器的操作概述
都是可逆容器。
有多个构造函数,用默认构造函数创建的容器对象是空的;
可以使用赋值成员函数 operator= 和两种类型的 assign() assign(it1,it2); assign(value_count,value); )对容器赋值;
可以用 resize() 重置容器大小;
可以用 insert() 在容器中插入一个或一组元素;
可以使用 resverse_iterator 反向读取容器中的元素;
可以用 erase() 的两个不同版本来清除序更中间的一个元素或一组元素;
可以用 clear() 来清空容器。
可以用++或——把 iterator 前向或后向移动一个位置,因为 vector deque 可以产生随机访问迭代器,所以它们还可以使用 operator+ operator- 来一次移多个位置,但 list 就只能一次移一个位置。
它们三个都支持 push_back() pop_back() list deque 还支持 push_front() pop_front() ,但 vector 不支持 push_front() pop_front()
成员函数 swap() 可以相互交换两个 ( 持有相同类型对象的 ) 容器的所有东西,也就高效的交换了容器本身。还有可用于交换两元素的非成员函数 swap() ,还有用于通于迭代器交换同一容器内两个元素的 iter_swap 算法。
下面将分别具体讨各类型序列容器的特点。
2) 向量(vector)
vector 模板有点像数组,具有数组风格的快速索引,还可以动态地扩展。它可以快速索引和迭代,也可以在最后一个元素之后新增加元素,但不能在中间或前面插入元素。
l     已分配存储区溢出引起的问题
当新增元素而存储空间不够时,就会 (1) 在其它位置重新分配更大的新存储空间, (2) 用拷贝构造函数把原位置的所有元素都拷贝到新的存储空间中, (3) 调用析构函数把原位置的元素销毁, (4) 释放原位置的内存。这样会引起一些副作用。
可以用 reserve() vector 预先分配足够大的空间,但是它与用 vector 构造函数的第一个参数来指定分配空间大小的做法是有区别的,后者是使用元素类型的默认构造函数来初始化元素 , reserve() 只是预分配空间,但并没调用构造函数初始化,且用 size() 取得值为0,也就是仍是空容器。有些操作会引起迭代器无效,譬如 resize() 。在进行一些迭代器无效的操作之后,如果要使用迭代器,需重新置迭代器。
由此可见,选择使用 “vector” 的最安全的方法是:一次性填入所有元素,然后在程序的另一处只使用它而不再加入新元素。
l     插入和删除元素
使用 vector 最有效的条件是:
1)  在开始时用 reserver() 分配了正确数量的存储构, vector 绝不再重新分配存储区;
2)  仅仅在序列的后端添加或者删除元素。
3) 双端队列(deque
deque vector 的区别就在于:
vector 的存储区必须是连续的,而 deque 的存储区不必是连续的存储块;
deque 也有 vector 的所有操作,比 vector 多了 push_front pop_front 操作;
在随机访问元素时 deque vector 稍慢,但是在插入元素需要重新分配存储区时 deque 不需要复制并销毁原有元素,插入和删除元素 deque 都比 vector 有效率的多。
 
已配置存储区溢出的引起的问题
vector 不同,当 deque 所用存储区不够用时,它会根据当前缺少就只分配多少的一个新存储区,原有的存储区和元素都不变,也就不会有额外的拷贝构造和析构发生,不过它用于保存数据块索引信息的存储区有可能需要重新分配。在 deque 中间插入元素比 vector 更麻烦,但代价不大。
 
因为 deque 的存储管理方式,在 deque 两端添加元素,不会引起现有迭代器失效。
再次说 vector 用于预先知道容器中要存入的元素数目的情况比较合适。
当不确定将被存入容器中的元素数目时,用 deque 优于 vector
只需很小改动即可以把 vector 转用 deque
总之,在以下情况下使用 deque 是最好的:
1)  从序列的两端插入或删除元素;
2)  合理的快速遍历容器元素;
3)  使用 operator[ ] 相当快速的随机访问。
4) 序列容器间的转换
序列容器都有一个由两个迭代器作参数的双参数构造函数和一个将数据读入现存容器中的 assign() 成员函数,用它们来从一个序列容器转换为另一个序列容器是很容易的。它只是把那些对象拷贝构造到新容器中。
5) 被检查的随机访问
vector deque 都提供了两个随机访问函数:索引操作符 operator[ ] 和判定是否到了容器边界的的函数 at() ,是边界则返回 true ,否则返回 false ,超出边界则抛出一个异常。但是用 at() 的代价比 operator[ ] 的代价大。
6) 链表(list)
list 是一个以双向链表数据结构实现的序列容器。
如果有较大、复杂的对象,就首选 list ,特别是如果构造、拷贝、赋值、析构操作的代价大,如果进行大量的譬如对象排序或以别的方式进行重新排列操作是更是这样。
list 可以在序列的任何地方快速的插入或删除元素,但是代价较高,所以适插入或删除较大对象的元素;没有 operator[ ] ,所以它随机访问元素非常慢,适合的情况就是从头到尾或从尾到头顺序遍历元素而不是随机访问一个中间元素,但还是很慢。除非已知要访问元素附近的一个迭代器,否则它的遍历都是从头或尾开始。
list 容器中的元素在创建以后绝对不会移动,即使插入或删除一个元素(这时只会改变它的链接关系),也不会拷贝构造或向某个实际对象赋值,所以向 list 中添加元素,已有迭代器不会失效。
对于只需要改变链接而不移动对象的操作(如逆转、排序),不会进行拷贝对象。 Swap 就是通过拷贝来进行两个元素的交换。
一般来说如果系统提供了一个算法的成员版本就采用它的成员版本而不用其等价的通用的算法。通用的 sort() reverse() 算法只适用于数组、 vector deque
l     特殊的 list 操作
list1. splice (it,list2);// 把整个 list2 插入到 list1 it 位置处,并删除 list2 源链表中所有对象元素
list1 .splice (it1,list2,it2);// list2 it2 处的元素插入到 list1 it , 同时删除源链表中该元素的值
list1. splice (it1,list2,it2,it3);// list2 中的 it2 it3 之间 ([it2,it3]) 的元素插入到 list1 it1
remove (it)        // 删除 list 中所有与 it 处元素等值的元素 , 删除操作链表不必排序
list. merge (list1);// list list1 合并并排序,合并后将 list1 源链表已被删除 ( 因为它们已移到新链表中 ) ,合并前要先分别对它们用 sort() 排序
unique ()   // 删除 list 中相邻的重复的元素,须先对 list sort 排序然后再 unique() 才有效
l     链表与集合
执行 list unique 操作之后, list 变成了一个 set
也就是 list unique() set() 都可以保证容器中的元素不重复,但 set() 更高效一些
7) 交换序列
成员函数 swap() 用于同类型序列的相互交换。它是高效率的。它的执行不需进行拷贝和赋值,不论交换的两同类型的序列长度是否相等。实际上它们执行的时它们两个资源的相互交换。 STL 也包括一个 swap() 算法,它用在两相同类型的容器交换时,它具有很快速的性能。所以,对容器的容器用 sort 算法排序,也是很快速的。
5.      集合set
set 只接受每个元素的一个副本。 set 中的元素是用 operator< 进行排序的。想创建一个容器放入元素时就自动排序,用 set 是很好的选择。 Set find() 也很快速,比通用的 find() 算法要快很多,因为对已排好序的序列容器在查找元素时,用 equal_range() 就可以得到对数级的算法复杂性。
6.      堆栈stack
堆栈 stack queue priority_queue 一起被归类为适配器容器。它们通过调整一个基本序列容器以存储自己的数据。
Stack 类的 pop() 并不返回栈顶元素,而是返回一个 void 值。如果要取得栈顶元素,可以用 top() 来得一个它的引用。 Stack 没有提供迭代器,也没有初始化形式,只提供了一个简单的接口。
7.      队列 queue
队列也是一个适配器容器,也是建立在一个基本序列容器之上,它的默认模板参数是 deque deque queue 的理想的实现。它是受限的 deque ,它只能在一端插入元素,在另一端删除元素。在需要使用 queue 的任何地方使用 deque ,那样也可以使用 deque 的附加功能。当强调只使用 queue 相似的行为的时候,使用 queue 而不用 deque
8.      优先队列 priority_queue
优先队列也是一个适配器容器,也是基于基本序列容器进行构建的适配器,默认的序列容器是 vector
优先队列拥有与 stack 几乎相同的接口,但是这的表现不同。优先队列的栈顶元素是具有优先级最高的元素。
不能在一个 priority_queue 上从头到尾迭代,但可以用一个 vector 来模拟 priority_queue 的行为,因此允许访问那个 vector
它使用的函数有: make_heap() push_heap() pop_heap()
可以说,堆就是一个优先队列, priority_queue 是对堆的一个封装。
make_heap() & sort_heap()
9.      持有二进制位
表示二进制的两个类: bitset vector<bool> 。它们都不是传统的“ STL 容器”。
bitset vector<bool> 的区别:
1)  bitset 是持有固定数目的二进制位; vector<bool> 持有的二进制位数目可以扩展。
2)  bitset 模板是为了在操纵二进制位时提高性能而设计的,它不是正常的 STL 容器,没有迭代器,它允许底层的整型数组存储在运行时的栈上,它有一个面向二进制位层次的操作接口,绝不与前面所讨论的 STL 容器相似。 vector<bool> vector 容器的一个特化,它的设计是用来提高 bool 数据的空间使用率,有普通 vector 的所有操作。
1) bitset<n>
一些相关操作:      
      to_ulong 把二进制位转化成 usigned long 数字
      cbits(string)
      cbits(string,int)
      cbits(string,int,int)
      test(i) 测试第 i 位是否为1
      set() 置所有位为1 ,set(i) 置第 i 位为1
      reset() 置0, reset(i) 将第 i 位置0
      flip() 将所有位取反, flip(i) 将第 i 位取反
      count() 有多少个 1
      any() (true) 没有 (false)1
      none() (true) (false) 全部为 0
      可以使用索引 operator[ ]
2) vector<bool>
它的迭代器必须特殊定义,不能使用指向 bool 的指针
它的操作函数比 bitset 受更多的限制,它比普通的 vector 多一个操作 flip() 就是对所有位取反。
使用 operator[ ] 时返回一个 vector<bool>::reference 类型的对象,这个对象也有一个用于对个别位取反的成员函数 flip()
// Convert to a bitset:
      将一个 vector<bool> 转换成 bitset<> 时,可以把 vector<bool> 转换成 string ,再转换成一个 bitset<>
      ostringstream os;
      copy(vb.begin(), vb.end(),ostream_iterator<bool>(os, ""));
      bitset<10> bs(os.str());
10. 关联式容器
关联式容器有: set map multimap 。因为它们将关键字和值关联在一起,至少 map multimap 是这样子的。 Set 可以看作是没有值的 map ,它只有关键字, multiset mutipmap 的关系也是这样。它们的结构是相似的。
它们主要的操作就是把对象放入容器。当把对象放入容器时, set 会检查是否集合中已有这个对象; map 会检查是否已有这个关键字,如果有就把这个值关联给关键字。
isert()
cunt() 是否有元素
fnd() 返回这个元素第一次出现的 iterator
只对 multimap multiset 有意义 :
lwer_bound()
uper_bound()
eual_range()
map 中将索引设置为一个超出范围的值,意味着要创建一个新条目,如果使用 operator[ ] 来查找一个条目,当不存在这个条目时 map 会新创建一个新条目,所以一般用 count() find() 查找。
1) 用于关联式容器的发生器和填充器
在序列容器中用 fill() fill_n() generate() generate_n() 填充数据时,其实现是用赋值方式 operator= 将值放进序列,对关联式容器填充数据时,是用各自的 insert() 函数来实现。
2) 不可思议的映像
map 的迭代器解析得到的是一个 pair (也就是称为 value_type 的对象),可以用 first 得到它的键值,用 second 得到它的键值。用 insert() 插入时也是先构建一个 pair ,然后把这个 pair 插入 map
3) 多重映像和重复的关键字
multimap 里面会有重复值,这时可以用 equal_range() 成员函数,它返回这些重复值 pair 集的起始迭代器和结超出范围的迭代器
4) 多重集合
Mutipset 里可以有重复的元素值,但是这些重复值是一定连续的排在一起。同 multimap 一样可以用 equal_range() 返回这些重复值的起始和超出范围的迭代器,还可以用 distance() 来得到这个范围有多大。
也可以取得有哪些值是唯一存在的。
如果想不受限制的存放一个字符串,可以使用 vector/deque/list
11. 将STL容器联合使用
有时单独使用任一种类容器都不合适,那么可以把几种容器联合起来使用。譬如可以创建 vector map ,而 vector 又包含 map
12. 清除容器的指针
容器中的指针不会自动清除,需要我们手动清除,但是,也要保证不要对多个容器中持有的指向同一对象的指针进行多次清除,也就是不要对一个对象多次清除。但是对一个容器中的指针清除后赋为0,即使对这些指针多次清除也不会有问题。
13. 创建自己的容器
有了 STL 作基础,用户就也可以创建自己的容器了。
14. 对STL的扩充
尽管 STL 已提供了需要的很多功能,但它们还不是完美的。譬如 set map 的速度虽然很快但有时还是不能满足用户的需要。 SGI STL 增加很多扩充的容器,包括 hash_map hash_set hash_multimap hash_multiset slist( 单链表 ) rope( 一个 string 的变种,对非常大型的字符串、字符串的快速连结、取子字符串等操作进行了优化 ) hash_map 要比 map 的某些操作的性能好些。
map hash_map find() 要比 operator[ ] 的操作稍快些。
15. 非STL容器
在标准库中有两种“非 STL ”容器: bitset valarry 。因它们没有完全符合 STL 的要求而被称为非 STL 容器。它们都没有迭代器。 bitset 把二进制位打包成整数且不允许对其成员进行直接寻址。
valarry 是一个类 -vector 的模板类,它对一些有效率的数值计算进行了优化。 Valarry 提供了一个构造函数,该构造函数接受一个目标数组类型的参数和数组中元素计数的参数来初始化一个新的 valarry 。成员函数 shift() 将元素左移一个位置,移走后该位置的空位置填元素类型的默认值,如果参数是负的表是向右移; cshift() 是进行循环移动;二进位运算符要求 valarry 具有相同大小和类型的参数;成员函数 apply() 对每一个元素应用一个成员函数,但结果被收集到一个结果 valarry 中;关系运算符返回大小匹配的 valarry<bool> 实例,该实例显示了元素与元素逐个对比的结果。它的大多数操作返回一个数组,但也有个别的返回一个数值,如: min() max() sum() 。对 valarry 可以引用其元素中的一个子集(这个子集也被称为切片 slice )。某些运算也可以用切片来做它们的工作。一个切片接受三个参数:起始索引、要提取的元素合计数和“跨距”(也就是用户感兴趣的两个元素之间的间距)。切片可以作为一个现有 valarry 的索引,并返回一个包含被提取元素的新的 valarry
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值