【C++从练气到飞升】25--C++11:右值引用 | 移动语义 | 完美转发

 🎈个人主页库库的里昂
收录专栏C++从练气到飞升
🎉鸟欲高飞先振翅,人求上进先读书🎉

目录

⛳️推荐

一、左值引用和右值引用

二、什么是左值?什么是左值引用?

三、什么是右值?什么是右值引用?

四、左值引用与右值引用的比较

4.1 左值引用总结

4.2 右值引用总结

五、左值引用的使用场景和意义

六、右值引用的使用场景和意义

七、对左值引用和右值引用的总结

八、右值引用引用左值及其一些更深入的使用场景分析

8.1 move的特性

8.2 move真正的使用场景

九、完美转发

9.1 模板中的&&万能引用

9.2 右值引用自身并不是一个右值

9.3 std::forward 完美转发在传参的过程中保留对象原生类型属性

9.4 &&虽好,可不要贪杯哦

十、新的类功能

十一、对左值引用、右值引用、拷贝构造、移动构造的进一步认识


⛳️推荐

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站

一、左值引用和右值引用

传统的 C++ 语法中就有引用的语法,而 C++11 中新增了右值引用的语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论是左值引用还是右值引用,都是给对象取别名。

二、什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,一般可以对它赋值(有一些不能复制的也是左值,比如被 const 修饰的变量),左值可以出现在赋值符号的左边,右值不能出现在赋值符号的左边。定义时 const 修饰符后的左值,不能给它赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

//左值引用
int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

小Tips:左值也可以出现在赋值符号的右边,例如 int a = b;,这里的 b 任然是一个左值。这里我们可以得出一个结论:出现在赋值符号左边的一定是左值,出现在赋值符号右边的可能是右值也可能是左值。因此我们不能简单的通过观察其在赋值符号的左边还是右边就断定它是左值还是右值。

三、什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等。右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,不能对右值取地址。右值引用就是对右值的引用,给右值取别名。

//右值引用
int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);

	//字符串常量是左值,因为可以取它的地址
	cout << &("xxxxxxxx") << endl;
	
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;//整型常量
	double&& rr2 = x + y;//表达式返回值
	double&& rr3 = fmin(x, y);//函数返回值

	// 下面编译会报错:error C2106: “=”: 左操作数必须为左值
	//10 = 1;
	//x + y = 1;
	//fmin(x, y) = 1;
	return 0;
}

小Tips1:数字常量(整型常量)被定义为右值,字符串常量被定义为左值。一种我认为比较合理的解释是:数字常量(整型常量),CPU 可以立即寻址,所以数字常量(整型常量)其实只存在寄存器中,没有放到内存地址。而字符串常量的话,CPU 是没办法立即寻址的,所以对于字符串常量预先就放到了内存当中。所以我们可以对一个字符串常量取地址,那它就是一个左值,但是,凡是都有一个但是,const char*& = "xxxxx"; 在 VS 编译器下会报错,得用右值引用 const char*&& ps = "xxxxx";,这样就没问题了。

小Tips2:需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定的位置,且可以取到该位置的地址,也就是说,例如:不能取字面常量10的地址,但是上面代码中的引用 rr1 引用后,可以对 rr1 取地址,也可以修改 rr1(这里的修改是去修改 rr1 引用的内容,并不是让 rr1 变成其他右值的引用)。如果不想 rr1 被修改,可以用 const int&& rr1 去引用,是不是感觉很神奇,这个了解一下即可,实际中右值引用的使用场景并不在于此,这个特性也不重要。

小Tips3:可以将我们常见的右值分为两类,一类是内置类型的右值,也被叫做纯右值,一般会出现在字面常量、表达式返回值中。另一类是自定义类型的右值,一般出现在函数返回值中,该函数的返回值是一个自定义类型的对象,这个被返回的对象也被叫做将亡值,因为在执行完 return 语句后,该对象就随着函数栈帧的销毁而销毁了。其次,函数返回过程中产生的临时中间变量,也可以被叫做将亡值,因为这个临时的中间变量,它的生命周期往往只有一行。

四、左值引用与右值引用的比较

4.1 左值引用总结

  • 左值引用只能作为左值的别名,不能作为右值的别名。

  • 但是 const 左值引用既可以做左值的别名,也可以做右值的别名。

