《EssentialC++》笔记4—以Template进行编程

本文唯一重点:使用VS2019写C++,在写模版类中用到了模板参数的成员函数时,该成员函数不能放到.cpp文件里,也就是说头文件不能与cpp文件分离。这就意味着,你头文件定义的含模版的地方必须在头文件中实现,没用模版定义的地方可以放在cpp中实现。


  我们在前面已经用了很多的模板类了,比如vector<int>等等,本章的目标是打造一个模板类二叉树。

  二叉树包含两个class,一个是BinaryTree,用来储存一个指针,指向根结点,另一个是BTnode,用来存储结点值以及链接左、右两棵子树,此处“结点值的类型”便是我们想要加以参数化的地方。

  我们希望我们的BinaryTree类提供插入、移除、搜索、清除、前序遍历、中序遍历、后续遍历接口。

  我们的插入的实现满足以下性质:

  第一个插入空树的值,会成为此树的根结点。接下来每个结点必须以特定规则插入:如果小于根结点,就被放置在左子树,如果大于根结点,就被放置在右子树。任何一个值只能在树中出现一次,但是此树有能力记录同一值的插入此树,也就是说,这是一颗二叉排序树。

6.1 被参数化的类型

  我们可以用template定义模板类,可以把类型名给参数化提取出来。

template <typename elemtype>
class BinaryTree {
public:
	
private:
	BTnode<elemtype>* _root;
	//以<elemtype>类型实例化的BTnode是以<elemtype>类型实例化BinaryTree类的成员
};
template <typename elemtype>
class BTnode {
public:
	friend class BinaryTree<elemtype>;
	//因为BinaryTree需要能访问此类的数据,所以声明成friend比较好
	//这里加上<type>是表明以type实例化的类是以type实例化的BTnode的朋友
private:
	BTnode* _left;
	BTnode* _right;
	type _val;
	int count;
};

6.2 Class Template的定义

  总体框架:

template<typename elemtype>//先声明BinaryTree是一个模板类
class BinaryTree;
template <typename elemtype>
class BTnode {
public:
	friend class BinaryTree<elemtype>;
	//因为BinaryTree需要能访问此类的数据,所以声明成friend比较好
	//这里加上<elemtype>是表明以elemtype实例化的BinaryTree类是类BTnode的朋友
	BTnode(const elemtype& val);
	void insert_val(const elemtype& val);
	void remove_val(const elemtype& val, BTnode*& prev);
	static void lchild_leaf(BTnode* leaf, BTnode* subtree);
    //辅助删除的函数
	void preorder(BTnode* root, ostream& os = cout) const;
	void inorder(BTnode* root, ostream& os = cout) const;
	void postorder(BTnode* root, ostream& os = cout) const;
private:
	BTnode* _left;
	BTnode* _right;
	elemtype _val;
	int _count;
};

template <typename elemtype>
class BinaryTree {
public:
	BinaryTree();//默认构造函数
	BinaryTree(BinaryTree&);//拷贝构造函数
	~BinaryTree();//析构函数
	BinaryTree& operator=(const BinaryTree&);
	bool empty() { return _root == nullptr; }
	void clear()//清空(释放堆区内存)这个链式二叉树的函数接口
	{
		if (_root)
		{
			clear(_root);
			_root = nullptr;
		}
	}
	void insert(const elemtype& val);//插入
	void remove(const elemtype& val);//删除
	void preorder(ostream& os = cout);
	void inorder(ostream& os = cout);
	void postorder(ostream& os = cout);
	
private:
	BTnode<elemtype>* _root;
	//以<elemtype>类型实例化的BTnode是以<elemtype>类型实例化BinaryTree类的成员
	void copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src);
	//将src所指的子树复制到tar所指子树
	void remove_root();//辅助删除的函数
	void clear(BTnode<elemtype>*);//递归实现删除链式二叉树的函数
	void _copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src);
    //递归实现赋值功能的子函数
};

  以template编程时,在类体外给成员函数的定义的语法比较复杂,如下:

