⚡【C++要笑着学】(14) 模板:泛型编程 | template | 函数模板 | 函数模板实例化 | 类模板

   ​​​​​​ 🤣 爆笑教程 👉  《C++要笑着学》 👈 火速订阅  🔥

 🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。
了解更多: 👉 "不太正经" 的专栏介绍 试读第一章
订阅链接: 🔗《C语言趣味教程》 ← 猛戳订阅!

   本篇博客全站热榜排名:3  🔥

  • 💭 写在前面:本章将正式开始介绍C++中的模板,为了能让大家更好地体会到用模板多是件美事!我们将会举例说明,大家可以试着把自己带入到文章中,跟着思路去阅读和思考,真的会很有意思!如果你对网络流行梗有了解,读起来将会更有意思!

Ⅰ. 泛型编程(Generic Programming)

0x00 引入:通用的交换函数

C 语言中实现两数交换,如果不用花的方法,比如异或啥的,中规中矩的写法是通过 tmp 交换。

比如我们这里想交换 变量a变量b 的值,我们可以写一个 Swap 函数:

void Swap(int* px, int* py) {
	int tmp = *px;  // 创建临时变量,存储a的值
	*px = *py;      // 将b的值赋给a
	*py = tmp;      // 让b从tmp里拿到a的值
}

int main(void)
{
	int a = 0, b = 1;
	Swap(&a, &b);   // 传址

	return 0;
}

变量a 和 变量b 是整型,如果现在有了是浮点型的 变量c 和 变量d。

还可以用我们这个整型的 Swap 函数交换吗?

void Swap(int* px, int* py) {  
	int tmp = *px; 
	*px = *py;      
	*py = tmp;      
}

int main(void)
{
	int a = 0, b = 1;
	double c = 1.1, d = 2.2;  // 浮点型
	Swap(&a, &b);
	Swap(&c, &d);

	return 0;
}

这肯定是不行的,因为我们实现的 Swap 函数接受的是整形数据,这里传的是浮点数了。

 我们可以再写一个浮点数版本的 Swap 函数…… 叫 SwapDouble ???

void SwapDouble(double* px, double* py) {
	double tmp = *px; 
	*px = *py;      
	*py = tmp;      
}

不错,问题是解决了。但是我现在又出现了字符型的 变量e 和 变量f 呢?

 ……

那我现在又出现了各种乱七八糟的类型呢?

"欸他码码滴!有完没完!"

SwapIntSwapDoubleSwapChar 真是乱七八糟的,

❓ 能不能实现一个通用的 Swap 函数呢?

 那我们不用C语言了!我们用 C++,C++ 里面不是有 函数重载 嘛!

用 C++ 我们还能用引用的方法交换呢,直接传引用,取地址符号都不用打了,多好!

💬 test.cpp: 于是咔咔咔,改成了C++之后 ——

void Swap(int& rx, int& ry) {
	int tmp = rx;
	rx = ry;
	ry = tmp;
}
void Swap(double& rx, double& ry) {
	double tmp = rx;
	rx = ry;
	ry = tmp;
}
void Swap(char& rx, char& ry) {
	char tmp = rx;
	rx = ry;
	ry = tmp;
}

int main(void)
{
	int a = 0, b = 1;
	double c = 1.1, d = 2.2;
	char e = 'e', f = 'f';

	Swap(a, b);
	Swap(c, d);
	Swap(e, f);

	return 0;
}

 场面一度尴尬…… 

好像靠函数重载来调用不同类型的 Swap,只是表面上看起来 "通用" 了 ,

实际上问题还是没有解决,有新的类型,还是要添加对应的函数…… 

❌ 用函数重载解决的缺陷:

① 重载的函数仅仅是类型不同,代码的复用率很低,只要有新类型出现就需要增加对应的函数。

② 代码的可维护性比较低,一个出错可能导致所有重载均出错。

 哎!要是能像做表情包那样简单就好了……

