【类模板】

1.定义模板类

下面以第十章的Stack类为基础来建立模板。原来的类声明如下:

typedef unsigned long Item;

class Stack
{
private:
    enum { MAX = 10 };
    Item* pitems;
    int size;
    int top;
public:
    Stack(int n = MAX);
    Stack(const Stack& st);
    ~Stack();
    bool isempty() const;
    bool isfull() const;
    bool push(const Item& item);
    bool pop(Item& item);
    Stack& operator=(const Stack& st);
    friend ostream& operator<<(ostream& os, const Stack& st);
};

采用模板时,将使用模板定义替换Stack声明,使用模板成员函数替换Stack的成员函数。

template<class Type>
class Stack
{
private:
    enum { MAX = 10 };
    Type* pitems;
    int size;
    int top;
public:
    Stack( );
    bool isempty() const;
    bool isfull() const;
    bool push(const Type& item);
    bool pop(Type& item);
};
template<class Type>
Stack<Type>::Stack()
{
	top = 0;    //top只有有数据输入才会增加
}
template<class Type>
bool Stack<Type>::isempty() const
{
	return (top == 0);
}
template<class Type>
bool Stack<Type>::isfull() const
{
	return (top == MAX);
}
template<class Type>
bool Stack<Type>::push(const Type& item)
{
	if (top == MAX)
		return false;
	else
		pitems[top++] = item;
	return true;

}
template<class Type>
bool Stack<Type>::pop(Type& item)
{
	if (top == 0)
		return 0;
	else
		item = pitems[--top];
	return true;
}

 模板定义:

template<class Type>
class Stack
{
private:
    enum { MAX = 10 };
    Type* pitems;
    int size;
    int top;
public:
 Stack( );
    bool isempty() const;
    bool isfull() const;
    bool push(const Type& item);
    bool pop(Type& item);
 
};

模板类以第一行template<class Type>开头,class可以替换为typename.也可以使用自己的泛型名代替Type,比如用T。当模板被调用时,Type将被具体的类型值取代。可以使用泛型名标识要存储再栈中的类型。

模板成员函数:

template<class Type>
Stack<Type>::Stack()
{
	top = 0;    //top只有有数据输入才会增加
}

template<class Type>
bool Stack<Type>::isempty() const
{
	return (top == 0);
}

template<class Type>
bool Stack<Type>::isfull() const
{
	return (top == MAX);
}

template<class Type>
bool Stack<Type>::push(const Type& item)
{
	if (top == MAX)
		return false;
	else
		pitems[top++] = item;
	return true;

}

template<class Type>
bool Stack<Type>::pop(Type& item)
{
	if (top == 0)
		return 0;
	else
		item = pitems[--top];
	return true;
}

每个函数头都将以相同的模板声明开头,同样应该使用泛型名代替typedef标识符。另外,还需将限定符从Stack::改为Stack<Type>:: 。

注意:模板不是类和成员函数定义,是C++编译指令,说明了如何生成类和成员函数的定义。不能将模板成员函数放在独立的实现文件中,因为模板不是函数,模板必须与特定的模板实例化请求一起使用。最简单的办法就是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。 

2.使用模板类

#include <iostream>
#include <cctype>
#include<string>
#include "stack.h"


int main()
{
    using namespace std;
    Stack<string> st;
    char ch;
    string po;
    cout << "Please enter A to add a purchase order,\n" << "P to process a PO, Q to quit.\n";
    while (cin >> ch && toupper(ch) != 'Q')
    {
        while (cin.get() != '\n')
            continue;
        if (!isalpha(ch))
        {
            cout << '\a';
            continue;
        }
        switch (ch)
        {
        case 'A':
        case 'a': cout << "Enter a PO number to add: ";
            cin >> po;
            if (st.isfull())
                cout << "stack already full.\n";
            else
                st.push(po);
            break;
        case 'p':
        case 'P': if (st.isempty())
            cout << "stack already empty.\n";
                else
        {
            st.pop(po);
            cout << "PO #" << po << " popped.\n";
        }
                break;
        }
        cout << "Please enter A to add a purchase order,\n" << "P to process a PO, or Q to quit.\n";
    }

    cout << "Bye\n";
    return 0;
}

 下面的代码生成两个栈:

 Stack<string> st; 
 Stack<int> st2;

