【侯捷】C++STL标准库与泛型编程(第四讲)

第四讲

1、一个万用的Hash Function

image-20211231171153305

说明:

1.1 针对自定义类型的哈希函数的编写方式

自定义类型:

#include <functional>
class Customer {
    ...
};

针对自定义类型编写哈希函数的三种方式:

  • 方式一:编写仿函数
//形式1
class CustomerHash {
public:
    std::size_t operator()(const Customer& c) const {
        return ...
    }
};
//第二个模板参数传入的是类型
unordered_set<Customer, CustomerHash> custset;
  • 方式二:自定义哈希函数,并将函数对象传入
//形式2
size_t customer_hash_func(const Customer& c) {
    return ...
}
//第二个模板参数为函数类型
unordered_set<Customer, size_t(*)(const Customer&)> custset(20, customer_hash_func); 
  • 方式三:以 struct hash 偏特化形式实现 Hash Function
    在这里插入图片描述
    针对自定义的类型实现偏特化的Hash function,如下是对MyString类型进行特化的版本
    image-20220101132045090
    G4.9中所有的基本类型都有自己的hash function,G2.9中没有对string类型进行特化。

1.2 哈希函数的具体设计

image-20211231171211012

  • 方案一:因为基本类型都具有自己的hash function,所以想着将Customer中的所有数据拆开为基本类型,然后对各种类型进行hash,最后结果合在一起:

    class CustomerHash {
    public:
        std::size_t operator()(const Customer&) const {
            return std::hash<std::string>()(c.fname) +
                std::hash<std::string>()(c.lname) + 
                std::hash<long>()(c.no);
        }
    };
    

    这种方案可以使用,但是太过天真,后期可能会产生很多碰撞💥;

  • 方案二:使用variadic templates(可变化的模板)

    class CustomerHash {
    public:
        std::size_t operator()(const Customer&) const {
            return hash_val(c.fname, c.lname, c.no); //将Customer的数据都放入
        }
    };
    
    template <typename... Types> //接受任意个数的模板参数,语法"typename..."
    inline size_t hash_val(const Types&... args) {
        size_t seed = 0;
        hash_val(seed, args...);
        return seed;
    }
    
    template <typename T, typename... Types> //可以接受任意个数的模板参数
    inline void hash_val(size_t& seed, const T& val, const Types&... args) {
        hash_combine(seed, val);
        hash_val(seed, args...); //递归调用本身,拆分参数,每次都取一个参数进行hash_combine,直到取到只有一个元素调用hash_val(size_t&, const T&)函数
    }
    //上面两个函数的区别是第一个实参的类型
    
    #include <functional>
    template <typename T>
    inline void hash_combine(size_t& seed, const T& val) {
        //最终获得的seed就是Customer的hash-code
        seed ^= std::hash<T>()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2); 
    }
    
    template <typename T>
    inline void hash_val(size_t& seed, const T& val) {
        hash_combine(seed,val);
    }
    

    上面代码中的hash_combine函数里的 0x9e3779b9 是黄金比例:
    image-20211231171224902

  1. 上文的源代码:
    image-20211231171237136

  2. G4.9版本下的Hash Function使用示例:
    image-20211231171255402

2、Tuple用例

tuple:一堆东西的组合,可以指定任意类型的任意个元素为一个整体结构。

  • tuple使用示例:

image-20220101132643551

说明:

  1. 源码
#include<iostream>
#include <cstring>
#include <complex>
#include <functional>
using namespace std;

