C++标准库 STL -- 容器源码探索

2 篇文章 0 订阅

文章内容为侯捷老师的《C++标准库与泛型编程》的学习笔记

第二讲:容器源码探索

源码之前,了无密码。

GP 编程

  • Containers 和 Algorithm 团队各自忙自己的事情,其间通过 Iterator 进行沟通。
  • Algorithm 通过 Iterator 确定操作的范围,Iterator 从 Container 取用元素。
template<class T>
inline const T& max(const T& a, const T& b)
{
	return b > a ? b: a;
}

template<class T, class Compare>
    inline const T& max(const T&a, const T& b)
{
    return compare(a,b) ? b : a;
}

上述代码实现了两种排序方式,第一种是 普通数据类型的比较,当然也可以用来比较class,不过需要重载 运算符 <,第二种则不需要,但需要提供额外的比较条件——compare 。

使用:

bool strLonger(const string& s1, const string& s2)
{
    return s1.size() < s2.size();
}

cout << "max of zoo and hello: "
    << max(string("zoo"), string("hello")) << endl;
cout << "longest of zoo and hello:"
    << max(string("zoo"), string("hello"), strLonger) << endl;

链表不能使用std::sort来进行排序,因为sort 底层在排序时,用到了随机迭代器,可以随意对迭代器进行++,而链表不具备这样的能力。它不是一个连续空间的数据结构。

所有的 algorithm ,其内最终涉及元素本身的操作,无非就是比大小。

操作符重载和模板

操作符重载。不同的操作符会有不同的操作数。比如 a++ 、a--分别只需要一个操作数。而a+b 则需要两个操作数。

image-20210304171236963

表格来源

指针具有的操作,迭代器都会进行重载一遍,为了实现将迭代器当成指针使用的目的。比如一个链表的迭代器。

template<class T, class Ref, class Ptr>
    struct __list_iterator {
        typedef __list_iterator<T, Ref, Ptr> self;
      	typedef __list_node<T>* link_type;
        link_type node;
        
        reference operator*() const { return (*node).data; }
        pointer operator->() const { return &(operator*()) ; }
        self & operator++() { node = ( link_type)((*node).next) ); return *this; }
        self operator++(int) { self tmp = *this; ++*this; return tmp;}//我比较纳闷,这里的int 有没有被用上。但源码的确是这样的。
    };

在使用模板时,你要告诉编译器,你传入什么类型进入模板类中。使用尖括号明白的告诉编译器使用的类型,然后跟上变量名,最后在小括号中,将初始化数值传入类中。

complex<double> c1(2.5, 1.6);

把它们 (编程逻辑) 里面的类型暂定为一个符号,而不写死,等到真正使用的时候,才去把它确定下来。这个就是模板

分配器

调用alloc 会得到一块内存,但实际上只用到了一部分。如下图:

image-20210305104155000

其中只用到了 蓝色部分,灰色是debug 时产生的。砖红色是一些cookie。绿色的是 为调整到某一个边界所额外开销。附加的这些东西是基本固定的,你要的某一块越大,它所附加东西的比例就越小。你要的东西越小,附加东西的比例就越大。

  • cookie 记录申请内存块的大小,所以free 只需要一个指针即可回收内存

直接malloc带来的问题

一个string 的类型只有四个字节。当你往一个容器中放入100万个string 时,额外的开销会比100万个 string 还要多。

//分配512 ints

int *p = allocator<int>().allocate(512, (int*)0);
allocator<int>().decallocate(p, 512);

//allocator<int>() 是一个object

/* 扩展
	cout << vector<int>().size() << endl;
	cout << vector<int>(10).size() << endl;
	cout << vector<int>(10, 100).front() << endl;
*/

良好分配器的解决方式

一个分配器有16个链表,每个链表负责不同的大小。第0个链表负责 8字节大小的内存,第n 负责 8*n 字节大小的内存,第16个负责128个字节的内存。所有需要内存的容器都向这个分配器要内存。如果分配器没有对应大小的内存,分配器才会去向操作系统要——要大块,然后做切割。割成一小块块的。切出来的一块块用链表串起来。结果是这切出来的每一个块都不带cookie。

image-20210305114352355

其它的分配器