注意:泛型标识符成为类型参数,意味着它们类似于变量,但是赋给它们的不能是数字,只能是类型。必须显式地提供所需的类型,这与常规模板不同,常规模板可以根据函数参数类型来确定要生成哪种函数,类模板不能这样。 

3.深入探讨模板类

对于上边的示例,我们可以将内置类型或对象用作类模板的类型,指针可以吗?下面解释上边的示例不适合用char *指针的原因,然后介绍一个适合使用栈指针的示例。

3.1不正确的使用指针栈

简要介绍3个试图对上边示例程序修改,使之使用指针栈的简单但有缺陷的示例。这三个示例都已完全正确的栈模板为基础:

1.string po 替换为 char * po;

这种方法很快失败了,因为仅仅创建指针,没有创建用于保存输入字符串的空间。

2.string po替换为 char po [40];

这在pop函数中也是失败的,因为引用变量pitems必须引用某种类型的左值,而不是数组名。其次,假设代码可以给pitems赋值,即使引用变量能够引用数组,也不能为数组名赋值。

template<class Type>
bool Stack<Type>::pop(Type& item)
{
	if (top == 0)
		return 0;
	else
		item = pitems[--top];
	return true;
}

3.string po替换为 char *po = new char[40];

为字符串分配空间,并且po是变量,这里的最基本的问题是,只有一个po变量,该变量总是指向相同的内存单元。每次读取新字符时,老字符就会被覆盖,并且出栈操作每次弹出的也是相同的地址,没有任何用途。 

3.2正确使用指针栈

使用指针栈的方法之一是:让调用程序提供一个指针数组,其中每个数组都指向不同的字符串。栈的任务是管理指针,而不是创建指针。

假设我们要模拟下面的情况。 某人将一车文件夹交给P,如果P的收取篮是空,他将取出最上层文件夹放进去,如果收取篮是满的,他将取出收取篮最上层文件夹,放入发出篮中。如果收取篮不满也不空,则上边两种行为都可能发生。

可以用一个指针数组来模拟这种情况,指针指向表示车中的文件夹,用栈表示收取篮,第二个指针数组表示发出篮。指针从输入数组压入栈表示将文件添加到收取篮,栈中弹出项目,将它添加到发出数组中表示处理文件。

因为用到了动态分配数组,我们需要有析构,复制和等号的重载。

//STACK.H
#include <iostream>
using std::ostream;
template<class Type>
class Stack
{
private:
    enum { MAX = 10 };
    Type *pitems;
    int size;
    int top;
public:
	explicit Stack(int ss = size);
    bool isempty() const;
    bool isfull() const;
    bool push(const Type& item);
    bool pop(Type& item);
	Stack(const Stack& st);
	~Stack() { delete[] pitems; }
	Stack& operator=(const Stack& st);
};
template<class Type>
Stack<Type>::Stack(int ss):size(ss),top(0)
{
	pitems = new Type[size];
}
template<class Type>
bool Stack<Type>::isempty() const
{
	return (top == 0);
}
template<class Type>
bool Stack<Type>::isfull() const
{
	return (top == size);
}
template<class Type>
bool Stack<Type>::push(const Type& item)
{
	if (top == size)
		return false;
	else
		pitems[top++] = item;
	return true;

}
template<class Type>
bool Stack<Type>::pop(Type& item)
{
	if (top == 0)
		return 0;
	else
		item = pitems[--top];
	return true;
}
template<class Type>
Stack<Type>::Stack(const Stack& st)
{
	size = st.size;
	pitems = new Type[size];
	top = st.top;
	for (int i = 0; i < top; i++)
		pitems[i] = st.pitems[i];
}
template<class Type>
Stack<Type>& Stack<Type>::operator=(const Stack& st)
{
	if (this == &st)
		return &this;
	delete[] pitems;
	size = st.size;
	pitems = new Type[size];
	top = st.top;
	for (int i = 0; i < top; i++)
		pitems[i] = st.pitems[i];
}