int main() {
    cout << "string, sizeof = " << sizeof(string) << endl;
    cout << "double, sizeof = " << sizeof(double) << endl;
    cout << "float, sizeof = " << sizeof(float) << endl;
    cout << "int, sizeof = " << sizeof(int) << endl;
    cout << "complex<double>, sizeof = " << sizeof(complex<double>) << endl;

    //tuples
    //create a four-element tuple
    //- elements are initialized with default value (0 for fundamental types)
    tuple<string, int, int, complex<double> > t;
    cout << "sizeof = " << sizeof(t) << endl;

    //create and initialized a tuple explicitly
    tuple<int, float, string> t1(41, 6.3, "nico");
    cout << "tuple<int, float, string>, sizeof = " << sizeof(t1) << endl;
    //iterator over elements: 取出元素
    cout << "t1:" << get<0>(t1) << " " << get<1>(t1) << " " << get<2>(t1) << endl;

    //create tuple with make_tuple()
    auto t2 = make_tuple(22, 44, "stacy"); //编译器会进行实参推导出类型

    //assign second value in t2 to t1
    get<1>(t1) = get<1>(t2); 


    //comparison and assignment
    //- include type conversion from tuple<int, int, const char*>
    //to tuple<int, float, string>
    if (t1 < t2) { //compares value for value
        cout << "t1 < t2" << endl;
    } else {
        cout << "t1 >= t2" << endl;
    }
    t1 = t2; //Ok, assigns value for value
    cout << "t1 : " << t1 << endl; //必须重载操作符重载operator<<

    tuple<int, float, string> t3(77, 1.1, "more light");
    int i1;
    float f1;
    string s1;
    tie(i1, f1, s1) = t3; //assigns values of t to i, f, and s ,取出t3的成分绑定到各个变量

    typedef tuple<int, float, string> TupleType;
    cout << tuple_size<TupleType>::value << endl; //yields 3,有3个成分
    tuple_element<1, TupleType>::type f1 = 1.0; //yields float
    typedef tuple_element<1, TupleType>::type T;

    return 0;
}
  1. 为什么tuple<stirng, int, int, complex<double>> t;sizeof(t) 是32, 而不是28(理论上将tuple中的类型大小加起来)呢?
  2. 如下是参数数量可变的模板应用到tuple,variadic templates语法自动把n个参数分解为1和n-1,再把n-1分解为1和n-2,…,不断递归直到没有参数,所以variadic templates一定是有一个主体,有一个终止条件。

image-20220101132708298

tuple继承了自己(每次成分减少一个),variadic template会自动将其处理为继承的关系,如上图右边所示。当参数为0个的时候,使用特化的版本template<> class tuple<>{};。子类的大小 = 子类本身的数据 + 父类的数据,那么上面提到的问题2,按照这种想法应该就是28,而不应该是32,这个待思考🤔。图中的tail()函数返回值this本来指的是例子中的这三块(41、6.3和nico),但是经过转型后变成只指向上两块(6.3和nico)。

type traits

  • G2.9版本的type traits

image-20220103010219672

struct __true_type {};
struct __false_type {};

//泛化版本
template <class type>
struct __type_traits {
	typedef __true_type  	this_dummy_member_must_be_first;
    typedef __false_type 	has_trivial_default_constructor; //默认构造函数是否重要,默认情况下是重要的
    typedef __false_type 	has_trivial_copy_constructor; //拷贝构造函数是否重要,默认情况下是重要的
    typedef __false_type 	has_trivial_assignment_operator; //赋值操作是否重要,默认情况下是重要的
    typedef __false_type 	has_trivial_destructor; //析构函数是否重要,默认情况下是重要的
    typedef __false_type 	is_POD_type; //Plain Old Data,C中的structure,没有function,只有data
};

//特化版本
//如果明确知道上面提问的那些函数不重要,那么就可以为这个泛化版本写特化版本,来说明答案不是默认的答案,而是有自己的答案
//对于整数这种type,所提问的那些函数都不重要
template<> struct __type_traits<int> {
    typedef __true_type 	has_trivial_default_constructor; 
    typedef __true_type 	has_trivial_copy_constructor; 
    typedef __true_type 	has_trivial_assignment_operator; 
    typedef __true_type 	has_trivial_destructor; 
    typedef __true_type 	is_POD_type; //Plain Old Data
};

//特化版本
template<> struct __type_traits<double> {
    typedef __true_type 	has_trivial_default_constructor; 
    typedef __true_type 	has_trivial_copy_constructor; 
    typedef __true_type 	has_trivial_assignment_operator; 
    typedef __true_type 	has_trivial_destructor; 
    typedef __true_type 	is_POD_type; //Plain Old Data
};

//自定义的类型,可以照着这种写法自己实现自定义类型的特化版本,自己判断那些函数是否重要,自己回答
//比如“复数类”,没有指针,只有实部和虚部,可以不用写构造函数和析构函数,因为编译器有默认版本,所以那些函数都是不重要的。