image-20210305114500754

目前大多用的是 allocator分配器,当然除了 常规的allocator外,还提供了一些额外的分配器。比如:__poll_alloc

容器 - 结构与分类

容器会分为两大类:序列式容器和关联式容器。其中有一些容器是由一些容器衍生出来的,这里的衍生,并非继承而是复合。

比如 stack 和 queue 由 deque衍生而来,stack(queue) 中有一个 deque,它所表现出来的能力由deque 所提供,所以stack 和 queue 又是一种容器适配器。

set ,map, multimap,multiset 的功能由rb_tree(红黑树) 实现。

List

    template <class T>
    struct __list_node {
        typedef void* void_pointer;
        void_pointer prev;
        void_pointer next;
        T data;
    };

    template<class T, class Ref, class Ptr>
    struct __list_iterator{
        typedef T   value_type;
        typedef Ref reference;
        typedef Ptr pointer;
        typedef bidirectional_iterator_tag iterator_category;//使用一种标签来表明它的特性
        typedef ptrdiff_t difference_type;  //固定的长度,如果容器添加的元素超过这个长度,可能会爆掉

        typedef __list_iterator<T, Ref, Ptr> self;
        typedef __list_node<T>* link_type;
        link_type node;

        reference operator*() const { return (*node).data; }
        pointer operator->() const { return &(operator *());}///等同于 ===> &((*node).data) 可实现元素类型-> 的效果
        self& operator++ ()
        { node = (link_type)((*node).next); return *this; }

        self operator++ (int)
        { self tmp = *this;  ++*this;  return tmp;}
        /**
          后置++的代码执行流程
          1. 记录原值
            self tmp = *this;
            此处的不会调用operator* 来处理this,执行的是 copy cotr ,用以创建 tmp 并以 *this 为初值。
            __list_iterator(const iterator& x) : node(x.node) {}
            当*this 当做ctor 的参数后,就不调用operator* 的重载函数
          2. 进行操作
*/
        /**
          关于 前置++ 的返回值为引用,后置++ 返回值为对象的原因:
          向整型操作致敬,因为 C++ 允许整型两次前置++,而不允许两次后置++。
*/
    };


    template <class T, class Alloc = alloc>
    class List {
    protected:
        typedef __list_node<T> list_node;
    public:
        typedef list_node* link_type;
        typedef __list_iterator<T, T&, T*> iterator;
    protected:
        link_type node;

    };

有趣的是,GCC4.9版本的 list 是使用继承来拥有 next 和 prev

   template <typename _Tp>
    struct _List_iterator{
        typedef _Tp* pointer;
        typedef _Tp* reference;
    };

    template<typename _Tp,
             typename _alloc = std::allocator<_Tp>>
    class list: protected _List_base<_Tp, _Alloc>
    {
    public:
        typedef _List_iterator<_Tp> iterator;
    };

    struct _List_node_base {
        _List_node_base * _M_next;
        _List_node_base * _M_prev;
    };

    template <typename _Tp>
    struct _List_node
            : public _List_node_base {
        _Tp _M_data;
    };
    /**
      通过继承的方式来获取 头部指针和尾部指针
*/

Iterator需要遵循的原则

Iterators 迭代器 是处于容器和算法之间的桥梁,负责从容器中获取数据,也负责回答算法提出的问题。比如:负责告诉算法接受的迭代器 是什么样的数据类型。

所以迭代器中要设计出5种关联类型。

template <class _Iterator>
struct iterator_traits {
  typedef typename _Iterator::iterator_category iterator_category;
  typedef typename _Iterator::value_type        value_type;
  typedef typename _Iterator::difference_type   difference_type;
  typedef typename _Iterator::pointer           pointer;
  typedef typename _Iterator::reference         reference;
};

比如List 的迭代器

template<class _Tp, class _Ref, class _Ptr>
struct _List_iterator {
  typedef _List_iterator<_Tp,_Tp&,_Tp*>             iterator;
  typedef _List_iterator<_Tp,const _Tp&,const _Tp*> const_iterator;
  typedef _List_iterator<_Tp,_Ref,_Ptr>             _Self;

