C++11新特性

统一的列表初始化

{}初始化

C++11扩大了用大括号扩起的列表(初始化列表,注意不是列表初始化,这是两个不同的概念)的使用范围,使其可用于所有的内置类型和用户自定义类型,使用初始化列表时,可添加等号(=),也可以不添加

struct Point
{
	int _x;
	int _y;
};
int main()
{
	//内置类型
	int x1 = 1;
	int x2{ 2 };

	int arry1[]{ 1,2,3,4,5 };
	int arry2[5]{ 0 };

	//自定义类型
	Point p{ 1,2 };

	//在C++11中列表初始化也可适用于new表达式中
	int* pa = new int[4]{ 0 };

	return 0;
}

创建对象时也可使用列表初始化方式调用构造函数初始化

#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "Date()" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//过去
	Date d1(2023, 9, 29);

	//C++11,这里会调用构造函数进行初始化
	Date d1{ 2023,9,29 };
	Date d1 = { 2023,9,29 };
	return 0;
}

std::initializer_list

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
它是什么类型的

#include <iostream>

using namespace std;

int main()
{
	auto il = { 10,20,30 };
	cout << typeid(il).name() << endl;
}

在这里插入图片描述

std::initializer_list使用场景

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加了std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。
让vector也支持{}赋值,只需让initializer_list做构造函数和拷贝构造函数的参数即可

namespace sw
{
template<class T>
class vector {
public:
     typedef T* iterator;
     vector(initializer_list<T> l)
     {
         _start = new T[l.size()];
         _finish = _start + l.size();
         _endofstorage = _start + l.size();
         iterator vit = _start;
         typename initializer_list<T>::iterator lit = l.begin();
         while (lit != l.end())
         {
             *vit++ = *lit++;
         }
         //for (auto e : l)
         //   *vit++ = e;
     }
     vector<T>& operator=(initializer_list<T> l) {
         vector<T> tmp(l);
         std::swap(_start, tmp._start);
         std::swap(_finish, tmp._finish);
         std::swap(_endofstorage, tmp._endofstorage);
         return *this;
     }
private:
     iterator _start;
     iterator _finish;
     iterator _endofstorage;
 };
}

声明

auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃原来的用法,将其用于实现自动类型推断。这样就要求必须显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

#include <iostream>

using namespace std;

int main()
{
	int i = 100;
	auto p = &i;

	auto pf = strcpy;
	cout << typeid(p).name() << endl;
	cout << typeid(pf).name() << endl;
	return 0;
}

在这里插入图片描述

decltype

关键字decltype将变量的类型声明为表达式指定的类型

#include <iostream>

using namespace std;

template <class T1, class T2>
void F(T1 t1, T2 t2)
{
	decltype(t1 * t2) ret;//ret的类型为t1 *t2 结果的类型
	cout << typeid(ret).name() << endl;
}

int main()
{
	const int x = 1;
	double y = 2.2;

	decltype(x * y) ret;//ret的类型为double
	decltype(&x) p;  //p的类型是int const*
	cout << typeid(ret).name() << endl;
	cout << typeid(p).name() << endl;

	F(1, 'a');
	return 0;
}

右值引用和移动语义

右值引用和左值引用

无论是左值引用还是右值引用都是给对象取别名
左值引用和右值引用的根本区别就是能否取地址
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),**我们可以获取它的地址且可以对它赋值,左值可以出现在赋值符号的左边,右值不能出现在赋值符号的左边。**定义时const修饰符后的左值不能给它赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