__type_traits<Foo>::has_trivial_destructor  //算法要通过__type_traits询问Foo:你有不重要的析构函数吗
  • C++11版本的type traits

image-20220103010252815

image-20220103010319608

新版本不需要对自己的自定义类型实现特化版本,而且可以询问的东西更多了。

type traits的测试

image-20220103010354017

#include <iostream>
using namespace std;

//global function template
template <typename T>
void type_traits_output(const T& x) {
    cout << "\ntype traits for type : " << typeid(T).name() << endl;

    cout << "is_void\t" << is_void<T>::value << endl; //结果为true或者false
    cout << "is_integral\t" << is_integral<T>::value << end;
    cout << "is_floating_point\t" << is_floating_point<T>::value << endl;
    cout << "is_arithmetic\t" << is_arithmetic<T>::value << endl;
    cout << "is_signed\t" << is_signed<T>::value << endl;
    cout << "is_unsigned\t" << is_unsigned<T>::value << endl;
    cout << "is_const\t" << is_const<T>::value << endl;
    cout << "is_volatile\t" << is_volatile<T>::value << endl;
    cout << "is_class\t" << is_class<T>::value << endl;
    cout << "is_function\t" << is_function<T>::value << endl;
    cout << "is_reference\t" << is_reference<T>::value << endl;
    cout << "is_lvalue_reference\t" << is_lvalue_reference<T>::value << endl;
    cout << "is_rvalue_reference\t" << is_rvalue_reference::value << endl;
    cout << "is_pointer\t" << is_pointer<T>::value << endl;
    cout << "is_member_pointer\t" << is_member_pointer<T>::value << endl;
    cout << "is_member_object_pointer\t" << is_member_object_pointer<T>::value << endl;
    cout << "is_member_function\t" << is_member_function<T>::value << endl;
    cout << "is_fundamental\t" << is_fundamental<T>::value << endl;
    cout << "is_scalar\t" << is_scalar<T>::value << endl;
    cout << "is_object\t" << is_object<T>::value << endl;
    cout << "is_compound\t" << is_compound<T>::value << endl;
    ... //如下的测试结果图中更多
}

image-20220103010420319

说明:

  1. 以上是将 string 放入 type_traits_output 函数进行测试的结果;
  2. 类中如果有指针的时候就需要编写析构函数;当类要被作为基类的时候,需要写虚析构函数,但是因为 string 并不会被作为基类,所以析构函数不需要写成virtual;

image-20220103010653691

class Foo {
private:
    int d1, d2;
};

type_traits_output(Foo());

说明:

  1. Foo 中没有 function,只有data,所以它是pod type;

image-20220103010717974

class Goo {
public:
    virtual ~Goo { }
private:
    int d1, d2;
};
type_traits_output(Goo());

说明:

  1. Goo中有虚析构函数,所以has_virtual_destructor结果是true;
  2. is_polymorphic是否为多态,“多态”类是声明或继承了virtual 方法,Goo中声明了 virtual function,所以它是多态的;

image-20220103010807126

class Zoo {
public:
    Zoo(int i1, int i2): d1(i1), d2(i2) { } //构造函数
    Zoo(const Zoo&) = delete; //拷贝构造
    Zoo(Zoo&&) = default; //move constructor
    Zoo& operator=(const Zoo&) = default; //拷贝赋值
    Zoo& operator=(const Zoo&&) = delete; //move assignment 
    virtual ~Zoo() { }
private:
    int d1, d2;
};
type_traits_output(Zoo(1, 2));

说明:

  1. 因为自己编写了构造函数,所以没有默认构造函数;
  2. 拷贝构造函数被delete了,所以没有拷贝构造函数;

image-20220103011102123

说明:

  1. 复数只有实部和虚部,所以不用写虚析构函数,编译器会有默认的,这个析构函数是不重要的;

image-20220103011209379

type traits的实现

  • is_void
    image-20220103011330517