template <typename elemtype>
inline BinaryTree<elemtype>::BinaryTree()
{
	_root = nullptr;
}

  首先以一个模板开始,第一个BinaryTree的作用是以elemtype实例化BinaryTree,然后提供一个类作用范围标识符,第二个BinaryTree()就在BinaryTree<elemtype>内了,下面是其他几个的实现。

//拷贝构造函数
template <typename elemtype>
inline BinaryTree<elemtype>::BinaryTree(const BinaryTree& Bit)
{
	copy(_root, Bit._root);
}

//析构函数
template <typename elemtype>
inline BinaryTree<elemtype>::~BinaryTree()
{
	clear();
}

//赋值操作符重载
template <typename elemtype>
inline BinaryTree<elemtype>& BinaryTree<elemtype>::operator=(const BinaryTree& BiT)
{
	if (this != &BiT)
	{
		clear();
		copy(_root, BiT._root);
	}
	return *this;
}

6.3 Template类型的处理

  考虑到我们指定的类型可能是某种类,所以我们在设计如find函数的时候,为了效率最好传引用,并且写构造函数的时候,_val最好用成员逐次初始化列表(这样如果_val的类型是某种类,就可以调用它的构造函数,因为它可能没有重载=运算符)。

template<typename elemtype>
BTnode<type>::BTnode(const elemtype& val):_val(val)
{
	_left = _right = nullptr;
	_count = 1;
}

//不建议下面这样写
template<typename elemtypetype>
BTnode<type>::BTnode(const elemtypetype& val)
{
    _val = val;
	_left = _right = nullptr;
	_count = 1;
}

  放在函数体内在elemtype是类的时候,效率明显比前者低。

  因为,构造函数体内对_val的赋值操作可以分成两个步骤:

  1. 函数体执行前,类的默认构造函数会先作用在_val上。
  2. 函数体会以赋值运算符将val复制给_val。

  如果我们使用放在函数体外的成员逐次初始化列表,就只需要一个步骤就可以完成工作:以复制构造函数将val复制给_val。

6.4 实现Class Template

  首先是向树插入结点:(注意模板参数名字要写的一样)

template <typename elemtype>
void BinaryTree<elemtype>::insert(const elemtype& val)
{
	if (_root == nullptr)
		_root = new BTnode<elemtype>(val);
	else
		_root->insert_val(val);
}

  new表达式的动作可以分成两步:

  1. 向程序的空闲空间请求内存。如果分配到足够的空间,就返回一个指针,指向新对象。(如果空间不足,就会抛出bad_alloc异常)
  2. 如果第一步成功了,并且外界制定了一个初值,这个新对象便会以最恰当的方式初始化。

  我们这里的恰当方式明显是会调用BTnode的构造函数。

  接下来是插入结点,在二叉排序树讲过,就是一个递归的过程。

template<typename elemtype>
void BTnode<type>::insert_val(const elemtype& val)
{
	if (_val == val)
	{
		++_count;
		return;
	}
	if (val < _val)//如果传入的值小于根结点 就插入在左子树
	{
		if (_left != nullptr)
		{
			_left->insert_val(val);
		}
		else
		{
			_left = new BTnode<elemtype>(val);
			return;
		}
	}
	else
	{
		if (_right != nullptr)
		{
			_right->insert_val(val);
		}
		else
		{
			_right = new BTnode<elemtype>(val);
			return;
		}
	}
}

  这里的移除算法也在二叉排序树中讲过,我们就不讲了。

template <typename elemtype>
void BTnode<type>::lchild_leaf(BTnode* leaf, BTnode* subtree)
{
	while (subtree->_left)
		subtree = subtree->_left;
	subtree->_left = leaf;
}
template <typename elemtype>
void BinaryTree<elemtype>::remove_root()
{
	if (!_root)
		return;
	BTnode<elemtype>* tmp = _root;
	if (_root->_right)
	{
		_root = _root->_right;
		if (tmp->_left)
		{
			BTnode<elemtype>* lc = tmp->_left;
			BTnode<elemtype>* newlc = _root->_left;
			if (!newlc)
				_root->_left = lc;
			else
				BTnode<elemtype>::lchild_leaf(lc, newlc);
		}
	}
	else
		_root = _root->_left;
	delete tmp;
}
template <typename elemtype>
void BTnode<type>::remove_val(const type& val, BTnode*& prev)
//为了修改指针的值,传
{
	if (val < _val)
	{
		if (!_left)
			return;
		else
			_left->remove_val(val, _left);
	}
	else if (val > _val)
	{
		if (!_right)
			return;
		else
			_right->remove_val(val, _right);
	}
	else
	{
		if (_right)
		{
			prev = _right;
			if (_left)
				if (!prev->_left)
					prev->_left = _left;
				else
					BTnode<type>::lchild_leaf(_left, prev->_left);
		}
		else
			prev = _left;
		delete this;
	}
}

  这里遇到了一个VS编译器的问题,如果头文件中用template声明的部分,必须在头文件中实现。

  其他一些功能没有什么新的知识,我们就不单独拿出来直接放到汇总里了。