  typedef bidirectional_iterator_tag iterator_category;
  typedef _Tp value_type;
  typedef _Ptr pointer;
  typedef _Ref reference;
  typedef ptrdiff_t difference_type;
};

Traits 特性,特征,特质

Iterator Traits 用来区分 class iterators 和 no-class iterators。

如果是class iterator T,可直接T::valueType,来获取迭代器的数据类型。

如果是class * iterator pT。我无法通过pT->valueType;

所以要通过一个中间层,这个中间层,被称为萃取机。

   /**
     * 为了解决 传入的 iterator 是 class 还是 pointer,
     *  下面制定了泛化版本 iterator_traits 和 偏特化版本 iterator_traits
*/
namespace my {
    template <class T >     //如果 T 是 class iterator
    struct iterator_traits {
        typedef typename T::value_type value_type;
    };

    template <class T>  //如果 T 是 pointer to T
    struct iterator_traits<T*> {
        typedef T value_type;
    };

    template <class T>  //如果 T 是 pointer to const T
    struct iterator_traits<const T*> {
        typedef T value_type;
    };

}

完整的 iterator_traits。

template <class _Iterator>
struct iterator_traits {
  typedef typename _Iterator::iterator_category iterator_category;
  typedef typename _Iterator::value_type        value_type;
  typedef typename _Iterator::difference_type   difference_type;
  typedef typename _Iterator::pointer           pointer;
  typedef typename _Iterator::reference         reference;
};
template <class _Tp>
struct iterator_traits<_Tp*> {
  typedef random_access_iterator_tag iterator_category;
  typedef _Tp                         value_type;
  typedef ptrdiff_t                   difference_type;
  typedef _Tp*                        pointer;
  typedef _Tp&                        reference;
};

template <class _Tp>
struct iterator_traits<const _Tp*> {
  typedef random_access_iterator_tag iterator_category;
  typedef _Tp                         value_type;
  typedef ptrdiff_t                   difference_type;
  typedef const _Tp*                  pointer;//这里的指针和引用都有常量的修饰
  typedef const _Tp&                  reference;
};

除了有迭代器的 Traits 外,还有

image-20210305190249459

vector

template <class T, class Alloc = alloc>
class vector {
public:
    typedef T value_type;
    typedef value_type * iterator;
    typedef value_type& reference;
    typedef size_t size_type;
protected:
    //不同于链表,所有连续空间存储元素的容器都可以使用指针作为迭代器。
    iterator start;
    iterator finish;
    iterator end_of_storage;
public:
    iterator begin() { return start;}
    iterator end() { return finish;}
    size_type size() const
    { return size_type(end() - begin());}

    size_type capacity()
    { return size_type( end_of_storage - begin() );}

    bool empty() const { return begin() == end();}
    reference operator[] (size_type n)
    { return *(begin() + n);}
    reference front() { return *begin();}
    reference back() { return *finish();}

};

deque

image-20210308104300675

其中的 map 是一个vector, 其中的元素是一个个的指针,这些指针分别指向各个缓冲区的buff。

image-20210308153417236

容器结构源码和容器迭代器源码

/**
 *  //sz 为元素A的大小,如果元素A大小 大于 512 则一个缓冲区放入一个元素。
    //反则就放入 size_t(512 / sizeof(A)) 个数量
    inline size_t __deque_buf_size(size_t n, size_t sz)
    { return n != 0 ? n :( sz < 512 ? size_t(512 / sz) :size_t(1) );}
**/
inline size_t _deque_buf_size(size_t n, size_t sz)
{
    return n != 0 ? n : ( sz < 512 ? size_t(512/sz) : size_t(1));
}

template <class T, class Ref, class Ptr, size_t BufSize>
struct __deque_iterator {
    typedef random_access_iterator_tag iterator_catogory;   //随机访问迭代器的类型
    typedef T value_type;
    typedef Ptr pointer;
    typedef Ref reference;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;
    typedef T** map_pointer;
    typedef __deque_iterator self;

    T* cur;//元素的位置
    T* first;//缓冲区的头部
    T* last;//缓冲区的尾部
    map_pointer node;

    reference operator*() const { return *cur; }
    pointer operator->() const { return &(operator *());}