说明:

  1. 都是使用“模板”对类型做操作;
  2. remove_cv是先将关键字const和volatile拿掉;
  3. struct remove_const<_Tp const> 是偏特化,范围的偏特化;
  4. is_void模板类先利用remove_cvconstvolatile去除,然后进入__is_void_helper,如果是 void,则返回真,否则返回假;
  • is_integral
    image-20220103011400517

说明:

  1. is_integral判断是否为整数类型;
  2. is_integral会先将const和volatile关键字拿掉,然后进入__is_integral_helper中,根据类型不同进入不同的偏特化版本;
  • is_class,is_union,is_enum, is_pod
    image-20220103011436556

说明:

  1. 蓝色的这些没有在C++标准库的源代码中,可能是编译器编译的时候整理出来的结果;
  • is_move_assignable
    image-20220103011501814

说明:

  1. 和上面一样,is_reference已经不在C++标准库源代码中;
  2. type traits就是一个类型萃取机;

3、cout

image-20220103011521657

说明:

  1. cout是个对象,extern表明外界可以使用它;
  2. 通过cout输出对象,因为它对不同类型产生的对象进行了重载operator<<,如果没有出现在重载的类型中,就需要自己重载operator<<
  3. 标准库中对<<的操作符重载:
    image-20220103011537389

4、moveable元素对容器速度效能的影响

image-20220103011559124

说明:

  1. 上面的是moveable 元素的测试,300万个元素放入vector中,调用了0次copy constructor,但是调用了7194303次move constructor。花费的时间是8547ms;

  2. 下面的是non-moveable元素的测试,同样300万个元素,放入vector中,调用了7194303次copy constructor,调用了0次move constructor,花费的时间是14235ms;

  3. M c11(c1);是拷贝构造;M c12(std::move(c1));是move copy;

  4. 不同类型的容器调用相同的一份代码测试插入300万个元素:

    for (long i = 0; i < value; ++i) {
        snprintf(buf, 10, "%d", rand());
        auto ite = c1.end();
        c1.insert(ite, V1type(buf));
    }
    
  5. vector的特性是当空间不够用的时候,就要2倍增长空间,并且将原来的数据拷贝到新的空间,这个过程会调用拷贝构造或拷贝赋值,因此,多出了很多的构造函数的调用;

image-20220103011615400

说明:

  1. 不同于vector,list没有扩充或增长的行为,放300万个元素就调用300万次构造函数;
  2. 后面的容器也是相同的行为模式

image-20220103011629785

image-20220103011643134

image-20220103011656143

写一个moveable class

image-20220103011711668

image-20220103011726355

源码:

#include <iostream>
#include <cstdio>  //snprintf()
#include <cstdlib> //RAND_MAX
#include <cstring> //strlen(), memcpy()
#include <string> 
using std::cin;
using std::cout;
using std::string;

//以下 MyString 是為了測試 containers with moveable elements 效果.  
class MyString { 
public: 
    static size_t DCtor;  	//累計 default-ctor 的呼叫次數 
    static size_t Ctor;  	//累計 ctor      的呼叫次數 
    static size_t CCtor;  	//累計 copy-ctor 的呼叫次數 
    static size_t CAsgn;  	//累計 copy-asgn 的呼叫次數 
    static size_t MCtor;  	//累計 move-ctor 的呼叫次數 
    static size_t MAsgn;  	//累計 move-asgn 的呼叫次數 		    
    static size_t Dtor;	//累計 dtor 的呼叫次數 
private:     
  	char* _data; 
  	size_t _len; 
  	void _init_data(const char *s) { 
    		_data = new char[_len+1]; 
    		memcpy(_data, s, _len); 
    		_data[_len] = '\0'; 
  	} 
public: 
	//default ctor
  	MyString() : _data(NULL), _len(0) { ++DCtor;  }

	//ctor
  	MyString(const char* p) : _len(strlen(p)) { 
  		++Ctor; 
    	_init_data(p); 
  	} 

	// copy ctor
  	MyString(const MyString& str) : _len(str._len) { 
		++CCtor;  	  
    	_init_data(str._data); 	//COPY
  	} 

	//move ctor, with "noexcept", 和copy ctor的区别在于参数有两个&,只是拷贝指针
    MyString(MyString&& str) noexcept : _data(str._data), _len(str._len)  {  
        ++MCtor;    
    	str._len = 0; 		
    	str._data = NULL;  	//避免 delete (in dtor) 
 	}
 