你看我做表情,有些是可以靠模板去制作的,比如这种 "狂粉举牌" 表情:

这就是模板!如果在C++中也能够存在这样一个模板该有多好?

就像这里,只要在板子上写上名字(类型),

就可以做出不同的 "举牌表情"(生成具体类型的代码)。

 那将会节省很多头发!

" 巧妙的是!C++里面有这种神器!!!"

而且大佬已经把神器打造好了,你只要学会如何使用就能爽到飞起!

下面让我们开始函数模板的学习!在这之前我们再来科普一下什么是泛型编程。

0x01 什么是泛型编程

泛型编程是一种编程风格,其中算法以尽可能抽象的方式编写,而不依赖于将在其上执行这些算法的数据形式。这个概念在 1989 年由 David Musser 和 Alexander A. Stepanov 首次提出。

 泛型,就是针对广泛的类型的意思。

泛型编程: 编写与类型无关的调用代码,是代码复用的一种手段。 模板是泛型编程的基础。

Ⅱ. 函数模板(Template)

0x00 函数模板的概念

上面我们提到了 "神器" ,现在我们来学会如何去使用它,我们先来介绍一下概念。

 📚 函数模板代表了一个函数家族,该函数模板与类型无关,

在使用时被参数化,根据实参类型产生函数的特定类型版本。

0x01 函数模板格式

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}

template 是定义模板的关键字,后面跟的是尖括号 < >

typename 是用来定义模板参数的关键字

T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。

 👈 就像这个表情包模板,我给他取名为 "狂粉举牌" 表情。

💬 解决刚才的问题:

① 我们来定义一个叫 Swap 的函数,我们这不给具体的类型:

void Swap();

② 然后在它的前面定义一个具体的类型:

template<typename T>    // template + <typename 模板名>
void Swap();

 ③ 这时候,我们就可以用这个模板名来做类型了:

template<typename T>         // 模板参数列表 ———— 参数类型
void Swap(T& rx, T& ry) {    // 函数参数列表 ———— 参数对象
	T tmp = rx;
	rx = ry;
	ry = tmp;
}

这,就是函数模板!虽然参数的名字我们可以自己取 (你写成 TMD 也没人拦你 )

 但是我们一般喜欢给它取名为 T,因为 T 代表 Type(类型),

有些地方也会叫 TP、TY、X ,或者 KV 结构(key-value-store)我们还会给它取名为 Key,

而且,如果你需要多个类型,也是可以定义多个类型的:

template<typename T1, typename T2, typename T3>

📌 注意事项:

函数模板不是一个函数,因为它不是具体要调用的某一个函数,而是一个模板。就像 "好学生",主体是学生,"好" 是形容 "学生" 的;这里也一样,"函数模板" 是模板,所以 函数模板表达的意思是 "函数的模板" 。所以,我们一般不叫它模板函数,应当叫作函数模板。

"函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。"  —— 《百度百科》

我们在用 template< > 定义模板的时候,尖括号里的 typename 其实还可以写成 class

template<class T>     // 使用class充当typename (具体后面会说)
void Swap(T& rx, T& ry) {
	T tmp = rx;
	rx = ry;
	ry = tmp;
}

我反正是挺喜欢写 class 的,因为比 typename 好敲,更顺手些,这个看个人习惯和团队要求。

🚩 现在我们把完整的代码跑一下看看:

template<typename T>
void Swap(T& rx, T& ry) {
	T tmp = rx;
	rx = ry;
	ry = tmp;
}

int main(void)
{
	int a = 0, b = 1;
	double c = 1.1, d = 2.2;
	char e = 'e', f = 'f';

	Swap(a, b);
	Swap(c, d);
	Swap(e, f);

	return 0;
}

 (代码成功运行)

🐞 调试:打开监视看看是否都成功交换了

搞定!我们使用模板成功解决了问题,实现了通用的 Swap 函数!

如果是自定义类型,函数里面就要是拷贝构造,你要实现好就行。