汇总:

//Template.h
#pragma once
using namespace std;
#include <vector>
#include <string>
#include <algorithm>
#include <iterator>
#include <iostream>

template<typename elemtype>
class BinaryTree;
template <typename elemtype>
class BTnode {
public:
	friend class BinaryTree<elemtype>;
	//因为BinaryTree需要能访问此类的数据,所以声明成friend比较好
	//这里加上<type>是表明以type实例化的类是以type实例化的BTnode的朋友
	BTnode(const elemtype& val);
	void insert_val(const elemtype& val);
	void remove_val(const elemtype& val, BTnode*& prev);
	static void lchild_leaf(BTnode* leaf, BTnode* subtree);
	void preorder(BTnode* root, ostream& os = cout) const;
	void inorder(BTnode* root, ostream& os = cout) const;
	void postorder(BTnode* root, ostream& os = cout) const;
private:
	BTnode* _left;
	BTnode* _right;
	elemtype _val;
	int _count;
};

template<typename elemtype>
BTnode<elemtype>::BTnode(const elemtype& val):_val(val)
{
	_left = _right = nullptr;
	_count = 1;
}

template <typename elemtype>
class BinaryTree {
public:
	BinaryTree();
	BinaryTree(BinaryTree&);
	~BinaryTree();
	BinaryTree& operator=(const BinaryTree&);
	bool empty() { return _root == nullptr; }
	void clear()
	{
		if (_root)
		{
			clear(_root);
			_root = nullptr;
		}
	}
	void insert(const elemtype& val);
	void remove(const elemtype& val);
	void preorder(ostream& os = cout);
	void inorder(ostream& os = cout);
	void postorder(ostream& os = cout);
	
private:
	BTnode<elemtype>* _root;
	//以<elemtype>类型实例化的BTnode是以<elemtype>类型实例化BinaryTree类的成员
	void copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src);
	//将src所指的子树复制到tar所指子树
	void remove_root();
	void clear(BTnode<elemtype>*);
	void _copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src);
};
template <typename elemtype>
inline BinaryTree<elemtype>::BinaryTree()
{
	_root = nullptr;
}

template <typename elemtype>
inline BinaryTree<elemtype>::BinaryTree(BinaryTree& Bit)
{
	copy(_root, Bit._root);
}

template <typename elemtype>
inline BinaryTree<elemtype>::~BinaryTree()
{
	clear();
}

template <typename elemtype>
inline BinaryTree<elemtype>& BinaryTree<elemtype>::operator=(const BinaryTree& BiT)
{
	if (this != &BiT)
	{
		clear();
		copy(_root, BiT._root);
	}
	return *this;
}

template <typename elemtype>
void BinaryTree<elemtype>::insert(const elemtype& val)
{
	if (_root == nullptr)
		_root = new BTnode<elemtype>(val);
	else
		_root->insert_val(val);
}

template<typename elemtype>
void BTnode<elemtype>::insert_val(const elemtype& val)
{
	if (_val == val)
	{
		++_count;
		return;
	}
	if (val < _val)//如果传入的值小于根结点 就插入在左子树
	{
		if (_left != nullptr)
		{
			_left->insert_val(val);
		}
		else
		{
			_left = new BTnode<elemtype>(val);
			//_left->_val = val;
			return;
		}
	}
	else
	{
		if (_right != nullptr)
		{
			_right->insert_val(val);
		}
		else
		{
			_right = new BTnode<elemtype>(val);
			//_right->_val = val;
			return;
		}
	}
}