 	//copy assignment
  	MyString& operator=(const MyString& str) { 
    	++CAsgn;  	 
		if (this != &str) { 
    		if (_data) delete _data;  
      		_len = str._len; 
      		_init_data(str._data); 	//COPY! 
    	} 
    	else {
		    // Self Assignment, Nothing to do.   
		}
    	return *this; 
  	} 

	//move assignment
   	MyString& operator=(MyString&& str) noexcept { 	 
     	++MAsgn;   	
    	if (this != &str) { 
    		if (_data) delete _data; 
      		_len = str._len; 
      		_data = str._data;	//MOVE!
      		str._len = 0; 
      		str._data = NULL; 	//避免 deleted in dtor 
    	} 
    	return *this; 
 	}
 
 	//dtor
  	virtual ~MyString() { 	
  	    ++Dtor;	      	  	    
    	if (_data) { //
    		delete _data; 	
		}
  	}   	
  	
  	bool 
  	operator<(const MyString& rhs) const	//為了讓 set 比較大小  
  	{
	   return std::string(this->_data) < std::string(rhs._data); 	//借用事實:string 已能比較大小. 
	}
  	bool 
  	operator==(const MyString& rhs) const	//為了讓 set 判斷相等. 
  	{
	   return std::string(this->_data) == std::string(rhs._data); 	//借用事實:string 已能判斷相等. 
	}	
	
	char* get() const { return _data; }
}; 
//在class之外给static数据定义
size_t MyString::DCtor=0;  	
size_t MyString::Ctor=0;  	 
size_t MyString::CCtor=0;
size_t MyString::CAsgn=0;
size_t MyString::MCtor=0;
size_t MyString::MAsgn=0;
size_t MyString::Dtor=0;

namespace std 	//必須放在 std 內 
{
template<> 
struct hash<MyString> 	//這是為了 unordered containers 
{
	size_t 
	operator()(const MyString& s) const noexcept
	{  return hash<string>()(string(s.get()));  }  
	    //借用現有的 hash<string> (in ...\include\c++\bits\basic_string.h)
};
}

image-20220103011745981

class MyStrNoMove { 
public: 
    static size_t DCtor;  	//累計 default-ctor 的呼叫次數 
    static size_t Ctor;  	//累計 ctor      的呼叫次數 
    static size_t CCtor;  	//累計 copy-ctor 的呼叫次數 
    static size_t CAsgn;  	//累計 copy-asgn 的呼叫次數 
    static size_t MCtor;  	//累計 move-ctor 的呼叫次數 
    static size_t MAsgn;  	//累計 move-asgn 的呼叫次數 		    
    static size_t Dtor;	    //累計 dtor 的呼叫次數 
private:     
  	char* _data; 
  	size_t _len; 
  	void _init_data(const char *s) { 
    		_data = new char[_len+1]; 
    		memcpy(_data, s, _len); 
    		_data[_len] = '\0'; 
  	} 
public: 
	//default ctor
  	MyStrNoMove() : _data(NULL), _len(0) { 	++DCtor; _init_data("jjhou"); }

	//ctor
  	MyStrNoMove(const char* p) : _len(strlen(p)) { 
    	++Ctor;  _init_data(p); 
  	} 

	// copy ctor
  	MyStrNoMove(const MyStrNoMove& str) : _len(str._len) { 
		++CCtor;  	 
    	_init_data(str._data); 	//COPY
  	} 

 	//copy assignment
  	MyStrNoMove& operator=(const MyStrNoMove& str) { 
    	++CAsgn;

		if (this != &str) { 
    		if (_data) delete _data;  
      		_len = str._len; 
      		_init_data(str._data); 	//COPY! 
    	} 
    	else {
		    // Self Assignment, Nothing to do.   
		}
    	return *this; 
  	} 

 	//dtor
  	virtual ~MyStrNoMove() { 	   
  	    ++Dtor;		  	    
    	if (_data) {
    		delete _data; 	
		}
  	}   	
  	
  	bool 											
  	operator<(const MyStrNoMove& rhs) const		//為了讓 set 比較大小 
  	{
	   return string(this->_data) < string(rhs._data);  //借用事實:string 已能比較大小. 
	}  	
	