    self & operator++()
    {
        ++cur;                      //切换到下一个元素
        if (cur == last) {          //如果抵达缓冲区尾端
            set_node(node + 1);     //就切换到下一个节点
            cur = first;            //cur 再指向于 first    cur 是指向缓冲区的元素,first 和 end 是缓冲区的首尾元素
        }

        return * this;
    }

    self operator ++(int)
    {
        self tmp = *this;
        ++*this;
        return tmp;
    }

    difference_type operator-(const self& x) const
    {
        //buffer_size 等同于  _deque_buf_size
        return difference_type(buffer_size()) * (node - x.node - 1) +
                (cur - first) + (x.last - x.cur);
        /**
          node - x.node - 1 其中减一的原因是,x.node 所在的buff 上,存在元素未放满的情况
*/
    }

    reference operator[] (difference_type n) const
    { return *(*this + n);}

    /// 依次修改 node first last 的位置
    void set_node(map_pointer new_node)
    {
        node = new_node;
        first = *new_node;
        last = first + difference_type(buffer_size());
    }
};

/**
    BufSiz 指的是每个 buffer 容纳的元素个数
*/
template <class T, class Alloc = alloc,
          size_t BufSize = 0>
class deque {
public:
    typedef T value_type;
    typedef value_type* pointer;
    typedef __deque_iterator<T, T&, T*, BufSiz> iterator;//自定义类作为迭代器
    typedef size_t size_type;
protected:
    typedef pointer* map_pointer;
protected:
    iterator start;//指向容器头部的迭代器 ,start.cur 是容器的第一个元素,*start 也是指向容器的第一个元素
    iterator finish;//指向容器尾部的迭代器, finish.cur 是容器的最后一个元素,finish 是指向容器最后一个元素的下一个位置。要取最后一个元素,需要--finish.
    map_pointer map;
    size_type map_size;
public:
    iterator begin() { return start; }
    iterator end() { return finish; }
    size_type size() { return finish - start;}
    size_type max_size() const { return size_type(-1); }
};

queue

image-20210308164247677

#include "deque.h"

/*
 * queue 依赖 deque ,头尾可进可出的特性,实现了队列形式的先进先出
*/

template <class T, class _Sequence = deque<T> >//也可以使用其它容器作为 _Sequence 的参数,如果该容器可以实现 queue 中的成员函数。 ,比如 List
class queue {
public:
  typedef typename _Sequence::value_type      value_type;
  typedef typename _Sequence::size_type       size_type;
  typedef          _Sequence                  container_type;

  typedef typename _Sequence::reference       reference;
  typedef typename _Sequence::const_reference const_reference;
protected:
  _Sequence c;
public:

  bool empty() const { return c.empty(); }
  size_type size() const { return c.size(); }
  reference front() { return c.front(); }
  const_reference front() const { return c.front(); }
  reference back() { return c.back(); }
  const_reference back() const { return c.back(); }
  void push(const value_type& __x) { c.push_back(__x); }//在尾部添加一个数据
  void pop() { c.pop_front(); }//在头部删除一个元素
};

stack

image-20210308164230502


/* *
 * 同queue 类似,都是借用了 deque 的头尾都可进出的能力,实现了先进后出的栈
*/

#include "deque.h"

//GCC2.95

template <class _Tp, class _Sequence = deque<_Tp> >//也可以使用其它容器作为 _Sequence 的参数,如果该容器可以实现 stack 中的成员函数,比如 List  或 vector
class stack {
public:
  typedef typename _Sequence::value_type      value_type;
  typedef typename _Sequence::size_type       size_type;
  typedef          _Sequence                  container_type;

  typedef typename _Sequence::reference       reference;
  typedef typename _Sequence::const_reference const_reference;
protected:
  _Sequence _M_c;
public:

  bool empty() const { return _M_c.empty(); }
  size_type size() const { return _M_c.size(); }
  reference top() { return _M_c.back(); }
  const_reference top() const { return _M_c.back(); }
  void push(const value_type& __x) { _M_c.push_back(__x); }//在尾部添加一个元素
  void pop() { _M_c.pop_back(); }//在尾部删除一个元素
};

容器rb_tree