template <typename elemtype>
void BinaryTree<elemtype>::remove(const elemtype& val)
{
	if (_root)
	{
		if (_root->_val == val)
			remove_root();
		else
			_root->remove_val(val, _root);
	}
}

template <typename elemtype>
void BTnode<elemtype>::lchild_leaf(BTnode* leaf, BTnode* subtree)
{
	while (subtree->_left)
		subtree = subtree->_left;
	subtree->_left = leaf;
}

template <typename elemtype>
void BinaryTree<elemtype>::remove_root()
{
	if (!_root)
		return;
	BTnode<elemtype>* tmp = _root;
	if (_root->_right)
	{
		_root = _root->_right;
		if (tmp->_left)
		{
			BTnode<elemtype>* lc = tmp->_left;
			BTnode<elemtype>* newlc = _root->_left;
			if (!newlc)
				_root->_left = lc;
			else
				BTnode<elemtype>::lchild_leaf(lc, newlc);
		}
	}
	else
		_root = _root->_left;
	delete tmp;
}

template <typename elemtype>
void BTnode<elemtype>::remove_val(const elemtype& val, BTnode*& prev)
{
	if (val < _val)
	{
		if (!_left)
			return;
		else
			_left->remove_val(val, _left);
	}
	else if (val > _val)
	{
		if (!_right)
			return;
		else
			_right->remove_val(val, _right);
	}
	else
	{
		if (_right)
		{
			prev = _right;
			if (_left)
				if (!prev->_left)
					prev->_left = _left;
				else
					BTnode<elemtype>::lchild_leaf(_left, prev->_left);
		}
		else
			prev = _left;
		delete this;
	}
}

template <typename elemtype>
void BinaryTree<elemtype>::clear(BTnode<elemtype>* pt)
{
	if (pt)
	{
		clear(pt->_left);
		clear(pt->_right);
		delete pt;
	}
}

template <typename elemtype>
void BinaryTree<elemtype>::copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src)
{
	if (tar != nullptr)
	{
		clear(tar);
	}
	_copy(tar, src);
}

template <typename elemtype>
void BinaryTree<elemtype>::_copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src)
{
	if (src == nullptr)
	{
		tar = nullptr;
		return;
	}
	tar = new BTnode<elemtype>(src->_val);
	_copy(tar->_left, src->_left);
	_copy(tar->_right, src->_right);
}

template <typename elemtype>
void BTnode<elemtype>::preorder(BTnode* root, ostream& os) const
{
	if (root)
	{
		os << root->_val << ' ';
		if (root->_left)
			preorder(root->_left, os);
		if (root->_right)
			preorder(root->_right, os);
	}
}

template <typename elemtype>
void BTnode<elemtype>::inorder(BTnode* root, ostream& os) const
{
	if (root)
	{
		if (root->_left)
			preorder(root->_left, os);
		os << root->_val << ' ';
		if (root->_right)
			preorder(root->_right, os);
	}
}

template <typename elemtype>
void BTnode<elemtype>::postorder(BTnode* root, ostream& os) const
{
	if (root)
	{
		if (root->_left)
			preorder(root->_left, os);
		if (root->_right)
			preorder(root->_right, os);
		os << root->_val << ' ';
	}
}

template <typename elemtype>
void BinaryTree<elemtype>::preorder(ostream& os)
{
	_root->preorder(_root, os);
}

template <typename elemtype>
void BinaryTree<elemtype>::inorder(ostream& os)
{
	_root->inorder(_root, os);
}

template <typename elemtype>
void BinaryTree<elemtype>::postorder(ostream& os)
{
	_root->postorder(_root, os);
}
#include "Template.h"
int main()
{
	BinaryTree<string> bt;
	bt.insert("Piglet");
	bt.insert("Eeyore");
	bt.insert("Roo");
	bt.insert("Tigger");
	bt.insert("Chris");
	bt.insert("Pooh");
	bt.insert("Kanga");
	bt.inorder();
	cout << '\n';
	BinaryTree<string> bt1(bt);
	bt1.preorder();
	bt.remove("Piglet");
	cout << '\n';
	bt.preorder();
}