//左值引用只能引用左值
int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a;   // ra为a的别名
	//int& ra2 = 10;   // 编译失败,因为10是右值

	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

4.2 右值引用总结

  • 右值引用只能做右值的别名,不能做左值的别名。

  • 但是右值引用可以做经过 move 操作的左值的别名。

int main()
{
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;
	int a = 10;
	//int&& r2 = a;
	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	// // message : 无法将左值绑定到右值引用

	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);
	return 0;
}

五、左值引用的使用场景和意义

左值引用可以做函数的参数和函数的返回值,这样可以避免在函数传参和函数返回的时候去调用拷贝构造函数,对于一些大对象和需要进行深拷贝的对象来说,这样做可以提高效率。

void func1(wcy::string s)
{}
void func2(const wcy::string& s)
{}
int main()
{
	bit::string s1("hello world");
	// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
	func1(s1);
	func2(s1);
	// string operator+=(char ch) 传值返回存在深拷贝
	// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
	s1 += '!';
	return 0;
}

左值引用的缺陷:当函数返回的对象是一个局部变量时,出了函数的作用域该对象就被销毁了,就不能使用左值引用返回,只能通过传值返回。而传值返回会导致至少调用一次拷贝构造,如果是旧一点的编译器可能是调用两次构造函数。

wcy::string Func()
{
	wcy::string str("xxxx");//构造

	return str;
}

int main()
{
	wcy::string str1 = Func();//连续的 拷贝构造 + 拷贝构造 ===> 拷贝构造

	cout << "========================" << endl;
	wcy::string str2;//构造
	str2 = Func();//拷贝构造 + 赋值 
	return 0;
}

小Tips:只有连续的构造才会被编译器优化,构造 + 赋值是不会被编译器优化的。为了解决左值引用存在的缺陷,下面我们引入右值引用和移动语义。

六、右值引用的使用场景和意义

首先我们需要明确一点,两个同名函数,分别用左值引用和右值引用作为形参,它们两个是构成函数重载的。

void Func(int& pa)//左值引用
{
	cout << "void Func(int& pa)" << endl;
}

void Func(int&& pa)//右值引用
{
	cout << "void Func(int&& pa)" << endl;
}

int main()
{
	int x = 10;
	int y = 20;
	Func(x);
	Func(x + y);
	return 0;
}

小Tips:即使在 int& pa 的前面加上 const,此时 pa 即可以是左值的别名,也可以是右值的别名,那重载的两个 Func 函数在语法上都可以接收右值。但是当实参传递的是右值的时候,编译器会去走最匹配的,即去调用 void Func(int&& pa)

再来分析一下深拷贝对象传值返回存在的缺陷

小Tips:通过上图我们可以发现,函数在返回的过程中创建了一个临时的中间变量,这个临时的中间变量作为该函数的返回值,它就是一个右值。再观察我们可以发现,这个临时的中间变量,它的声明周期就是有一行,即 str2 = Func(); 这一行,到了下一行,这个临时的中间变量就被销毁了,我们将这种值也称作将亡值。那么,我们是否可以考虑让 str2 和这个临时的中间变量进行一个资源互换,让 str2 去继承这个临时变量里面的资源,再把 str2 的资源交给这个临时的中间变量,中间变量在销毁的时候顺便就把之前 str2 中的资源给清理了,其实我们说的这些就是移动赋值的原理。

移动赋值

//my_string.h
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动赋值" << endl;
	swap(s);
	return *this;
}
//test.cpp
wcy::string Func()
{
	wcy::string str("xxxx");//构造

	return str;
}

int main()
{
	wcy::string str2;//构造
	str2 = Func();//拷贝构造 + 赋值 ===> 赋值
	return 0;
}

小Tips:在创建完临时的中间变量后,str 会被销毁,所以 str 的境地和这个临时的中间变量很像,所以这里可以考虑让这个临时的中变量去继承 str 中的资源,即编译器将 str 识别成右值中的将亡值,只不过这个临时的中间变量并没有被提前创建,所以需要调用移动构造。而 str2 因为已经提前被创建出来了,所以调用的是移动赋值。再加入移动构造和移动赋值之后,上面这段代码的从原来需要执行两次深拷贝,到现在只需要进行资源的转移。代码效率得到了极大的提升。

移动构造:其原理和移动赋值一样,这里就不过多赘述。