因为 T 没有规定是什么类型,所以任意类型都是可以的,内置类型和自定义类型都可以的。

 真是太香了!这,就是模板!

0x02 模板函数的原理

❓ 思考:这下面三个调用调用的是同一个函数吗?

🔑 不是同一个函数!这三个函数执行的指令是不一样的,你可以这么想,

它们都需要建立栈帧,栈帧里面是要开空间的,你就要给 rx 开空间,

rx 的类型都不一样(double int char)。所以当然调用的不是同一个函数了。

 我们来思考一下模板函数的原理是什么。

比如说我现在想把杜甫写的《登高》做出一万份出来,怎么做?

 

 

最后我们传递出去的也不是印诗的模具,而是印出来的纸,

不管是手抄还是印刷,传递出去的都是纸。所以我们再来看这里的代码:

template<typename T>
void Swap(T& rx, T& ry) {
	T tmp = rx;
	rx = ry;
	ry = tmp;
}

int main(void)
{
	int a = 0, b = 1;
	double c = 1.1, d = 2.2;
	char e = 'e', f = 'f';

	Swap(a, b);
	Swap(c, d);
	Swap(e, f);

	return 0;
}

和上面说的一样,我们不会把印诗的模具传递出去,而是印出来的纸,

 所以这里调用的当然不是模板,而是这个模板造出来的东西。

而函数模板造出 "实际要调用的" 的过程,叫做模板实例化。

编译器在调用之前会干一件事情 —— 模板实例化。

我们下面就来探讨一下模板实例化。

Ⅲ. 函数模板实例化(Template Instantiation)

0x00 引入:这些不同类型的 Swap 函数是怎么来的?

int a = 0, b = 1;
Swap(a, b);

编译器在调用 Swap(a, b) 的时候,发现 a b 是整型的,编译器就开始找,

虽然没有找到整型对应的 Swap,但是这里有一份模板 —— 

template<typename T>   // 大家好我是模板,飘过~
void Swap(T& rx, T& ry) {
	T tmp = rx;
	rx = ry;
	ry = tmp;
}

这里要的是整型,编译器就通过这个模板,推出一个 T int 类型的函数。

这时编译器就把这个模板里的 T 都替换成 int,生成出一份 T int 的函数。

char e = 'e', f = 'f';
Swap(e, f);

一样的,如果要调用 Swap(e, f) ,e f 是字符型,编译器就会去实例化出一个 char 的。

 

你调的函数还是那些函数,只是你写一份模板出来,让编译器去用模板生成那些函数。

前面注意事项那里我们说过,函数模板本身不是函数。

它是是编译器使用方式产生特定具体类型函数的模具,在编译器编译阶段,

对于模板函数的使用,编译器需要根据传入的实参类型来推演,生成对应类型的函数以供调用。

比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,

T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于字符类型也是如此。

0x01 转到反汇编观察

🐞 调试:我们刚才调试的时候在监视窗口已经看到了,它们的值成功交换了。

现在我们再调试一次,这次转到反汇编,去验证一下编译器通过模板生成函数这件事:

0x02 模板实例化的定义

模板将我们本来应该要重复做的活,交给了编译器去做。

编译器不是人,它不会累,让编译器拿着模板实例化就完事了。

 用手搓衣服舒服,还是用洗衣机洗舒服?

 自己手写舒服,还是编译器自己去生成舒服?

📚 用不同类型的参数使用模板参数时,成为函数模板的实例化。

模板参数实例化分为:隐式实例化 显式实例化 ,下面我们来分别讲解一下这两种实例化。

0x03 模板的隐式实例化

📚 定义:让编译器根据实参,推演模板函数的实际类型。

我们刚才讲的 Swap 其实都是隐式实例化,就是让编译器自己去推。

💬 现在我们再举一个 Add 函数模板做参考:

#include <iostream>
using namespace std;

template<class T>
T Add(const T& x, const T& y) {
	return x + y;
}