对于赋值运算符号的重载,原型声明是将返回类型声明为Stack&,而实际模板函数定义将返回类型定义为 Stack<Type>&,前者是后者的缩写,但只能在类中使用。但在类的外边,就必须用完整的Stack<Type>&。(函数定义用缩写报错,因为类已经出了)

#include <iostream>
#include <cstdlib>
#include<ctime>
#include "stack.h"
const int Num = 10;

int main()
{
    using namespace std;
    srand(time(0));
    cout << "Please enter stack size:";
    int size;
    cin >> size;
    Stack<const char* >st(size);

    //In basket
    const char* in[Num]
    {
        "1:HD DUASO","2:Kijfd du9",
        "3:Qjfd dis","4:Pfdews fds",
        "5:Adjwes dso","6:Bfedo fuedo",
        "7:Ffjei fdso","8:Lhjfcs dhsi",
        "9:Rjfd fdjs","10:Wfji fds0i"
    };

    //out basket
    const char* out[Num];
    int processed = 0;
    int nextin = 0;
    while (processed < Num)  //全部输出
    {
        if (st.isempty())
            st.push(in[nextin++]);
        else if (st.isfull())
            st.pop(out[processed++]);
        else if (rand() & 2 && nextin < Num)
            st.push(in[nextin++]);
        else
            st.pop(out[processed++]);
    }
    for (int i = 0; i < Num; i++)
        cout << out[i] << endl;
    cout << "Bye\n";
    return 0;
}

 字符串本身不会移动,把字符串压入栈本质上是创建一个新指向该字符串的指针,栈弹出字符串把地址值复制到out数组中。

4.数组模板示例和非类型参数

模板常用作容器类,因为类型参数的概念非常适合于将相同的存储方案用于不同的类型。现在来学习非类型参数(表达式参数)以及如何使用数组来处理继承族。

1.首先介绍一个允许指定数组大小的简单模板。有两种方式,第一种是再类中使用动态数组和构造函数参数来提供元素数目(Stack类就是这样的方式),另一种方法是使用模板参数来提供数组的大小(array就是这样做的)。

#ifndef ARRAYTP_H_
#define ARRAYTP_H_

#include <iostream>
#include<cstdlib>

template<class T, int n>
class ArrayTP
{
private:
	T ar[n];
public:
	ArrayTP();
	explicit ArrayTP(const T& v);
	virtual const T& operator[](int i) const;
	virtual T& operator[](int i);

};
template<class T, int n>
ArrayTP<T, n>::ArrayTP()
{

}

template<class T, int n>
ArrayTP<T, n>::ArrayTP(const T& v)
{
	for (int i = 0; i < n; i++)
		ar[i] = v;
}

template<class T, int n>
const T& ArrayTP<T, n>::operator[](int i) const
{
	if (i < 0 || i >= n)
	{
		std::cerr << "Error in array limits.";
		std::exit(EXIT_FAILURE);
	}
	return ar[i];
}
template<class T, int n>
T& ArrayTP<T, n>::operator[](int i)
{
	if (i < 0 || i >= n)
	{
		std::cerr << "Error in array limits.";
		std::exit(EXIT_FAILURE);
	}
	return ar[i];
}


#endif

说明:

1.模板头: 

template<class T, int n>

关键字class指出T为参数类型,int 指出 n的类型为int。这种参数(指定特殊的类型而不是用作泛型名)称为非类型或表达式参数。表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。double m是非法的,double * m是合法的。另外,模板代码不能修改参数的值,也不能使用参数的地址。在模板中不能使用n++或&n这样的操作。另外,实例化模板时,用作表达式参数的值必须是常量表达式。

该方法的优点在于方法使用的是为自动变量维护的内存栈,而不是动态内存,执行速度更快。

缺点是:每种数组大小都将生成自己的模板,内存占用大。

另一个区别是,构造函数方法更通用,因为数组大小是作为类成员存储在定义中。这样可以将一种尺寸的数组赋给另一个尺寸的数组,也可以允许创建允许数组大小可变的类。  

5.模板多功能性

可以将用于常规类的技术用于模板类。

模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数。

1.递归使用模板

对于前边的数组模板定义,可以这样使用:

	ArrayTP< ArrayTP<int, 5> , 10> twodee;