//my_string.h
// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移动语义" << endl;
	swap(s);
}
//test.cpp
wcy::string Func()
{
	wcy::string str("xxxx");//构造

	return str;
}

int main()
{
	wcy::string str1 = Func();//连续的 拷贝构造 + 拷贝构造 ===> 拷贝构造
	return 0;
}

小Tips:上面这段代码,其实编译器进行了以下两个优化:

  • 对连续的构造、拷贝构造,合二为一。

  • 编译器把 str 识别成右值——将亡值。

需要注意:这里把 str 识别成将亡值是编译器的行为,因为根据左值和右值的定义去判断,str 是一个左值,用它作为参数去调用拷贝构造创建 str1 理论上应该调用普通的拷贝构造函数(即左值引用的构造函数)进行深拷贝,但是,但是!!!这里的 str 在函数调用结束后就要销毁了,那么采用移动构造不是很香嘛。

另外另外:以上两个编译器的优化是建立在传值返回的基础之上的,千万不能将 Func 函数的返回值修改成左值引用或右值引用类型,即 Func 函数的返回值不可以是 string& 或者 string&&。因为无论是左值引用还是右值引用,它们本质上都是别名,而这里的 str 在函数调用结束后就会被立即销毁,所以不能给它取别名。

七、对左值引用和右值引用的总结

左值引用的核心价值是减少拷贝,提高效率。左值引用减少拷贝的方法是,以函数返回左值引用为例,如果一个被返回的对象除了作用域还在,就可以采用左值引用返回,可以看出左值引用减少拷贝的策略是将该对象自身返回。但是如果该对象出了作用域就被销毁了,此时左值引用就不在适合了,在没有右值引用的时候,我们就只能老老实实的去调用拷贝构造,用这个被返回的对象去构造一个临时的中间对象,如果待拷贝的对象是一个内置类型或者是一个普通的自定义类型(即不需要进行深拷贝)那么它的拷贝代价并不大,我们无需担心。但是如果待拷贝的对象是一个需要进行深拷贝的自定义类型,此时拷贝的代价就会变得非常大。于是,为了解决这个问题,C++11 中就提出了右值引用和移动语义,C++11 中将普通传值返回的对象视为右值中的将亡值,然后提供移动构造(该构造函数的参数是一个右值引用),通过继承将亡值中的资源避免了深拷贝效率低下的问题。这里也说明了一点问题,内置类型和浅拷贝的自定义类型是不需要移动构造的。右值引用的出现弥补了左值引用解决不了的函数传值返回时需要进行深拷贝的问题。在左值引用和移动构造的加持下,大多数场景都不必再使用拷贝构造了,但是拷贝构造函数依然是必不可少的,如果被拷贝的对象是一个左值,此时我们还是只能老老实实的去调用拷贝构造函数进行深拷贝,如下面这段代码所示。

int main()
{
	wcy::string str1("xxxxxxxxxxxxxxxxxxxx");
	wcy::string str2(str1);//此时只能老老实实的去调用拷贝构造函数进行深拷贝
}

八、右值引用引用左值及其一些更深入的使用场景分析

8.1 move的特性

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值嘛?有些场景下,可能真的需要用右值引用去引用左值实现移动语义。在 1.4.2 小节提到过,右值引用可以做为一个经过 move 操作的左值的别名。C++11 中,std::move 函数位于 utility 这个头文件中。该函数的名字具有迷惑性,它并不搬移任何东西,唯一的功能就是返回一个右值引用(注意:move 并不是将一个左值变成右值)。以下面的代码为例:

int main()
{
	wcy::string str1("xxxxxxxx");
	move(str1);
	wcy::string str2(str1);
	return 0;
}

小Tips:如上图所示,str2 并没有去继承 str1 的资源,这说明经过 move 操作的 str1 并没有从左值变成右值,因为这里 str1 如果变成右值的话,创建 str2 应该去调用移动构造,进行资源转移,但是通过监视窗口并没有发现资源转移。再来看下面这段代码。

小Tips:上面这段代码在创建 str2 的时候进行了资源的置换。因为 str1 经过 move 操作后会返回一个右值引用,该引用就是 str1 的别名,然后再去创建 str2,创建过程中因为 move 的返回值是一个右值引用,所以就会去调用移动构造,而移动构造中完成的是自愿置换的工作,所以就出现了上面这一幕,创建完 str2 后,str1 中的内容消失了,其实并不是消失了,只是被置换到了 str2 中。正所谓,笑容并不会消失,只会从一个人身上转换到另一个人身上。