int main(void)
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;

	return 0;
}

❓ 现在思考一个问题,如果出现 a1 + d2 这种情况呢?实例化能成功吗?

Add(a1, d2);

这必然是失败的, 因为会出现冲突。一个要把它实例化成 int ,一个要把它实例成 double

💡  解决方式

① 传参之前先进行强制类型转换,非常霸道的解决方式:

template<class T>
T Add(const T& x, const T& y) {
	return x + y;
}

int main(void)
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;

	cout << Add((double)a1, d2) << endl;

	return 0;
}

② 写两个参数,那么返回的参数类型就会起决定性作用:

#include <iostream>
using namespace std;

template<class T1, class T2>
T1 Add(const T1& x, const T2& y) {   // 那么T1就是int,T2就是double
	return x + y;      // 范围小的会像范围大的提升,int会像double "妥协"
} // 最后表达式会是一个double,但是最后返回值又是T1,是int,又会转

int main(void)
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;

	cout << Add(a1, d2) << endl;   // int,double  👆

	return 0;
}

当然,这种问题严格意义上来说是不会用多个参数来解决的,

 这里只是想从语法上演示一下,我们还有更好地解决方式,我们继续往下看。

③ 我们还可以使用 "显式实例化" 来解决:

Add<int>(a1, d2);     // 指定实例化成int
Add<double>(a1, d2)   // 指定实例化成double

我们下面先来详细介绍一下显式实例化,然后再回来看看它是如何解决的。

0x04 模板的显式实例化

📚 定义:在函数名后的 < > 里指定模板参数的实际类型。

简单来说,显式实例化就是在中间加一个尖括号 < > 去指定你要实例化的类型。

(在函数名和参数列表中间加尖括号)

函数名 <类型> (参数列表);

💬 代码演示:解决刚才的问题

template<class T>
T Add(const T& x, const T& y) {
	return x + y;
}

int main(void)
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;

	cout << Add<int>(a1, d2) << endl;     // 指定T用int类型
	cout << Add<double>(a1, d2) << endl;  // 指定T用double类型

	return 0;
}

🚩 运行结果:

🔑  解读:

像第一个 Add<int>(a1, a2)  ,a2 是 double,它就要转换成 int

第二个 Add<double>(a1, a2),a1 是 int,它就要转换成 double

这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。

double int 这种相近的类型,是完全可以通过隐式类型转换的。

 如果无法成功转换,编译器将会报错。

🔺 总结:

  • 函数模板你可以让它自己去推,但是推的时候不能自相矛盾。
  • 你也可以选择去显式实例化,去指定具体的类型。

0x05 模板参数的匹配原则

我们还是用刚才的 Add 函数模板来举例,现在我需要对整型的 a1 和 a2 进行加法操作:

template<class T>
T Add(const T& x, const T& y) {
	return x + y;
}

int main(void)
{
	int a1 = 10, a2 = 20;	
	cout << Add(a1, a2) << endl;

	return 0;
}

 我们是通过这个 Add 函数模板,生成 int 类型的加法函数的。

💬 如果我们有一个现成的、专门用来处理 int 类型加法的函数:

// 专门处理int的加法函数
int Add(int x, int y) {
	return x + y;
}

// 通用加法函数
template<class T>
T Add(const T& x, const T& y) {
	return x + y;
}

int main(void)
{
	int a1 = 10, a2 = 20;	
	cout << Add(a1, a2) << endl;

	return 0;
}

❓  思考:如果你是编译器,当 Add(a1, a2) 时你会选择用哪一个?

是用函数模板印一个 int 类型的 Add 函数,还是用这现成的 Add 函数呢?

我们继续往下看……

📚 匹配原则:

① 一个非模板函数可以和一个同名的模板函数同时存在,

而且该函数模板还可以被实例化为这个非模板函数:

// 专门处理int的加法函数
int Add(int x, int y) {
	cout << "我是专门处理int的Add函数: ";
	return x + y;
}