红黑树是平衡二分搜寻树中常被使用的一种。平衡二分搜寻树的特征:排列规则有利于 Search 和 insert,并保持适度平衡——无任何节点过深。

rb_tree 提供两种 insertion操作:insert_unique 和 insert_equal。前者表示节点的key 一定是在整个tree 中独一无二,否则安插失败。后者表示节点的key 可重复。独一无二的key 对应的就是 set 和 map,可以重复的key 对应的就是 multiSet 和 multiMap。

rb_tree 的结构

template <class Key,
          class Value,      //此处的Value 是 key+data ,封装起来的pair。也可以将key 当做 data,那体现出的容器就是Set
          class KeyOfValue, //如何从value 中拿到key
          class Compare,    //排序比大小的方式
          class Alloc = alloc>
class rb_tree {
protected:
    typedef __rb_tree_node<Value> rb_tree_node;
public:
    typedef rb_tree_node* link_type;
    typedef Value value_type;
    typedef const value_type* const_pointer;
    typedef value_type* pointer;
    typedef size_t size_type;
    typedef value_type& reference;
    typedef const value_type& const_reference;


    typedef _Rb_tree_iterator<value_type, reference, pointer> iterator;
    typedef _Rb_tree_iterator<value_type, const_reference, const_pointer>
            const_iterator;
protected:
    size_type node_count;
    link_type header;
    Compare key_compare;        //key 的大小比较规则:应该是一个函数对象,即仿函数
};

在GCC 7.3 版本下运行的rb_tree

//GCC
_Rb_tree <int, int, _Identity<int>, less<int> > itree;
cout << "itree.empty(): \t " <<itree.empty() << endl;
cout << "itree.size(): \t " << itree.size() << endl;

for (int i = 0; i < 10; ++i) {
    itree._M_insert_unique(std::move(i));
}

itree._M_insert_equal(5);

cout << "itree.empty(): \t " <<itree.empty() << endl;
cout << "itree.size():\t " << itree.size() << endl;
cout << "itree.count():\t " << itree.count(5) << endl;

在上面涉及到了两个函数对象。下面是它们在GCC 2.9 中的部分实现。

/* 仿函数,形态是一个类,。没有data ,但是行为上是一个函数 */

template <class Arg, class Result>
struct unary_function{
    typedef Arg argument_type;
    typedef Result result_type;
};

template<class T>// identity 的数学含义是证同性函数,即不做修改,保持原值。  identity n. 身份;同一性
struct identity : public unary_function<T,T> {
    const T& operator()(const T& x) const {return x;}
};

template <class Arg1, class Arg2, class Result>
struct binary_function {
    typedef Arg1 first_argument_type;
    typedef Arg2 second_argument_type;
    typedef Result result_type;
};

template <class T>
struct less : public binary_function<T, T, bool > {//模板类的继承,与常规类继承类似,不同的地方是传参数给基类。
    bool operator() (const T& x, const T& y)   { return x < y;}
};

顺便提一下仿函数

auto fun = My::less<int>();
cout << ( fun(5, 6) ? "5 < 6" : "5 > 6") << endl;

采用上面的less 并放入一个 My 的namespace 中,进行调用。开始是想通过 My::less<int>(5,6); 来使用,但被编译器告知constexpr My::less<int>::less(),所以把 My::less<int>(),当做一个类来看——创建对象,然后对象调用operator() (const T&x),问题解决。

Set

set/multiset 以 rb_tree 为底层结构,因此有元素自动排序的特性。排序的依据 key, 而 set/multiset 元素的 value 和 key 合一。

我们无法使用 set/multiset 的 iterator改变元素值,不仅是因为 key 有严谨的排列规则。而且set/multiset 的迭代器是采用的是底层 rb_tree 中的 const_iterator,就是为了禁止 用户 对元素赋值。换句话说,无法修改已插入Set 中的元素。

部分源码如下:

template <class Key,
          class Compare = less<key>,
          class Alloc = alloc>
class Set {
public:
    typedef Key key_type;
    typedef Key value_type;
    typedef Compare key_compare;
    typedef Compare value_compare;

private:
    typedef rb_tree<key_type, value_type,
        identity<value_type>, key_compare, Alloc> rep_type;
    rep_type t;
public:
    typedef typename rep_type::const_iterator iterator;
};