这使得twodee是一个包含10个数组,其中每个元素都是一个包含5个int元素的数组,常规数组声明如下:int twodee[10][5];维的顺序与等价二维数组相反。

#include<iostream>
#include"arraytp.h"

int main()
{
	using std::cout;
	using std::cin;
	using std::endl;
	ArrayTP<int, 10> sums;
	ArrayTP<double, 10> aves;
	ArrayTP< ArrayTP<int, 5> , 10> twodee;

	int i, j;
	for (i = 0; i < 10; i++)
	{
		sums[i] = 0;
		for (j = 0; j < 5; j++)
		{
			twodee[i][j] = (i + 1) * (j + 1);
			sums[i] += twodee[i][j];
		}
		aves[i] = (double)sums[i] / 10;
	}
	for (i = 0; i < 10; i++)
	{
		for (j = 0; j < 5; j++)
		{
			cout.width(2);
			cout << twodee[i][j] << " ";
		}
		cout << ": sum = ";
		cout.width(3);
		cout << sums[i] << ",average = " << aves[i] << endl;
		aves[i] = (double)sums[i] / 10;
	}
	cout << "Done!";
	return 0;
}

 2.使用多个类型参数

模板可以包含多个类型参数。例如,假设希望类可以保存两种值,则可以创建并使用Pair模板来保存两个不同的值。

#include<iostream>
#include<string>
template<class T1,class T2>
class pairs
{
private:
	T1 a;
	T2 b;
public:
	pairs(){}
	pairs(const T1& n, const T2& m):a(n),b(m){}
	T1& first() { return a; };
	T2& second() { return b; };
	T1 first() const { return a; };
	T2 second() const { return b; };
};
int main()
{
	using namespace std;
	pairs<string, int> rating[4] =
	{
		pairs<string,int>("The Purpled Duck",5),
		pairs<string,int>("Jaquie's Frisco",4),
		pairs<string,int>("Cafe Sou",5),
		pairs<string,int>("Bertie's Eats",3),

	};
	int joints = sizeof(rating) / sizeof(pair<string, int>);
	cout << "Rating:\tEatery\n";
	for (int i = 0; i < joints; i++)
		cout << rating[i].second() << ":\t"
		<< rating[i].first() << endl;
	cout << "Oops!Revised reating:\n";
	rating[3].second() = 6;
	cout << rating[3].second() << ":\t"
		<< rating[3].first() << endl;
	return 0;
 }

注意:在main()中必须使用pairs<string,int>来调用构造函数,并将它作为sizeof()参数,这是因为类名是 pairs<string,int>,而不是pair。

3.默认类型模板参数

类模板的另一项新特性是,可以为类型参数提供默认值:

template<class T1, class T2 = int> class Topo{...};

这样,如果省略T2的值,编译器将使用int;但不能为函数模板参数提供默认值。可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。

6.模板的具体化

模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。具体化可以有隐式实例化,显式实例化和显式具体化。

1.隐式实例化

声明一个或多个对象,指出所需的类型,而编译器使用通过模板提供的处方生成具体的类定义。编译器在没有指定对象前,是不会生成类的隐式实例化的。

2.显式实例化

使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。生命必须位于模板定义所在的名称空间中。

3.显式具体化

显式具体化是特定类型(用于替换模板中的泛型)的定义。在需要为特殊类型实例化时,对模板进行修改,使其行为不同,在这种情况下,应该创建显式具体化。具体化格式如下:

template<> class Classname<specialized-type-name>{......};

4.部分具体化

C++还允许部分具体化,即部分限制模板的通用性。部分具体化可以给类型参数之一指定具体的类型:

template<class T1,class T2> class Pair{...};
template<class T1>,class Pair<T1,int>{...};

 关键字template后边的<>声明的是没有被具体化的参数,如果指定所有类型,<>内将为空,这将导致显式具体化。

如果有多个模板可供选择,编译器将使用具体化程序最高的模板。

也可也i通过指针提供特殊版本 来部分具体化现有的模板:

template<class T>
class Feeb{};
template<class T*>
class Feeb{};  提供修改后的代码

 如果提供的类型不是指针,编译器将使用通用版本,如果提供的是指针,编译器将使用指针集体化版本,如果没有进行部分具体化版本,当传入指针参数,声明会使用通用模板,指针就是传入类型参数。