// 通用加法函数
template<class T>
T Add(const T& x, const T& y) {
	cout << "我是模板参数生成的: ";
	return x + y;
}

int main(void)
{
	int a1 = 10, a2 = 20;	
	cout << Add(a1, a2) << endl;       // 默认用现成的,专门处理int的Add函数
	cout << Add<int>(a1, a2) << endl;  // 指定让编译器用模板,印一个int类型的Add函数

	return 0;
}

② 对于非模板函数和同名函数模板,如果其他条件都相同,

在调用时会优先调用非模板函数,而不会从该模板生成一个实例。

如果模板可以产生一个具有更好匹配的函数,那么将选择模板。

// 专门处理int的加法函数
int Add(int x, int y) {
	cout << "我是专门处理int的Add函数: ";
	return x + y;
}

// 通用加法函数
template<class T1, class T2>
T1 Add(const T1& x, const T2& y) {
	cout << "我是模板参数生成的: ";
	return x + y;
}

int main(void)
{
	cout << Add(1, 2) << endl;     // 用现成的
	//(与非函数模板类型完全匹配,不需要函数模板实例化)

	cout << Add(1, 2.0) << endl;   // 可以,但不是很合适,自己印更好
	//(模板参数可以生成更加匹配的版本,编译器根据实参生产更加匹配的Add函数)

	return 0;
}

Ⅳ. 类模板(Class Template)

0x00 引入:和本篇开头本质上是一样的问题

💬 就比如 Stack,如果我们定它是 int,那么它就是存整型的栈:

class Stack {
public:
	Stack(int capacity = 4) 
		: _top(0) 
		, _capacity(capacity) {
		_arr = new int[capacity];
	}
	~Stack() {
		delete[] _arr;
		_arr = nullptr;
		_capacity = _top = 0;
	}
private:
	int* _arr;
	int _top;
	int _capacity;
};

❓ 如果我想改成存 double 类型的栈呢?

 当时我们在讲解数据结构的时候,是用 typedef 来解决的。

typedef int STDataType;
class Stack {
public:
	Stack(STDataType capacity = 4) 
		: _top(0) 
		, _capacity(capacity) {
		_arr = new int[capacity];
	}
	~Stack() {
		delete[] _arr;
		_arr = nullptr;
		_capacity = _top = 0;
	}
private:
	STDataType* _arr;
	int _top;
	int _capacity;
};

如果需要改变栈的数据类型,直接改 typedef 那里就可以了。

这依然是治标不治本,虽然看起来就像是支持泛型一样,

 它最大的问题是不能同时存储两个类型,你就算是改也没法解决:

int main(void)
{
	Stack st1;   // 存int数据
	Stack st2;   // 存double数据

	return 0;
}

你只能做两个栈,如果需要更多的数据类型……

那就麻烦了,你需要不停地 CV 做出各种数据类型版本的栈:

class StackInt {...};
class StackDouble {...};
……

这和文章开头提到的问题(Swap)本质上是一个问题,就是不支持泛型。

它们类里面的代码几乎是完全一样的,只是类型的不同。

 函数我们可以使用模板,类也是可以的,我们下面就来讲解一下类模板。

0x01 类模板的定义格式

📚 定义:和函数模板的定义方式是一样的,template 后面跟的是尖括号 < >

template<class T1, class T2, ..., class Tn>
class 类模板名 {
    类内成员定义
}

💬 代码演示:解决刚才的问题

template<class T>
class Stack {
public:
	Stack(T capacity = 4) 
		: _top(0) 
		, _capacity(capacity) {
		_arr = new T[capacity];
	}
	~Stack() {
		delete[] _arr;
		_arr = nullptr;
		_capacity = _top = 0;
	}
private:
	T* _arr;
	int _top;
	int _capacity;
};