6.5 以一个模板函数完成输出运算符

  输出二叉树的元素类型不同,我们自然想到利用template机制实现一个通用类型输出的函数。

template <typename elemtype>
inline ostream& operator<<(ostream& os, BinaryTree<elemtype>& bt)
{
	os << "PreorderTraverse: ";
	bt.preorder(os);
	os << '\n';
	os << "InorderTraverse: ";
	bt.inorder(os);
	os << "\nPosorderTraverse: ";
	bt.postorder(os);
    os << endl;
	return os;
}

6.6 常量表达式做template参数和默认参数值

  Template的参数并不一非得是某种类型不可,我们也可以用常量表达式作为template的参数,例如我们对数列类继承体系可以template化,将“对象所含元素个数”参数化。

template <int len>
class num_sequence {
public:
    num_sequence(int beg_pos = 1);
    //
};

template<int len>
class Fibonacci : public num_sequence<len> {
public:
    Fibonacci(int beg_pos = 1)
        :num_sequence<int>(beg_pos){}
}

  当我们产生Fibonacci对象的时候:

Fibonacci<16> fib1;
Fibonacci<17> fib2(17);

  其基类num_sequence会因为参数len而导致元素个数为16和17,同样的,我们还可以把长度和起始位置一起初始化:

template <int len, int beg_pos>
class num_sequence;

  大部分数列起始位置是1,如果能为起始位置提供默认值就更好了,template机制是可以做到这一点的。

template <int len, int beg_pos = 1>

  以template重写一下之前的数列类,这样的好处是不用再储存长度和起始位置这俩data member了。

//模板类num_sequence的总体框架
template <int len, int beg_pos>
class num_sequence {
public:
	virtual ~num_sequence() {};
	int elem(int pos) const;
	const char* what_am_i() const
	{
		return typeid(*this).name();
	}
	static int max_elems() { return _max_elems; }
	ostream& print(ostream& os = cout) const;
protected:
	virtual void gen_elems(int pos) const = 0;
	num_sequence(vector<int>* ps):_pelems(ps) {}
	static const int _max_elems = 1024;
	vector<int>* _pelems;
};

  输出运算符:

template <int len, int beg_pos>
ostream& operator<<(ostream& os, const num_sequence<len, beg_pos>& ns)
{
	return ns.print(os);
}

  检查以及补充数列元素的函数:

template <int len, int beg_pos>
int num_sequence<len, beg_pos>::elem(int pos) const
{
	if (!check_integrity(pos, _pelems->size()))
		return 0;
	return (*_pelems)[pos - 1];
}

template <int len, int beg_pos>
bool num_sequence<len,beg_pos>::check_integrity(int pos, int size) const
{
	if (pos > max_elems() || pos <= 0)
		return false;
	if (size < pos)
		gen_elems(pos);
	return true;
}

  打印函数:

template <int len, int beg_pos>
ostream& num_sequence<len, beg_pos>::print(ostream& os) const
{
	int elem_pos = beg_pos - 1;
	int end_pos = elem_pos + len;
	if (!check_integrity(end_pos, _pelems->size()))
		return os;
	os << "("
		<< beg_pos << ","
		<< len 
		<< ")";
	while (elem_pos < end_pos)
		os << (*_pelems)[elem_pos++] << ' ';
	return os;
}

  Fibonacci类:

template <int len, int beg_pos = 1>
class Fibonacci :public num_sequence<len, beg_pos>
{
public:
	Fibonacci() :num_sequence<len, beg_pos>(&_elems);
protected:
	static vector<int> _elems;
	virtual gen_elems(int pos) const;
};

  完成它的虚函数部分

template <int len, int beg_pos>
static vector<int> Fibonacci<len, beg_pos>::_elems;

template<int len, int beg_pos>
void Fibonacci<len, beg_pos>::gen_elems(int pos) const
{
	if (_elems.empty())
	{
		_elems.push_back(1);
		_elems.push_back(1);
	}
	if (_elems.size() < pos)
	{
		int i = _elems.size();
		int pprev = _elems[i - 2];
		int prev = _elems[i - 1];
		for (; i <= pos; i++)
		{
			int elem = pprev + prev;
			_elems.push_back(elem);
			pprev = prev;
			prev = elem;
		}
	}
}

  测试