8.2 move真正的使用场景

• 没有使用move进行插入

int main()
{
	list<wcy::string> ls;
	wcy::string str1("xxxxxxxxxxxxxxxxxxx");

	ls.push_back(str1);
	return 0;
}

• 使用move进行插入

int main()
{
	wcy::list<wcy::string> ls;
	wcy::string str1("xxxxxxxxxxxxxxxxxxx");

	ls.push_back(move(str1));
	return 0;
}

• 最常见的插入

int main()
{
	wcy::list<wcy::string> ls;

	ls.push_back("xxxxxxxxxxxx");
	return 0;
}

总结:右值引用还可以用在容器的插入接口中,如果插入的对象是右值,可以利用移动构造转移资源给数据结构中的对象,也可以减少拷贝。

九、完美转发

9.1 模板中的&&万能引用

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10);           // 右值
	int a;         
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);      // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

小Tips:模板中的 && 不代表右值引用,而是万能引用,其既能接受左值,又能接受右值。模板的万能引用只是提供了能够同时接受左值和右值的能力(包括 const 左值和 const 右值)。如上代码:实参如果传递的是一个左值 a,那么模板实例化出来的就是左值引用 int& t,有的书上也管这个叫引用折叠;实参如果传递的是一个右值 10,那么模板实例化出来的就是 int&& t(const 左值和 const 右值也类似)。这里需要注意一点,const 左值经过 move 操作之后得到的是一个 const 右值。除了要知道模板中的 && 表示万能引用之外,我们还需要清楚以下几点,才能明白为什么会出现上面的打印结果。

9.2 右值引用自身并不是一个右值

int main()
{
	int a = 10;
	int&& ra = move(a);

	cout << &a << endl;
	cout << &ra << endl;//对右值引用取地址

	cout << a << endl;
	ra++;
	cout << a << endl;

	return 0;
}

小Tips:如上面的代码所示,ra 是一个右值引用,但是它自身并不是一个右值。我们要时刻牢记,无论是左值引用还是右值引用它们的本质都是在取别名,这也是为什么 &a == &ra。其次因为 ra 并不是右值,所以我们可以对它进行修改,注意:对引用的修改是修改引用所指向的内容,并不是修改引用的指向,上面代码就通过对右值引用 ra++,去修改了 a 对应内存空间中存储的值。这里右值引用引用的是一个经过 move 操作的左值。如果,右值引用引用的是一个常量,那么还是可以对这个右值引用进行修改,虽然这个右值常量不能被修改,但是此时被引用的常量右值会被存储到特定的位置,这里去修改右值引用,本质上就是对这个特定空间中存储的值进行修改,这一点在三小节提到过,这里不再赘述。

总结:上面是从概念角度去证明了,一个右值引用它本身并不是一个右值,因为可以对它取地址,这说明一个右值引用它本质上是一个左值。其次,从实际角度再去理解,右值引用产生的目的是解决一些场景下的深拷贝问题,解决方式是通过右值引用,进行资源交换,那如果右值引用被编译器识别成右值,右值意味着不可被修改,那资源交换如何实现?这就违背了右值引用产生的价值。 这下再去看下面这段代码就不难理解了。

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10);           // 右值
	int a;         
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);      // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

小TipsPerfectForward 函数形参中的 t 无论是被实例化成左值引用还是右值引用,最终 Fun(t) 在执行函数调用的时候,t 都会被识别成一个左值,只有普通左值和 const 左值的区别。

9.3 std::forward 完美转发在传参的过程中保留对象原生类型属性

要想在进行函数调用 Fun(t) 的过程中,让 t 的类型保持不变,就需要用到 forward 进行完美转发。

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t)
{
	Fun(forward<T>(t));//进行完美转发
}
int main()
{
	PerfectForward(10);           // 右值
	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);      // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

小Tips:要让 t 保持原生类型属性的场景,其实在上面链表插入的时候就碰到过,只不过当时还没有提出完美转发这个概念,如下图所示,有了完美转发的概念之后,再碰到这种场景我们就可以使用 forward 了。

9.4 &&虽好,可不要贪杯哦