int main()
{
	//左值
	int* p = new int(10);
	int b = 1;
	const int c = 5;

	//左值引用
	int*& ro = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

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

double fmin(double x, double y)
{
	return x + y;
}

int main()
{
	double x = 1.1, y = 2.2;

	//常见右值
	10;
	x + y;
	fmin(x, y);

	//常见右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x + y);

	//这里编译会报错,"=":左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

注意右值时不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想让rr1被修改,可以用const int&& rr1去引用。这个了解一下即可,十几种右值引用的使用场景不在这,这个特性也不重要

int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;

	rr1 = 20;//可以修改
	//rr2 = 5.5;//报错
	return 0;
}

在类型后面加一个&表示左值引用,加&&表示右值引用

左值引用与右值引用比较

左值引用总结:

1.左值引用只能引用左值,不能引用右值。
2.但是const左值引用既能引用左值也能引用右值。

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

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

右值引用总结

1.右值引用只能引用右值,不能引用左值
2.但是右值引用可以引用move以后的左值

#include <iostream>

using namespace std;

int main()
{
	//右值引用只能引用右值,不能引用左值
	int&& r1 = 10;

	int a = 10;
	//int&& r2 = a;//无法将左值绑定到右值引用

	int&& r3 = move(a);
	return 0;
}

右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值又可以引用右值,那么C++11为什么还要提出右值引用呢?下面我们来看一下左值引用的短板,并且学习右值引用是怎么补齐这个短板的。

#include <iostream>
#include <assert.h>

using namespace std;

namespace sw
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动语义" << endl;
			swap(s);
		}
		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动语义" << endl;
			swap(s);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

int main()
{
	return 0;
}

左值引用的使用场景:
做参数和做返回值都可以提高效率。

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

左值引用的短板:
当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。
例如:sw::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造———旧的编译器会先把值拷贝构造给临时变量再拷贝构造给接收变量ret,如果是新的编译器会优化直接拷贝构造给ret,少了拷贝给临时变量的环节

右值引用和移动语义解决上述问题:

在sw:string中增加拷贝构造,移动构造本质是将参数右值的资源窃取过来,占为己有,那么就不需要再深拷贝了,所以它叫移动构造,就是窃取别人的资源来构造自己。

string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移动语义" << endl;
	swap(s);//交换
}

int main()
{
	sw::string ret2 = sw::to_string(-1234);
	return 0;
}

我们发现这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有开新空间,拷贝数据,所以效率提高了。
to_string的返回值是一个右值,用这个右值构造ret2,如果既有拷贝构造又有移动构造,就会调用匹配调用移动构造,因为编译器会选择最匹配的参数调用。那么这里就是一个移动语义。

不仅仅有移动构造,还有移动赋值

在sw::string类中添加移动赋值函数,再去调用sw::to_string(1234),不过这次是将sw::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造

//移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动语义" << endl;
	swap(s);
	return *this;
}

int main()
{
	sw::string ret1;
	ret1 = sw::to_string(1234);
	return 0;
}

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。sw::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的把这里的str识别成了右值,调用了移动构造。然后再把这个临时对象作为sw::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值

右值引用和左值引用机器一些更深入的场景分析

按照语法,右值引用只能引用右值,但右值一定不能引用左值吗?
因为有些情境下,可能真的需要右值去引用左值实现移动语义。当需要右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::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;
}

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。
那为什么下面的结果都是左值引用呢?
在这里插入图片描述
按理说PerfectForward(10); ,PerfectForward(std::move(a)); 和PerfectForward(std::move(b));调用后输出的结果都应该是打印右值引用啊
这是因为引用类型唯一的作用就是限制了接收的类型,在后续使用中都退化成了左值,
我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
先看下面代码,理解右值引用

int main()
{
	int a = 10;

	int& r = a;
	int&& rr = move(a);
	cout << &r << endl;
	cout << &rr << endl;

	rr++;
	cout << rr << endl;
	return 0;
}

在这里插入图片描述

从上述代码我们可以发现右值引用是可以进行取地址的而且是可以修改的,这是为什么呢?
因为右值引用它的属性不是右值而是左值可以取地址也可以进行修改,可以这样理解,右值引用在底层开了一块空间把这个右值存了起来
再回来看上面的如果我们想让传来的引用保持它的属性怎么办?C++11库函数中有一个完美转发函数就可以实现保持引用它原有的属性
std::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;
}

// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
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;
}

在这里插入图片描述
这里我们可以看到通过库函数forward();完美转发就实现了保持引用的原有属性.
实际使用场景