int main()
{
	Fibonacci<6,12> fib1;
	Fibonacci<1, 10> fib2;
	Fibonacci<8> fib3;
	Fibonacci<8, 8> fib4;
	Fibonacci<8, 12> fib5;
	cout << "fib1:" << fib1
		<< "\nfib2:" << fib2
		<< "\nfib3:" << fib3
		<< "\nfib4:" << fib4
		<< "\nfib5:" << fib5
		;
}

在这里插入图片描述

  我们知道,函数名是一种地址常量,因此它也可以做为template的参数,如下:

//pf是一个指向特定的函数类型,产生pos个元素,放到vector seq内的函数指针
template<void (*pf) (int pos, vector<int>& seq)>
class num_sequence
{
public:
    num_sequence(int len, int beg_pos = 1)
    {
        assert(pf);
        _len = len > 0 ? len : 1;
        _beg_pos = beg_pos > 0 ? beg_pos : 1;
        pf(beg_pos + len - 1, _elems);
    }
private:
    int _len;
    int _beg_pos;
    vector<int> _elems;
};

  使用的时候我们就可以只去实现那个产生数列到pos位置的元素并放到vector容器里的函数就行。

void Fibonacci(int pos, vector<int>& seq)
{
    if (seq.empty())
    {
        seq.push_back(1);
        seq.push_back(1);
    }
    if (pos > seq.size())
    {
        int i = seq.size();
        int prev = seq[i - 1];
        int pprev = seq[i - 2];
        for(;i <= pos; i++)
        {
            int elem = prev + pprev;
            seq.push_back(elem);
            pprev = prev;
            prev = elem;
        }
    }
}

int main()
{
    num_sequence<Fibonacci> fib(1,10);
    return 0;
}

6.7 以Template参数作为一种设计策略

  现在我们可以利用template机制把LessThan函数对象要绑定的元素类型参数化化:

template <typename elemtype>
class LessThan
{
public:
   LessThan(elemtype& val):_val(val){}
   bool operator()(elemtype val) const
   {
       return val < _val;
   }
   void re_set(elemtype& val)
   {
       _val = val;
   }
   elemtype& get_set() const
   {
       return _val;
   }
private:
    elemtype _val;
};

  这样写有一个潜在问题,如果用户提供的类型没有指定小于运算符怎么办呢?

  一种可行的策略是这样的,我们同样以一个template参数Cmp来传递用户自己定义的小于运算符,如果用户没有定义小于运算符,我们也救不了了,就默认使用less<elemtype>运算符吧。

template <typename elemtype, typename Cmp = less<elemtype>>
class Lessthan
{
    public:
   Lessthan(elemtype& val):_val(val){}
   bool operator()(elemtype val) const
   {
       return Cmp(val,_val);
   }
   void re_set(elemtype& val)
   {
       _val = val;
   }
   elemtype& get_set() const
   {
       return _val;
   }
private:
    elemtype _val;
}

  比如用户指定以下比较大小的函数:

bool cmp(string& s1, string& s2)
{
    return si.size() < s2.size();
}

  由于存在默认参数值,可以这样使用:

Lessthan<int> lt(72);
//相当于Lessthan<int,less<int>> lt(72);
Lessthan<string, cmp> lt("hhahahahaha");

6.8 模板成员函数

  类中的成员函数也可以定义为模板函数,以一个打印类为例:

class PrintIt {
public:
    PrintIt(ostream& os):_os(os){}
    template <typename elemtype>
    void print(elemtype& elem, elemtype& delimiter = '\n')
    {
        _os << elem << delimiter;
    }
private:
    ostream& _os;
};

  甚至我们可以再把输出流也参数化一下:

template <typename Ostream>
class PrintIt
{
public:
    PrintIt(Ostream& os):_os(os){}
    template <typename elemtype>
    void print(elemtype& elem, elemtype& delimiter = '\n')
    {
        _os << elem << delimiter;
    }
private:
    Ostream _os;
};
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值