模板中的 && 不代表右值引用,而是万能引用。此时就会有小伙伴会像,既然 && 可以表示万能引用,那么可不可以把类中原来的拷贝构造(形参是一个左值引用)给删了,只写一下 && 的版本呢?答案是可以的。但是 && 版本一定得是函数模板,因为 T&& t 中 t 表示万能引用是建立在 T 的类型是根据形参推导出来的,而作为类的成员函数,他如果不是一个函数模板,那么 T 的类型是在模板实例化的时候就被确定好了,此时 t 就被确定为 T 类型的右值引用,就不再是万能引用了。

template<class T>
struct ListNode
{
	ListNode<T>* _next;
	ListNode<T>* _prev;
	T _val;

	template<class TY>
	ListNode(TY&& val)
		:_next(nullptr)
		, _prev(nullptr)
		, _val(move(val))
	{}
};

小Tips:虽然这样写没毛病,但是还是不建议大家这样写,还是老老实实的再写一个左值引用的版本。

十、新的类功能

C++11 新增了两个默认成员函数——移动构造函数和移动赋值运算符重载函数。对于这两个函数有以下三点需要注意:

  • 一个类如果没有自己实现移动构造函数,并且没有实现析构函数、拷贝构造函数、拷贝赋值运算符重载函数。那么编译器会自动生成一个默认的移动构造函数。默认生成的移动构造函数,对于内置类型会逐行逐成员按字节拷贝,对于自定义类型的成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造函数。如果实现了前面三个函数中的任意一个,编译器不会生成默认的移动构造函数。
  • 一个类如果没有自己实现移动赋值运算符重载函数,并且没有实现析构函数、拷贝构造函数、拷贝赋值运算符重载函数,那么编译器会自动生成一个默认的移动赋值运算符重载函数。默认生成的赋值运算符重载函数,对于内置类型成员会逐成员逐字节拷贝(浅拷贝),对于自定义类型的成员,则需要看这个成员是否实现了移动赋值运算符重载,如果实现了就调用移动赋值运算符重载,没有实现就调用拷贝赋值运算符重载。如果实现了前面三个函数中的任意一个,编译器不会生成默认的移动赋值。
  • 如果自己完全提供了移动构造和移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

C++11 除了新增了两个默认的成员函数外,还允许在类定义的时候给成员变量初始缺省值,默认生成的构造函数会使用这些缺省值进行初始化,这一点在前面的类和对象文章中就提到过,并且多次使用,这里就不再细讲。

其次,C++11 还新增了两个关键字:default 和 delete

  • default:C++11 可以让我们更好的控制要使用的默认函数。假设我们要使用某个默认成员函数,但是因为一些原因这个函数没有默认生成。比如:当我们自己提供了拷贝构造,就不会默认生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成。
  • delete:如果想要限制某些默认成员函数的生成,在 C++98 中,是将该函数设置成 private,并且只声明补丁,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需要在该函数声明加上 =delete 即可,该语法指示编译器不生成对应函数的默认版本,称 =delete 修饰的函数为删除函数。

十一、对左值引用、右值引用、拷贝构造、移动构造的进一步认识

左值引用诞生的目的,说白了就是为了避免拷贝构造函数的调用,而拷贝构造函数诞生的目的是为了应对类中有需要进行深拷贝的成员变量,此时我们需要自己写构造函数,实现深拷贝的动作。深拷贝是有代价的,深拷贝要开同样大小的一块空间,还要进行赋值操作。说白了,左值引用的目的就是去防止深拷贝的发生。但是,有的时候这种深拷贝动作,不得不执行,就像在函数返回一个局部对象,此时无论如何是要去调用构造函数的,在右值引用和移动构造出来之前,这种情况下深拷贝动作无法避免。而在右值引用出来之后,前面这种情况,函数的返回值会被识别成一个右值,此时会去调用移动构造,移动构造函数中,并不进行深拷贝,而是完成资源的交换,这样以来,虽然无法避免拷贝动作的发生,但是实现拷贝的方式从深拷贝变成了资源交换。所以,我认为,拷贝构造函数诞生的目的是为了解决深拷贝问题,而右值引用和移动构造的产生实际上是对拷贝过程的一种优化,并且右值引用和移动构造是相辅相成的,少了其中的任意一个都是没有意义的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

库库的里昂

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值