部分具体化特性使得能够设置各种限制。

template<class T1, class T2, class T3> class Trio{...};


template<class T1, class T2> class Trio<T1,T2,T2>{...};


template<class T1> class Trio<t1,t1*,t1*>{...};

给定上述声明,编译器将做出不同选择。

Trio<int,short,char *> t1;


Trio<int,> t2;


Trio<int> t3;

7.成员模板

模板可用作结构、类或模板类的成员。

#include<iostream>
using std::cout;
using std::cin;
using std::endl;
template<typename T>
class beta
{
private:
	template<typename V>
	class hold
	{
	private:
		V val;
	public:
		hold(V v = 0):val(v){}
		void show()const { cout << val << endl; }
		V Value() const { return val; }
	};
	hold<T> q;
	hold<int>n;
public:
	beta(T t, int i):q(t),n(i){}
	template<typename U>
	U blab(U u, T t) { return (n.Value() + q.Value()) * u / t; }
	void Show() const { q.show(); n.show(); }
};

int main()
{
	beta<double> guy(3.5, 3);
	cout << "T was set to double\n";
	guy.Show();
	cout << "V was ste to T, which is double ,then V was set to int:\n";
	cout << guy.blab(10, 2.3) << endl;
	cout << "U was set to int\n";
	cout << guy.blab(10.0, 2.3) << endl;
	cout << "U was set to double\n";
	return 0;
}

就是可以嵌套。

如果在类外边的定义,在beta模板之外定义模板的方法的代码如下:

template<typename T>
  template<typename V>
  U beta<T>::blab(U u, T t)
  {
	  return  return (n.Value() + q.Value()) * u / t;
  }

 注意模板的嵌套和声明,是beta<T>的成员。

8.将模板用作参数

模板除了可以包含类型参数 和非类型参数,模板还可以包含本身就是模板的参数。

template <template<typename T> class Thing >
class Crab
{
private:
	Thing<int> s1;
	Thing<double> s2;
public:
	Crab() {};
	bool push(int a, double x) { return s1.push(a) && s2.push(x); }
	bool pop(int a, double x) { return s1.pop(a) && s2.pop(x); }
};

int main()
{
	using std::cout;
	using std::cin;
	using std::endl;
	Crab<Stack> nebula;
}

说明:

1. template<typename T> class 是类型,Thing是参数。

2.Crab<Stack> nebula;为了使上述声明被接受,模板参数Stack必须是一个模板类; bebula声明将用Stack<int> 和 Stack<double>代替Thing<int>和Thing<double>.

除此之外,我们还可以混合使用模板参数和常规参数,例如,Crab类的声明可以像这样打头:

template <template<typename T> class Thing, typename U,typename V >
class Crab
{
private:
	Thing<U> s1;
	Thing<V> s2;
public:
	Crab() {};
	bool push(int a, double x) { return s1.push(a) && s2.push(x); }
	bool pop(int a, double x) { return s1.pop(a) && s2.pop(x); }
};

现在成员s1和s2可存储的数据类型为泛型,而不是用硬编码指定的类型。模板参数T表示一种模板类型,类型参数U和V表示非模板类型(而不是非类型参数或表达式参数)。 

9.模板类和友元

模板类声明也可以有友元。模板的友元分为3类:非模板友元、约束模板友元、非约束模板友元。

1.非模板友元函数

在模板类中将一个常规函数声明为友元,该友元函数称为模板所有实例化的友元。该友元函数本身不是模板函数,而只是使用一个模板作为参数,这意味着必须为要使用的友元定义显式具体化。