template<class T>
struct ListNode
{
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	T _data;
};
template<class T>
class List
{
	typedef ListNode<T> Node;
public:
	List()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	void PushBack(T&& x)
	{
		//Insert(_head, x);
		Insert(_head, std::forward<T>(x));
	}
	void PushFront(T&& x)
	{
		//Insert(_head->_next, x);
		Insert(_head->_next, std::forward<T>(x));
	}
	void Insert(Node* pos, T&& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = std::forward<T>(x); // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
	void Insert(Node* pos, const T& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = x; // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
private:
	Node* _head;
};
int main()
{
	List<sw::string> lt;
	lt.PushBack("1111");
	lt.PushFront("2222");
	return 0;
}

记住要层层使用完美转发保持引用原有的属性。

lambda表达式

什么是lambda表达式?
先看在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法

#include <vector>
#include <algorithm>

struct Goods
{
	string _name; // 名字
	double _price;// 价格
	int _evaluate;// 评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};
struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};
struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
   3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), ComparePriceLess());//价格
	sort(v.begin(), v.end(), ComparePriceGreater());
}

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
   3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price < g2._price; });
	for (auto& e : v)
	{
		cout << e._name << endl;
	}
	cout << endl;

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price > g2._price; });
	for (auto& e : v)
	{
		cout << e._name << endl;
	}
	cout << endl;

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate < g2._evaluate; });
	for (auto& e : v)
	{
		cout << e._name << endl;
	}
	cout << endl;

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate > g2._evaluate; });
	for (auto& e : v)
	{
		cout << e._name << endl;
	}
	cout << endl;

	return 0;
}

在这里插入图片描述

上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个局部的匿名函数对象。

lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement
}

1.lambda表达式各部分说明

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来
判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda
函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以
连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数(捕捉列表中的变量或参数列表传过来的变量默认是const变量),mutable可以取消其常量,但这个变量毕竟是外面变量的拷贝只是让它在后面的函数体内可以被修改并不是可以改变外面变量的值。如果想要改变外面变量的值,那就直接传引用就好了。
性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回
值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推
导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为
空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

#include <iostream>

using namespace std;

int main()
{
	int a = 1, b = 2;
	auto add1 = [](int x, int y)->int {return x + y; };
	cout << add1(a, b) << endl;

	auto add2 = [](int x, int y) {return x + y; };
	cout << add2(a, b) << endl;
	return 0;
}

在这里插入图片描述
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调
用,如果想要直接调用,可借助auto将其赋值给一个变量。

两个lambda之前无法赋值因为他们的类型不同

#include <iostream>

using namespace std;

int main()
{
	int a = 1, b = 2;
	auto f1 = [](int x, int y)->int {return x + y; };
	cout << typeid(f1).name() << endl;

	auto f2 = [](int x, int y) {return x + y; };
	cout << typeid(f2).name() << endl;
	return 0;
}

在这里插入图片描述

2. 捕获列表说明

捕捉列表[ ] 描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:[表示值传递方式捕捉变量var
[=]:加等于号表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者
非局部变量都会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同

可变模板参数

C++11的新特性可变模板参数能够让你创建可以接收可变参数的函数模板和类模板,相比C++98/03,类模板只能定义固定数量的模板参数。
下面就是一个可变参数的函数模板

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

注:args是参数arguments的缩写,这里也可写成K,T等。
上面的参数args前面有省略号,所以它就是一个可变模板参数,我们把带省略号的参数称为
“参数包”,它里面包含了0到N个模板数。但是我们无法直接获取参数包args中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变参数的一个主要特点,也是难点,即如何展开可变参数包。由于语法不支持使用args[i]这样的方式获取可变参数,所以我们需要用一些奇招来获取参数包的值。
如何取到里面的值呢?
这里我们通过编译时的递归来确定每个参数

// 递归终止函数
template <class T>
void ShowList(const T& t)
{
	cout << t << endl;
}

// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " ";
	ShowList(args...);
}

int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

梦想很美

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

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

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

打赏作者

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

抵扣说明:

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

余额充值