  	bool 											
  	operator==(const MyStrNoMove& rhs) const	//為了讓 set 判斷相等. 
  	{
	   return string(this->_data) == string(rhs._data);  //借用事實:string 已能判斷相等. 
	} 
		
	char* get() const { return _data; }	
}; 
size_t MyStrNoMove::DCtor=0;  	
size_t MyStrNoMove::Ctor=0;  
size_t MyStrNoMove::CCtor=0;
size_t MyStrNoMove::CAsgn=0;
size_t MyStrNoMove::MCtor=0;
size_t MyStrNoMove::MAsgn=0;
size_t MyStrNoMove::Dtor=0;

namespace std 	//必須放在 std 內 
{
template<> 
struct hash<MyStrNoMove> 	//這是為了 unordered containers 
{
	size_t 
	operator()(const MyStrNoMove& s) const noexcept
	{  return hash<string>()(string(s.get()));  }  
	   //借用現有的 hash<string> (in ...\4.9.2\include\c++\bits\basic_string.h)
};
}
#include <ctime>  //clock_t, clock()
template<typename M, typename NM>	
void test_moveable(M c1, NM c2, long& value)
{ 	
char buf[10];
			
	//測試 move 
	cout << "\n\ntest, with moveable elements" << endl;			
	typedef typename iterator_traits<typename M::iterator>::value_type  V1type; 	
clock_t timeStart = clock();								
    for(long i=0; i< value; ++i)
    {
    	snprintf(buf, 10, "%d", rand());    		
        auto ite = c1.end();
        c1.insert(ite, V1type(buf)); //所有容器都提供insert, V1type(buf)是个临时对象,右值,所以编译器会调用其move版本	
	}
	cout << "construction, milli-seconds : " << (clock()-timeStart) << endl;	
	cout << "size()= " << c1.size() << endl;		
	output_static_data(*(c1.begin()));

	timeStart = clock();	
M c11(c1);		//将整个容器拷贝到c11,深拷贝。 c1不是临时对象			
	cout << "copy, milli-seconds : " << (clock()-timeStart) << endl;	

	timeStart = clock();	
M c12(std::move(c1));	//move copy,浅拷贝。				
	cout << "move copy, milli-seconds : " << (clock()-timeStart) << endl;
		
	timeStart = clock();	
	c11.swap(c12);	//交换					
	cout << "swap, milli-seconds : " << (clock()-timeStart) << endl;		

	
	
	//測試 non-moveable 	
	cout << "\n\ntest, with non-moveable elements" << endl;		
	typedef typename iterator_traits<typename NM::iterator>::value_type  V2type; 				
    timeStart = clock();								
    for(long i=0; i< value; ++i)
    {
    	snprintf(buf, 10, "%d", rand());    		
        auto ite = c2.end();
        c2.insert(ite, V2type(buf));	
	}

	cout << "construction, milli-seconds : " << (clock()-timeStart) << endl;	
	cout << "size()= " << c2.size() << endl;			
	output_static_data(*(c2.begin()));

	timeStart = clock();	
NM c21(c2);						
	cout << "copy, milli-seconds : " << (clock()-timeStart) << endl;	

	timeStart = clock();	
NM c22(std::move(c2));						
	cout << "move copy, milli-seconds : " << (clock()-timeStart) << endl;
		
	timeStart = clock();	
	c21.swap(c22);						
	cout << "swap, milli-seconds : " << (clock()-timeStart) << endl;			
}	

move之后原来的东西不再使用,才可以选择使用move copy,否则可能会带来隐患:两个指针指向同一块内存。

vector的copy ctor

image-20220103011759241

说明:

  1. M c11(c1)这是vector的深拷贝,即如上图的流程;

image-20220103011812065

说明:

  1. M c12(std::move(c1));这是调用的vector的move ctor,只是将三个指针进行交换,速度很快;

std::string 是否 moveable?

image-20220103011827090

说明:

  1. std::string 的实际类型是basic_string
  2. basic_string中有构造函数,拷贝构造函数,move assign和move constructor,所以std::string是moveable的,可以安心使用;
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值