#include<iostream>
using std::cout;
using std::endl;
 template<typename T>
 class HasFriend
 {
 private:
	 T item;
	 static int ct;
 public:
	 HasFriend(const T& i) :item(i) { ct++;}
	 ~HasFriend() { ct--; }
	 friend void counts();
	 friend void reports(HasFriend<T>&);
 };
 template<typename T>
 int HasFriend<T>::ct = 0;
 void counts()
 {
	 cout << "int count: " << HasFriend<int>::ct << " ;";
	 cout << "double count: " << HasFriend<double>::ct <<endl;
 }

 void reports(HasFriend<int>& hf)
 {
	 cout << "HasFriend<int>: " << hf.item << endl;
 }

 void reports(HasFriend<double>& hf)
 {
	 cout << "HasFriend<double>: " << hf.item << endl;
 }


 int main()
 {
	 cout << "No objects declared:";
	 counts();
	 HasFriend<int> hfi1(10);
	 cout << "After declared:";
	 counts();
	 HasFriend<int> hfi2(20);
	 cout << "After declared:";
	 counts();
	 HasFriend<double> hfdb(10.5);
	 cout << "After declared:";
	 counts();
	 reports(hfi1);
	 reports(hfi2);
	 reports(hfdb);
	 return 0;


 }

注意:这里的友元全都具体化了;counts()函数不是通过对象调用的,通过访问全局对象,或全局指针访问非全局对象,可以访问独立于对象的模板类的静态数据成员。

2.模板类的约束模板友元函数

约束类模板友元函数是使友元函数本身称为模板,使得类的每一个具体化都获得与友元匹配的具体化,是所有HasFriend类的友元,调用函数需要指明具体化。这分为3步:1.在类定义前声明每个模板函数;2.在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化(如果没有参数,必须使用模板参数语法来指明具体化);3.为友元提供模板定义。 

#include<iostream>
using std::cout;
using std::endl;
template<typename T> void counts();
template<typename T> void reports(T &);


template<typename T>
class HasFriend
{
private:
	T item;
	static int ct;
public:
	HasFriend(const T& i) :item(i) { ct++; }
	~HasFriend() { ct--; }
	friend void counts<T>();     //没有参数的显式指明类型  <>表明是个模板
	friend void reports<HasFriend<T>>(HasFriend<T>&);  //<>编译器可以从参数类型推断出要使用的具体化。使用<>格式也能获得同样的效果
};
template<typename T>
int HasFriend<T>::ct = 0;

template<typename T>
void counts()
{
	cout << "template size: " << sizeof(HasFriend<T>) << " ;";
	cout << "count: " << HasFriend<T>::ct << endl;
}

template<typename T>
void reports(T& hf)
{
	cout << "HasFriend<T>: " << hf.item << endl;
}



int main()
{

	cout << "No objects declared:";
	counts<int>();
	HasFriend<int> hfi1(10);
	cout << "After declared:";
	counts<int>();
	HasFriend<int> hfi2(20);
	cout << "After declared:";
	counts<int>;
	HasFriend<double> hfdb(10.5);
	cout << "After declared:";
	counts<double>();
	reports(hfi1);
	reports(hfi2);
	reports(hfdb);
	return 0;


}

3.模板类的非约束模板友元函数

前一节中的约束模板友元函数是在类外边声明的模板的具体化。通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数使不同的:

 

#include<iostream>
using std::cout;
using std::endl;
template<typename T>
class HasFriend
{
private:
	T item;
	static int ct;
public:
	HasFriend(const T& i) :item(i) { ct++; }
	~HasFriend() { ct--; }
	template<typename C, typename D> friend void show2(C&, D&);
};
template<typename T>
int HasFriend<T>::ct = 0;

template<typename C, typename D> void show2(C& c, D& d)   //直接显式两个对象
{
	cout << c.item << "," << d.item << endl;
}



int main()
{

	cout << "No objects declared:";
	HasFriend<int> hfi1(10);
	cout << "After declared:";
	HasFriend<int> hfi2(20);
	cout << "After declared:";
	show2(hfi1, hfi2);
	HasFriend<double> hfdb(10.5);
	cout << "After declared:";
	show2(hfi1, hfdb);
	
	return 0;


}

10.模板别名

如果能为类型指定别名,将很方便。

1.使用typedef为模板具体化指定别名:

typedef std::array<double,12> arrd;
arrd gallons;

2.使用模板提供一系列别名:

template<typename T>
using arrtype = std::array<T,12>;

arrtype<double> gallons;

3.C++y允许将语法using = 用于非模板。用于非模板,这种语法与常规typedef等价:

using std::array<double,12> = arrd;
arrd gallons;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值