C++学习笔记——模板

笔记整理自:黑马C++视频教程

什么是模板?
模板就是通用的工具,用于提高代码复用性。
模板只是一个架子,不能直接使用。
模板只是通用,并不是万能。

为什么要学习模板?
学习模板并不是为了写模板,而是在STL中能够运用系统提供的模板

C++ 中的模板

C++ 的编程思想之一【泛型编程】,主要是有点额技术就是【模板】。
C++ 有两种模板机制:函数模板和类模板。

函数模板

函数模板:在声明、定义函数时,涉及到的函数返回值类型、参数类型可以使用一个虚拟的类型代替,而不用指定具体的类型,这样就大大提高了该函数的通用性。

函数模板的语法

template<typename T>
紧接着写函数声明、定义
  • template 关键字:声明创建模板
  • typename 关键字:声明使用虚拟类型,可以使用 class 关键字代替
  • T :模板中是有点额虚拟类型的名称,可以自定义

函数模板使用

例如,编写一个交换两个变量值得函数,需要针对不同的数据类型编写对应的交换函数:
交换两个Int 类型的值如下:

//交换整型函数
void swapInt(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

//交换浮点型函数
void swapDouble(double& a, double& b) {
	double temp = a;
	a = b;
	b = temp;
}

int main() {
	int a = 1;
	int b = 2;
	swapInt(a,b);
	
	double c = 1.25;
	double d = 2.33;
	swapDouble(c,d);
	
	system("pause");
	return 0;
}

如果还需要交换其他类型的,还需要再编写对应类型的交换函数,类似这种相同逻辑的代码就可以使用函数模板来提高复用性:

  • 函数模板定义
// 利用模板提供通用的交换函数
template<typename T>
void mySwap(T& a, T& b)
{
	T temp = a;
	a = b;
	b = temp;
}
  • 函数模板调用方式一:自动类型推导
int main() {
	// 自动类型推导
	int a = 1;
	int b = 2;
	my_swap(a, b);
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	system("pause");
	return 0;
}
  • 函数模板调用方式二:显示指定类型
int main() {
	// 显示指定类型
	float c = 1.25f;
	float d = 0.05f;
	my_swap<float>(c, d);
	cout << "c = " << c << endl;
	cout << "d = " << d << endl;
	system("pause");
	return 0;
}

函数模板使用注意事项

  • 自动类型推导,必须能够推导出一致的数据类型才能调用
  • 必须能够确定出 T 的类型才能调用函数
//利用模板提供通用的交换函数
template<class T>
void mySwap(T& a, T& b)
{
	T temp = a;
	a = b;
	b = temp;
}

// 1、自动类型推导,必须推导出一致的数据类型T,才可以使用
void test01()
{
	int a = 10;
	int b = 20;
	char c = 'c';

	mySwap(a, b); // 正确,可以推导出一致的T
	//mySwap(a, c); // 错误,推导不出一致的T类型
}

// 2、模板必须要确定出T的数据类型,才可以使用
template<class T>
void func()
{
	cout << "func 调用" << endl;
}

void test02()
{
	//func(); //错误,模板不能独立使用,必须确定出T的类型
	func<int>(); //利用显示指定类型的方式,给T一个类型,才可以使用该模板
}

int main() {

	test01();
	test02();

	system("pause");

	return 0;
}

函数模板实现数组排序案例

利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序
排序算法采用选择排序和冒泡排序进行升序排序

#include <iostream>
#include <string>
using namespace std;

// 打印数组
template<class T>
void print_array(T arr[], int size) {
	for (int i = 0; i < size; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
}

// 使用函数模板实现选择排序
template<class T>
void selection_sort(T* arr, int size) {
	for (int i = 0; i < size; i++) {
		int min_index = i;
		for (int j = i + 1; j < size; j++) {
			if (arr[j] < arr[min_index]) {
				min_index = j;
			}
		}
		if (min_index != i) {
			T temp = arr[i];
			arr[i] = arr[min_index];
			arr[min_index] = temp;
		}
	}
}
// 使用函数模板实现冒泡排序
template<class T >
void bubble_sort(T* arr,int size) {
	for (int i = 0; i < size - 1; i++) {
		for (int j = 0; j < size - 1 - i; j++) {
			if (arr[j] > arr[j + 1]) {
				T temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}

// 测试选择排序:int型
void test_02_1() {
	int arr[10] = { 2,1,4,7,3,8,9,0,5,6 };
	print_array(arr, 10);
	selection_sort(arr, 10);
	print_array(arr, 10);
}
// 测试选择排序:char 型
void test_02_2() {
	char arr[10] = { 'S','s','A','b','a','C','c','D','e','d'};
	print_array(arr, 10);
	selection_sort(arr, 10);
	print_array(arr, 10);
}
// 测试选择排序:int型
void test_02_3() {
	int arr[10] = { 2,1,4,7,3,8,9,0,5,6 };
	print_array(arr, 10);
	bubble_sort(arr, 10);
	print_array(arr, 10);
}
// 测试选择排序:char 型
void test_02_4() {
	char arr[10] = { 'S','s','A','b','a','C','c','D','e','d' };
	print_array(arr, 10);
	bubble_sort(arr, 10);
	print_array(arr, 10);
}

int main02() {
	test_02_1();
	test_02_2();
	test_02_3();
	test_02_4();
	
	return 0;
}

普通函数与函数模板的区别

  • 普通函数调用时可以发生自动类型转换(隐式类型转换)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换

建议使用显示指定类型的方式,调用函数模板,因为可以自己确定通用类型T

#include <iostream>
#include <string>

using namespace std;

// 普通函数
void add_01(int a, int b) {
	cout << a + b << endl;
}
// 函数模板
template<class T>
void add_02(T a, T b) {
	cout << a + b << endl;
}

// 测试普通函数下的类型转换
void test03_1() {
	int a = 1;
	char b = 'A';
	// 能够自动将 char 类型转为 int 类型传入进行函数调用
	add_01(a, b);
}
void test03_2() {
	int a = 1;
	char b = 'A';
	// 如果采用自动类型推导,不能自动类型转换
	// add_02(a, b);
	// 需要显示的指定T的类型,才能发生数据类型转换
	add_02<int>(a, b);
}

int main() {
	test03_1();
	test03_2();

	return 0;
}

普通函数与函数模板的调用优先级及规则

  • 如果两者都能正常调用得到相同的目标结果,会优先调用普通函数
  • 可以通过空模板参数列表来强制调用函数模板
  • 模板函数可以发生重载
  • 如果函数模板能够更好的匹配,优先调用函数模板

总结:既然提供了函数模板,最好就不要再提供普通函数,否则容易出现二义性

#include <iostream>
#include <string>

using namespace std;

// 普通函数
void func(int a,int b) {
	cout << "调用普通函数" << endl;
}
// 模板函数
template<class T>
void func(T a,T b) {
	cout << "调用模板函数" << endl;
}

// 模板函数重载
template<class T>
void func(T a,T b,T c) {
	cout << "调用重载的模板函数" << endl;
}

// 普通函数与模板函数的调用优先级规则:两者都能实现,有限调用普通函数
void test04_1() {
	int a = 1;
	int b = 2;
	func(a, b); // 调用普通函数
}
// 普通函数与模板函数的调用优先级规则:通过空模板参数列表来强制调用函数模板
void test04_2() {
	int a = 1;
	int b = 2;
	func<>(a, b); // 调用模板函数
}
// 普通函数与模板函数的调用优先级规则:模板函数可以发生重载
void test04_3() {
	int a = 1;
	int b = 2;
	int c = 3;
	func<>(a,b); // 调用模板函数
	func(a,b,c); // 调用重载的模板函数
}

// 普通函数与模板函数的调用优先级规则:如果函数模板能够更好的匹配,优先调用函数模板
void test04_4() {
	char a = 1;
	char b = 2;
	func(a, b); // 调用模板函数
}

int main() {
	test04_4();
	
	return 0;
}

函数模板的局限性

  • 模板的通用性并不是万能的
  • 内置基本数据类型可以直接使用函数模板
  • 自定义的数据类型或数组等复杂类型,调用通用模板时就会受到限制

如下定义了一个比较函数模板,如果传入 基本数据类型的参数,可以正常调用,但是如果传入的是一个数组或者一个类,就无法正常运行

//普通函数模板
template<class T>
bool myCompare(T& a, T& b)
{
	if (a == b)
	{
		return true;
	}
	else
	{
		return false;
	}
}

针对这种局限,C++ 提供模板的重载机制,可以为这些特定的类型提供具体化的模板

#include <iostream>
#include <string>

using namespace std;

class Person {

public:
	string name;
	int age;

	Person(string name, int age) {
		this->name = name;
		this->age = age;
	}
};

// 普通函数模板
template<class T>
int my_compare(T a, T b) {
	if (a == b) {
		return 0;
	}
	else if (a > b) {
		return 1;
	}
	else {
		return -1;
	}
}

// 具体化的函数模板,特殊处理Person类
template<> int my_compare(Person a, Person b) {
	if (a.name == b.name && a.age == b.age) {
		return 0;
	}
	else if (a.age > b.age) {
		return 1;
	}
	else {
		return -1;
	}
}

// 内置基本数据类型可以直接使用函数模板
void test05_1() {
	cout << my_compare(1, 2) << endl;
}

// 自定义数据类型或复杂数据类型不能直接使用函数模板
// 可以创建具体化的Person数据类型的模板,用于特殊处理这个类型
void test05_2() {
	Person p1("zhangsan",19);
	Person p2("zhangsan",19);
	cout << my_compare(p1, p2) << endl; // 直接使用普通模板,运行出错
}

int main() {
	test05_2();
	return 0;
}

类模板

类模板:建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表。

类模板的语法

template<typename T>
紧接着编写类的声明、定义
  • template 关键字:声明创建模板
  • typename 关键字:声明使用虚拟类型,可以使用 class 关键字代替
  • T :模板中是有点额虚拟类型的名称,可以自定义

类模板的使用

编写一个类,类中成员变量的数据类型和成员函数返回值、参数类型都使用虚拟的数据类型代替,实例化类的对象时,再指定具体的类型:

#include <iostream>
#include <string>

using namespace std;

template<class T>
class Student {
private:
	T pro;
public:
	void setPro(T pro) {
		this->pro = pro;
	}
	T getPro() {
		return pro;
	}
};

int main() {
	Student<string> s1;
	s1.setPro("Hello");
	string pro1 = s1.getPro();

	Student<int> s2;
	s2.setPro(2);
	int pro2 = s2.getPro();

	return 0;
}

类模板与函数模板的区别

  • 使用方式类似
  • 函数模板的虚拟类型不能设置默认值,类模板的虚拟类型可以有默认值
  • 函数模板可以使用自动类型推导方式,类模板使用时没有自动类型推导,只能显示的指定具体类型
#include <iostream>
#include <string>

using namespace std;

// 类模板
// 可以定义多个虚拟类型
// 与函数模板不同:类模板中,类型参数可以指定默认值
template<class NameType,class AgeType = int>
class Demo {
private:
	NameType name;
	AgeType age;
public:
	Demo(NameType name, AgeType age) {
		this->name = name;
		this->age = age;
	}
	NameType getName() {
		return name;
	}
	void setName(NameType name) {
		this->name = name;
	}
	AgeType getAge() {
		return age;
	}
	void setAge(AgeType age) {
		this->age = age;
	}
	void info() {
		cout << "name = " << name << ",age = " << age << endl;
	}
};


int main() {
	// 与函数模板不同,类模板不能使用自动类型推导方式,
	// Demo<> demo1("jieke",10);
	// demo1.info();

	// 必须显示指定类型
	Demo<char, string> demo2('A', "三岁");
	demo2.info();
	demo2.setName('a');
	string demo2_age = demo2.getAge();

	// 如果虚拟类型定义了默认值,使用时没有指定的话,就使用默认的类型
	Demo<string> demo3("托尼", 18);
	demo3.info();

	return 0;
}

类模板中成员函数创建时机

类模板中成员函数和普通类中成员函数创建时机是有区别的:

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建
#include <iostream>
#include<string>

using namespace std;

class Acc {
public:
	void show_info_a() {
		cout << "Acc 类中的成员函数被调用" << endl;
	}
};
class Bcc {
public:
	void show_info_b() {
		cout << "Bcc 类中的成员函数被调用" << endl;
	}
};


template <class T>
class TestClass {
public:
	T obj;
	void func1() {
		obj.show_info_a();
	}
	void func2() {
		obj.show_info_b();
	}
};

void test07_1() {
	Acc acc;
	TestClass<Acc> tc;

	tc.func1(); // 可以正常调用

	// 不能正常调用
	// 报错:"show_info_b": 不是 "Acc" 的成员
	// 说明类模板中的成员函数不是一开始就生成,而是在调用类模板时才生产的
	// tc.func2();
}
void test07_2() {
	Bcc bcc;
	TestClass<Bcc> tc;

	// 不能正常调用
	// 报错:"show_info_a": 不是 "Bcc" 的成员
	// 说明类模板中的成员函数不是一开始就生成,而是在调用类模板时才生产的
	// tc.func1(); 

	// 可以正常调用
	tc.func2();
}

int main() {
	test07_2();
	return 0;
}

类模板的对象作为函数参数

类模板实例化出的对象,向函数传参的方式有三种:

  • 指定传入的类型:给类模板中的虚拟参数指定具体的类型传入
  • 参数模板化:在传参时,继续将类模板中的虚拟参数模板化
  • 整个类模板化:将作为函数参数的对象类型进行模板化
#include<iostream>
#include<string>

using namespace std;


template<class T1,class T2>
class Foo {
public:
	T1 pre1;
	T2 pre2;

	Foo(T1 p1, T2 p2) {
		pre1 = p1;
		pre2 = p2;
	}

	void info() {
		cout << pre1 << " " << pre2 << endl;
	}
};
// 类模板对象作为函数参数:显示指定对象的数据类型
void func1(Foo<int,float> foo) {
	foo.info();
}

// 类模板对象作为函数参数:函数参数模板化
template<class T1,class T2>
void func2(Foo<T1,T2> foo) {
	// 使用 typeid 函数获取虚拟参数的类型,name()函数获取类型名称
	cout << "T1 的实际类型为:" << typeid(T1).name() << endl;
	cout << "T2 的实际类型为:" << typeid(T2).name() << endl;

	foo.info();
}

// 类模板对象作为函数参数:将这个对象类型进行模板化,再传递
template<class T>
void func3(T foo) {
	cout << "T 的实际类型为:" << typeid(T).name() << endl;

	foo.info();
}

int main() {
	Foo<int, float> foo1(2, 1.25f);
	func1(foo1);

	Foo<string, bool> foo2("HelloC++", false);
	func2(foo2);

	Foo<string, string> foo3("Hello", "World");
	func3(foo3);
	return 0;
}

类模板继承

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活指定出父类中T的类型,子类也需变为类模板
#include<iostream>
#include<string>

using namespace std;

template<class T>
class Parent {
public:
	T pre;
	void info() {
		cout << "pre 的类型为:" << typeid(T).name() << endl;
		cout << "pre 的值为:" << pre << endl;
	}
};

// 类模板继承:1.子类继承时指定父类中的虚拟类型的具体类型
class Child01 : public Parent<string> {
	
};

// 类模板继承:2.如果子类继承时没有指定父类中的虚拟类型的具体类型,那么子类也应该定义为类模板
template<class T1,class T2>
class Child02 :public Parent<T2> {
public:
	T1 c_pre;
	Child02(T1 a, T2 b) {
		c_pre = a;
		this->pre = b;
	}
	
	void info() {
		cout << "c_pre 的类型为:" << typeid(T1).name() << endl;
		cout << "c_pre 的值为:" << c_pre << endl;
		// 调用父类中的方法
		Parent<T2>::info();
	}
};

int main() {

	Child01 c1;
	c1.pre = "Hello";
	c1.info();

	Child02<int, double> c2(2,3.14);
	c2.info();

	return 0;
}

类模板中的函数在类外实现

#include<iostream>
#include<string>

using namespace std;

template<class T>
class Boo {

private:
	T pre;
public:
	// 类内声明构造函数
	Boo(T p);
	// 类内声明成员函数
	T getPre();
};

// 类外实习构造函数
template<class T>
Boo<T>::Boo(T p) {
	this->pre = p;
}
// 类外实习成员函数
template<class T>
T Boo<T>::getPre() {
	return this->pre;
}

int main() {
	Boo<string> boo("Hello");
	string s = boo.getPre();
	cout << s << endl;
	return 0;
}

类模板分文件编写

类模板分文件编写存在的问题

以常规方式将类模板进行分文件编写:
① 头文件 demo.h

#pragma once

#include<iostream>
#include<string>

using namespace std;

template<class T1,class T2>
class Demo {
private:
	T1 pre1;
	T2 pre2;
public:
	// 声明函数
	Demo(T1 p1, T2 p2);
	void info();
};

② 源文件 demo.cpp

#include"demo.h"

// 实现函数
template<class T1,class T2>
Demo<T1, T2>::Demo(T1 p1, T2 p2) {
	this->pre1 = pre1;
	this->pre2 = pre2;
}
template<class T1, class T2>
void Demo<T1, T2>::info() {
	cout << "pre1的类型:" << typeid(T1).name() << endl;
	cout << "pre2的类型:" << typeid(T2).name() << endl;
}

③ test.cpp 中引入头文件 demo.h

#include"demo.h"

using namespace std;

int main() {
	Demo<int, string> demo(100, "Hello"); // 无法解析的外部命令
	demo.info(); // 无法解析的外部命令
	return 0;
}

因为类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到

类模板分文件编写解决方案

解决方式1:
直接包含 demo.cpp 源文件

#include"demo.cpp"

using namespace std;

int main() {
	Demo<int, string> demo(100, "Hello"); // 正常执行
	demo.info(); // 正常执行
	return 0;
}

解决方案2(主流的方案):
将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制
① 头文件 demo.hpp

#pragma once
#include<iostream>
#include<string>

using namespace std;

template<class T1, class T2>
class Demo {
private:
	T1 pre1;
	T2 pre2;
public:
	// 声明函数
	Demo(T1 p1, T2 p2);
	void info();
};

// 函数实现
template<class T1, class T2>
Demo<T1, T2>::Demo(T1 p1, T2 p2) {
	this->pre1 = pre1;
	this->pre2 = pre2;
}
template<class T1, class T2>
void Demo<T1, T2>::info() {
	cout << "pre1的类型:" << typeid(T1).name() << endl;
	cout << "pre2的类型:" << typeid(T2).name() << endl;
}

② test.cpp 中引入头文件 demo.hpp

#include"demo.hpp"

using namespace std;

int main() {
	Demo<int, string> demo(100, "Hello"); // 正常执行
	demo.info(); // 正常执行
	return 0;
}

类模板做友元

① 全局函数做类模板的友元:全局函数在类内实现

#include <iostream>
#include <string>

using namespace std;

template<class T>
class Hoo {
	// 全局函数做类模板的友元:类内实现全局函数(推荐:用法简单,而且编译器可以直接识别)
	friend void func1(Hoo<T>& hoo) {
		cout << hoo.pre << endl;
	}
private:
	T pre;
public:
	Hoo(T p) {
		this->pre = p;
	}
};

int main() {
	Hoo<string> hoo1("Hello");
	func1(hoo1); // Hello
	return 0;
}

② 全局函数做类模板的友元:类外实现全局函数(需要提前让编译器知道全局函数的存在)

#include <iostream>
#include <string>

using namespace std;

// 先声明类
template<class T> class Hoo;

// 将全局函数写在类前面,让编译器直接先找到全局函数

template<class T> 
void func(Hoo<T>& hoo) {
	cout << hoo.pre << endl;
}


template<class T>
class Hoo {
	friend void func<>(Hoo<T>& hoo);
private:
	T pre;
public:
	Hoo(T p) {
		this->pre = p;
	}
};

int main() {
	Hoo<int> hoo(100);
	func(hoo);
	return 0;
}

类模板案例

封装一个通用的数组类:

  • 可以对内置数据类型以及自定义数据类型的数据进行存储
  • 将数组中的数据存储到堆区
  • 构造函数中可以传入数组的容量
  • 提供对应的拷贝构造函数以及operator=防止浅拷贝问题
  • 提供尾插法和尾删法对数组中的数据进行增加和删除
  • 可以通过下标的方式访问数组中的元素
  • 可以获取数组中当前元素个数和数组的容量

① 头文件 myarray.hpp

#pragma once
#include<iostream>
#include<string>
using namespace std;

template<class T>
class MyArray {
private:
	T* paddr; // 指向堆中存储真正数据的指针
	int capacity; // 数组容量
	int size; // 数组元素个数

public:
	// 构造函数
	MyArray(int capacity) {
		// 初始化数组初始容量
		this->capacity = capacity;
		// 初始化数组初始大小
		this->size = 0;
		// 初始化数组初始数据指针:以当前容量在堆中创建某个类型的数组,并且将数据指针指向该数组
		this->paddr = new T[this->capacity];
	}
	// 析构函数
	~MyArray() {
		cout << this << endl;
		if (this->paddr != NULL) {
			delete[] this->paddr; // 释放堆中的内存
			this->paddr = NULL; // 防止野指针出现
			this->capacity = 0;
			this->size = 0;
		}
	}

	// 获取数组容量大小
	int getCapacity() {
		return this->capacity;
	}
	// 获取当前数组元素个数
	int getSize() {
		return this->size;
	}

	// 复制构造函数
	MyArray(const MyArray& source) {
		// 根据源数组初始化
		this->capacity = source.capacity;
		this->size = source.size;
		this->paddr = new T[this->capacity];
		// 将原数组中的元素进行深拷贝
		for (int i = 0; i < this->size; i++){
			// 如果T为对象,而且还包含指针,T类中必须需要重载 = 操作符,因为这个等号不是 构造 而是赋值,
			// 普通类型可以直接= 但是指针类型需要深拷贝
			this->paddr[i] = source.paddr[i];
		}
	}

	// 重载 赋值操作符
	MyArray& operator=(const MyArray& source) {
		if (this->paddr != NULL) {
			delete[] this->paddr;
			this->capacity = 0;
			this->size = 0;
		}
		this->capacity = source.capacity;
		this->size = source.size;
		this->paddr = new T[this->capacity];
		for (int i = 0; i < this->size; i++) {
			// 如果数组元素是对象,而且包含指针,需要重载 = 操作符,避免浅拷贝带来的问题
			this->paddr[i] = source[i];
		}
		// 返回对象本身
		return *this;
	}

	// 重载 [] 操作符
	// 返回元素的引用
	T& operator[](int index) {
		// 不考虑数组越界
		return this->paddr[index];
	}
	// 向数组尾部插入一个元素
	bool add(const T& e) {
		// 如果数组已满插入失败
		if (this->size == this->capacity) {
			return false;
		}
		this->paddr[size] = e;
		this->size++;
	}
	// 删除数组最后一个元素
	bool remove() {
		if (this->size > 0) {
			this->size--;
			return true;
		}
		return false;
	}
	// 遍历数组
	void show() {
		for (int i = 0; i < this->size; i++)
		{
			cout << this->paddr[i] << " ";
		}
		cout << endl;
	}

};

② 测试

#include"myarray.hpp"
using namespace std;



};
// 重写 左移操作符
ostream& operator<<(ostream& out, MyDog& dog) {
	out << "姓名:" << *dog.name << ",年龄:" << *dog.age;
	return out;
}

int main() {

	MyArray<int> arr1(5);
	arr1.add(1);
	arr1.add(2);
	arr1.add(3);
	arr1.show();
	
	MyArray<int> arr2(arr1);
	arr2.add(4);
	arr2.show();
	
	MyArray<int> arr3 = arr2;
	arr3.remove();
	arr3.remove();
	arr3.show();

	cout << arr3[2] << endl;

	system("pause");

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值