Map

类同于 Set 同样拥有 元素自动排序 的特性,排序的依据是 key。我们无法使用 map/multimap 的 iterator 改变元素的key,但可以用它来改变元素的 data。实现的原理是,map/multimap 内部自动将用户指定 的key 设置为const 类型,如此虽然迭代器不是 const 但也能禁止用户修改 元素的key 值。

源码如下:

template <class Key, class T,
          class Compare = less<Key>,
          class Alloc = alloc>
class Map {
public:
    typedef Key key_type;
    typedef T data_type;
    typedef pair<const Key, T> value_type;//通过const key 来限制 key 不能被外界修改
    typedef Compare key_compare;
private:
    typedef rb_tree<key_type, value_type, select1st<value_type>,
        key_compare, Alloc> rep_type;

    rep_type t;
public:
    typedef typename rep_type::iterator iterator;

};

在使用Map 创建对象时

map <int, string> imap;->
map <int, string, less<int>, alloc> imap;->
templdate < int, pair<const int, string>, select1st<pair<cosnt int, string>>, less<int>, alloc>
    class rb_tree;

容器 hashtable

无序关联容器

容器由 vector 和 链表构成。通过将元素转换为数字来除以当前 vector 的长度得到的余数就是在vector中安插的位置。另外,当vector 中安插的元素总个数大于vector 的size,容器会发生分裂现象。首先,vector的长度会扩展近似2倍,然后将原来的元素重新打散,再按照 元素转数字 的方式,重新安插元素到 新vector 中。

hashTable 的部分源码
gcc 2.95版本

/* 一个单项链表 */
template <class _Val>
struct _Hashtable_node
{
  _Hashtable_node* _M_next;
  _Val _M_val;
}; 

template <class _Val, class _Key, class _HashFcn,
          class _ExtractKey, class _EqualKey, class _Alloc>
struct _Hashtable_iterator {
   typedef hashtable<_Val,_Key,_HashFcn,_ExtractKey,_EqualKey,_Alloc>
          _Hashtable;
   typedef _Hashtable_node<_Val> _Node;
   _Node* _M_cur;
  _Hashtable* _M_ht;
};
/**
  这里的 node 是指向一个 buckets vector 中一个bucket的链表节点,这个节点用于知晓 迭代器 查找元素是否走到链表的末尾。
  当走到末尾时,迭代器有能力会切换到下一个 bucket 中,然后在新的bucket 中继续遍历链表。
*/

template <class _Val, class _Key, class _HashFcn,
          class _ExtractKey, class _EqualKey, class _Alloc>
class hashtable {
public:
  typedef _Key key_type;
  typedef _Val value_type;
  typedef _HashFcn hasher;//这个函数用于计算元素的hashCode,根据hashCode 决定元素的安插位置
  typedef _EqualKey key_equal;// 这个函数用于表示 两个元素相等的条件
private:
  hasher                _M_hash;
  key_equal             _M_equals;
  _ExtractKey           _M_get_key;
  vector<_Node*,_Alloc> _M_buckets;
  size_type             _M_num_elements;
public:
    friend struct
  _Hashtable_iterator<_Val,_Key,_HashFcn,_ExtractKey,_EqualKey,_Alloc>;
};

一些 hashFcn 函数的实现方式,如果在 hashtable 中添加其它元素时需要自行编写 HashFcn

template <class _Key> struct hash { };

#define __STL_TEMPLATE_NULL template<>

inline size_t __stl_hash_string(const char* __s)
{
  unsigned long __h = 0; 
  for ( ; *__s; ++__s)
    __h = 5*__h + *__s;
  
  return size_t(__h);
}

__STL_TEMPLATE_NULL struct hash<char*>
{
  size_t operator()(const char* __s) const { return __stl_hash_string(__s); }
};

__STL_TEMPLATE_NULL struct hash<const char*>
{
  size_t operator()(const char* __s) const { return __stl_hash_string(__s); }
};
....

hash function 的目的,就是希望根据元素值算出一个 hash code,使得元素经 hash code 映射之后能够【够乱够乱随机】地被安插于 hashtable 内。越是凌乱,越是不容易发生碰撞。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值