int main(void)
{
	Stack st1;   // 存储int
	Stack st2;   // 存储double

	return 0;
}

  但是我们发现,类模板他好像不支持自动推出类型,

 它不像函数模板,不指定它也可以根据传入的实参去推出对应的类型的函数以供调用。

函数模板之所以能推,是因为有实参传形参这么一个 "契机" ,让编译器能帮你推。

"你定义一个类,它能推吗?没这个能力你知道吧!"

所以,类模板只支持显示实例化,我们继续往下看。

0x02 类模板实例化

 基于上面的原因,我们想要对类模板实例化,我们可以使用显示实例化。

 类模板实例化在类模板名字后跟 < >,然后将实例化的类型放在 < > 中即可。

类名 <类型> 变量名;

💬 代码演示:解决刚才的问题


template<class T>
class Stack {
public:
	Stack(T capacity = 4) 
		: _top(0) 
		, _capacity(capacity) {
		_arr = new T[capacity];
	}
	~Stack() {
		delete[] _arr;
		_arr = nullptr;
		_capacity = _top = 0;
	}
private:
	T* _arr;
	int _top;
	int _capacity;
};

int main(void)
{
	Stack<int> st1;      // 指定存储int
	Stack<double> st2;   // 指定存储double

	return 0;
}

📌 注意事项:

Stack 不是具体的类,是编译器根据被实例化的类型生成具体类的模具。

template<class T>
class Stack {...};

类模板名字不是真正的类,而实例化的结果才是真正的类。

Stack 是类名,Stack<int> 才是类型:

Stack<int> s1;
Stack<double> s2;

0x03 类外定义类模板参数

❓ 思考问题:下面的 Push 为什么会报错?

template<class T>
class Stack {
public:
	Stack(T capacity = 4) 
		: _top(0) 
		, _capacity(capacity) {
		_arr = new T[capacity];
	}
	// 这里我们让析构函数放在类外定义
	void Push(const T& x);
	~Stack();
private:
	T* _arr;
	int _top;
	int _capacity;
};

/* 类外 */

void Stack::Push(const T& x) {   ❌
    ...
}

🔑 解答:

① Stack 是类名,Stack<int> 才是类型。这里要拿 Stack<T> 去指定类域才对。

② 类模板中的函数在类外定义,没加 "模板参数列表" ,编译器不认识这个 T 。类模板中函数放在类外进行定义时,需要加模板参数列表。

这段代码第一个问题是没有拿 Stack<T> 去指定类域,

最大问题其实是编译器压根就不认识这个 T

即使你用拿类型 Stack<T> 指定类域,编译器也一样认不出来:

我们拿析构函数 ~Stack 来演示一下:

template<class T>
class Stack {
public:
	Stack(T capacity = 4) 
		: _top(0) 
		, _capacity(capacity) {
		_arr = new T[capacity];
	}

	// 这里我们让析构函数放在类外定义
	~Stack();
private:
	T* _arr;
	int _top;
	int _capacity;
};

/* 类外 */
Stack<int>::~Stack() {    ❌  // 即使是指定类域也不行 
    ...
}

💬 代码演示:我们现在来看一下如何添加模板参数列表!

template<class T>
class Stack {
public:
	Stack(T capacity = 4) 
		: _top(0) 
		, _capacity(capacity) {
		_arr = new T[capacity];
	}

	// 这里我们让析构函数放在类外定义
	~Stack();
private:
	T* _arr;
	int _top;
	int _capacity;
};

// 类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>    👈 必须要加!!!
Stack<T>::~Stack() {   // Stack是类名,不是类型! Stack<T> 才是类型,
	delete[] _arr;
	_arr = nullptr;
	_capacity = _top = 0;
}

再次强调 —— Stack 是类名,不是类型,Stack<T> 才是类型!

 这样编译器就能认识了。

本章完!

​​

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2022.4.18 | 2023.10.4(重制)
❌ [ 勘误 ]   暂无
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]

  • 98
    点赞
  • 104
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬叶子C

喜欢的话可以支持下我的付费专栏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值