【深度剖析 C++11】 第三弹:C++11完结,迈进高效编程的新纪元

C++11系列学习参考:

话不多说,我们直接进入正题!

一、可变参数模板

1、基本语法

之前我们学过的类模板以及函数模板的参数都是不可变参数模板,模板定义了几个参数,实例化就需要传递几个参数。
 C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数量的参数被称 为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
语法格式如下:
 
//可变参数类模板
template <class ...Args> 

//普通类型可变参数函数模板
void Func(Args... args) {}

//左值引用类型 可变参数函数模板
void Func(Args&... args) {}

//万能引用类型 可变参数函数模板
void Func(Args&&... args) {}
我们用...省略号来指出⼀个模板参数或函数参数的⼀个包,在模板参数列表中,class...指出接下来的参数包表示零或多个类型;在函数参数列表中,类型名后面跟...指出接下来形参列表表示零或多个形参对象;函数参数包可以用左值引用或万能引用表示,跟前面普通模板 ⼀样,每个参数实例化时遵循引用折叠规则。
我们可以使用 sizeof 计算参数包中参数的个数:
//Args为可变参数类型 ...表示为可变参数模板 args为参数包
template<class ...Args>
void Calculate(Args&&...args)
{
	//计算参数包中参数的个数
	cout << sizeof...(args) << endl;
}
void Test()
{
	//参数包中没有参数
	Calculate();
	//有一个参数
	Calculate(10);
	//有两个参数
	Calculate(3.14, string("helloworld"));
	//有三个参数
	Calculate(10, 3.14, string("helloworld"));
}	

2、底层原理

实际使用时,当我们定义了一个可变参数的函数模板,在调用函数时,编译器会根据我们传递的参数的个数和类型,结合引用折叠的规则,去自动实例化对应的函数。

void Test()
{
	//参数包中没有参数
	Calculate();
	//有一个参数
	Calculate(10);
	//有两个参数
	Calculate(3.14, string("helloworld"));
	//有三个参数
	Calculate(10, 3.14, string("helloworld"));
}	

//编译器会根据我们的函数调用,结合引用折叠的规则,实例化以下四个函数
void Calculate(){}
void Calculate(int&& arg){}
void Calculate(double&& arg1, string&& arg2){}
void Calculate(int&& arg1, double&& arg2, string&& arg3){}

本质上来说如果没有可变参数模板这个东西,我们必须要去实现多个这样的函数模板才可以支持这样的功能,有了可变模板参数,我们进一步被解放,让我们的泛型编程更加灵活。

//如果没有可变参数模板,下面这些函数模板需要我们自己去实现,才能支持上面的功能
void Calculate(){}
template <class T>
void Calculate(T1&& arg1){}
template <class T1, class T2>
void Calculate(T1&& arg1, T2&& arg2){}
template <class T1, class T2, class T3>
void Calculate(T1&& arg1, T2&& arg2, T3&& arg3){}

3、包扩展

对于⼀个参数包,除了能计算它的参数个数,我们还可以扩展它,当扩展⼀个包时,我们还要提供用于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。这里比较抽象,大家可以试着多看几遍。
下面代码本质是利用了编译时递归,从而获取参数包里面的参数
//本质时编译时递归,终止条件需要放在上面
//递归的终止条件,参数包剩余0个参数时自动匹配,终止递归
void ShowList()
{
	cout<< endl;
}

//实现包扩展的函数模板
template<class T,class ... Args>
void ShowList(T value,Args... args)
{
	//打印参数包中第一个参数,剩下的n-1个递归ShowList获取
	cout << value << " ";
	ShowList(args ...);
}

void Test()
{
	ShowList();
	ShowList(1);
	ShowList(1, 2.2);
	ShowList(1, 2.2, 3.14);
	ShowList(1, 2.2, 3.14,"helloworld");
}	

解析参数包的结果:

也可以通过以下方法进行包扩展
template <class T>
 const T & GetArg(const T & value)
 {
	 cout << value << " ";
	 return value;
 }

 template < class ...Args>
 void Arguments(Args... args)
 {

 }

 template <class ...Args>
 void Print(Args... args)
 {
	 // 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
	Arguments(GetArg(args)...);
	cout << endl;
 }
void Test()
{
	Print();
	Print(1.1);
	Print(1.1, 2, 2);
	Print(1.1, 2.2, 3.3);
	Print(1.1, 2.2, 3.3, "helloworld");
}	

4、emplace系列接口

C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为可变参数模板。
功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container<T>,empalce还支持直接插入构造T对象的参数,在有些场景会更高效⼀些,可以直接在容器空间上构造T对象。下面我们看看STL中几个容器的emplace系列接口。
vector:
list:
map和unordered_map:
下面我们模拟list的emplace_push()接口。
注意:传递参数包过程中,如果是 Args&&... args 的参数包,要用完美转发参数包,方式如下
std::forward<Args>(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左 值。
namespace _zwy
{
	//链表的节点
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;
		ListNode(T&& data)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(data))
		{}
		template <class... Args>
		ListNode(Args&&... args)
			: _next(nullptr)
			, _prev(nullptr)
			, _data(std::forward<Args>(args)...)
		{}
	};

	//链表的迭代器
	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;
		ListIterator(Node* node)
			:_node(node)
		{}
		// ++it;
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		Ref operator*()
		{
			return _node->_data;
		}
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
	};


	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator; 
		typedef ListIterator<T, const T&, const T*> const_iterator;
		iterator begin()
		{
			return iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		void empty_init()
		{
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
		}
		list() 
		{
			empty_init();
		}
		void push_back(const T& x)
		{
			insert(end(), x);
		}
		void push_back(T&& x)
		{
			insert(end(), move(x));
		}
		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}
		iterator insert(iterator pos, T&& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(move(x));
			Node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}
		template <class... Args>
		void emplace_back(Args&&... args)
		{
			insert(end(), std::forward<Args>(args)...);
		}
		template <class... Args>
			iterator insert(iterator pos, Args&&... args)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(std::forward<Args>(args)...);
			Node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
			}
	private:
		Node* _head;
	};
}

结合我们模拟实现的string,我们研究emplace哪种场景下更高效。

	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}

		const_iterator end() const
		{
			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);
		}

		// 拷贝构造
		string(const string& s)
		{
			cout << "string(const string& s) -- 拷贝构造" << endl;
			reserve(s._capacity);
			for (auto ch : s)
			{
				push_back(ch);
			}
		}

		void swap(string& ss)
		{
			::swap(_str, ss._str);
			::swap(_size, ss._size);
			::swap(_capacity, ss._capacity);
		}

		// 移动构造
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			// 转移掠夺你的资源
			swap(s);
		}

		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 拷贝赋值" <<
				endl;
			if (this != &s)
			{
				_str[0] = '\0';
				_size = 0;
				reserve(s._capacity);
				for (auto ch : s)
				{
					push_back(ch);
				}
			}
			return *this;
		}

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

		~string()
		{
			//cout << "~string() -- 析构" << endl;
			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];
				if (_str)
				{
					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)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}

		size_t size() const
		{
			return _size;
		}
	private:
		char* _str = new char('\0');
		size_t _size = 0;
		size_t _capacity = 0;
	};
}

下面是具体的比较

void Test()
{
	_zwy::list<_zwy::string> lt;
	// 传左值,跟push_back⼀样,⾛拷贝构造
	_zwy::string s1("helloworld");
	lt.emplace_back(s1);
	cout<< endl;

	// 传右值,跟push_back⼀样,走移动构造
	lt.emplace_back(move(s1));
	cout <<  endl;

	// 直接把构造string参数包往下传,直接用string参数包构造string
	// 这⾥达到的效果是push_back做不到的
	lt.emplace_back("helloworld");
	cout << endl;


	_zwy::list<pair<_zwy::string, int>> lt1;
	// 传左值跟push_back⼀样
	// 构造pair + 拷贝构造pair到list的节点中data上
	pair<_zwy::string, int> kv("苹果", 1);
	lt1.emplace_back(kv);
	cout << endl;

	// 传右值跟push_back⼀样
	//构造pair + 移动构造pair到list的节点中data上
	lt1.emplace_back(move(kv));
	cout <<  endl;

	// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair
	// 这里达到的效果是push_back做不到的
	lt1.emplace_back("苹果", 1);
	cout <<  endl;
}

如果传递的是具体的对象,那么emplace_back和push_back效率一样,如果传递的是构造对象的参数包,emplace_back可以直接传递构造函数所需的参数,在容器内原地构造对象,无需先构造再拷贝或移动,而push_back做不到,emplace总体而言更高效,更安全。推荐以后使emplace系列替代insert和push系列进行容器的插入!

二、新的类功能

1、默认的移动构造和移动赋值函数

在C++11之前类中,一共有6个默认成员函数,分别是构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载,const 取地址重载。最重要的是前四个默认成员函数。

C++11后,新增了两个默认成员函数,移动构造函数和移动赋值重载。不过新增的默认成员函数的生成需要一定的条件。

(1)、移动构造:
如果你没有自己实现移动构造函数,且没有实现 析构函数 、拷贝构造函数、拷贝赋值重载 中的任意⼀ 个。那么编译器会自动生成⼀个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,否则就调用拷贝构造。
(2)、移动赋值重载:

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意 ⼀个,那么编译器会自动生成⼀个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,否则就调用拷贝赋值。(默认移动赋值重载跟移动构造完全类似)

如果你自己实现了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
结合我们自己模拟的string来带大家看下实际的规则:
class Student
{
public:
	//只提供构造函数
	//不提供析构函数 、拷贝构造、拷贝赋值重载的任意一个
	//编译器自动生成移动构造和移动赋值
	Student(const char* name="", int age=0)
		:_name(name)
		,_age(age)
	{}
private:
	_zwy::string _name;
	int _age;
};

void Test()
{
	Student s1;
	//调用编译器生成的拷贝构造
	Student s2 = s1;
	//调用编译器生成的移动构造
	Student s3 = move(s1);
	Student s4;
	//调用编译器生成的移动赋值
	s4 = move(s2);
}

2、声明时给缺省值

C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的
成员使用的。
class Date
{
public:
	//成员变量在声明位置给了缺省值,如果没有在初始化列表显示初始化
	// 那么初始化列表会用这个缺省值初始化
	// 
	//默认构造函数 三个成员变量都在初始化列表用缺省值初始化
	Date()
	{}

	//构造函数 _year和_month用缺省值初始化
	Date(int day)
		:_day(day)
	{}
private:
	//声明类的成员变量给缺省值
	int _year = 2024;
	int _month = 11;
	int _day = 19;
};

void Test()
{
	//d1 缺省值 2024 11 19
	Date d1;
	//d2 2024 11 20
	Date d2(20);
}


3、default和delete

(1)、default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为⼀些原因
这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用
default关键字显示强制指定移动构造生成。
class Student
{
public:
	Student(const char* name="", int age=0)
		:_name(name)
		,_age(age)
	{
		cout << "构造函数:Student(const char* name="", int age=0)" << endl;
	}
	Student(const Student& s)
		:_name(s._name)
		,_age(s._age)
	{
		cout << "拷贝构造:Student(const Student & s)" << endl;
	}

	//提供了拷贝构造,编译器不会生成默认的移动构造,我们指定生成
	Student(Student&& s)= default;
private:
	_zwy::string _name;
	int _age;
};
void Test()
{
	Student s1("张三", 18);
	//拷贝构造
	Student s2(s1);
	//使用编译器指定生成的移动构造
	Student s3(move(s1));
}

可以看到移动构造s3后,s1被置空了,说明确实调用强制编译器生成的移动构造。

(2)、delete
如果想要限制类中某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明不定义, 这样只要其他⼈想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法表示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
class Student
{
public:
	Student(const char* name="", int age=0)
		:_name(name)
		,_age(age)
	{
		cout << "构造函数:Student(const char* name="", int age=0)" << endl;
	}
	//将拷贝构造函数删除,编译器不会默认生成,调用拷贝构造就会报错
	Student(const Student& s) = delete;
private:
	_zwy::string _name;
	int _age;
};
void Test()
{
	Student s1("张三", 18);
	//拷贝构造被删除,编译报错
	Student s2(s1);
}

三、STL标准库的变化

1、新增容器:

实际最有用的是unordered_map和unordered_set,是基于哈希桶封装的一种数据结构,在查找和插入的方面,效率fei'ch后面我们会有单独的章节给大家深度讲解这两个容器。

2、容器的范围for遍历

C++11中引入了基于范围的for循环。for循环后的括号由冒号分为两部分:第一部分是范围
内用于迭代的变量,第二部分则表示被迭代的范围,范围for的底层很简单,容器遍历实际就是替换为迭代器。
void Test()
{
	//范围for遍历修改vector
	vector<int> v = { 1,2,3,4,5 };
	for (auto& e : v)
	{
		e *= 10;
		cout << e << " ";
	}	
	cout << endl;

	//范围for遍历string
	string s = "helloworld";
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;
}

四、包装器function

function - C++ Reference

std:function是⼀个类模板,也是⼀个包装器。头文件为<functional>。std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda bind 表达式等,存储的可调用对象被称为 std::function 的目

函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, std::function 的优势就是统
⼀类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。

语法格式为:

std::function<返回值类型(参数类型列表)> 变量名

1、包装函数指针:

int add(int x, int y)
{
	return x + y;
}
void Test()
{
	//返回值类型int 参数类型为 int,int
	function<int(int,int)> f1 = add;
	//调用包装器对象
	cout << f1(5, 10) << endl;
}

2、包装仿函数对象:

struct Greater
{
	bool operator()(int x, int y)
	{
		return x > y;
	}
};
void Test()
{
	//返回值类型bool 参数类型为 int,int
	function<bool(int, int)> f2 = Greater();
	//调用包装器对象
	cout << f2(10 ,5) << endl;
}

3、包装lambda表达式:

void Test()
{
	auto sub=[](int x, int y)->int {return x - y; };
	function<int(int, int)> f3 = sub;
	cout << f3(10, 5) << endl;
}

4、包装类的成员函数:

成员函数要指定类域并且前面加&才能获取地址

普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以。

class Calculate
{
public:

	//静态成员函数
	static int add(int x, int y)
	{
		return x + y;
	}
	//普通成员函数
	double div(double x, double y)
	{
		return x / y;
	}
};
void Test()
{
	//包装静态成员函数:
	function<int(int, int)> f4 = &Calculate::add;
	cout << f4(10,20) << endl;

	//包装普通成员函数
	//普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
	Calculate cal;
	function<double(Calculate,double, double)> f5 = &Calculate::div;
	//传对象
	cout << f5(cal, 10.0, 5.0) << endl;

	//传对象指针
	function<double(Calculate*, double, double)> f6 = &Calculate::div;
	cout << f6(&cal, 10.0, 5.0) << endl;

	//传对象引用
	function<double(Calculate&&, double, double)> f7 = &Calculate::div;
	cout << f7(move(cal), 10.0, 5.0) << endl;
}

5、绑定bind

bind - C++ Reference

bind 是⼀个函数模板,头文件为<functional>它也是⼀个可调用对象的包装器,可以把他看做⼀个函数适配器,对接收的可调用对象进行处理后返回⼀个可调用对象。bind 可以用来调整参数个数和参数顺序。
调用bind的⼀般形式: auto newCallable = bind(callable,arg_list); 其中
newCallable本身是⼀个可调用对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的
参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表示 newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表示生成的可调用对象 中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3....这些占 位符放到placeholders的⼀个命名空间中。
bind调整参数顺序(不常用)
//使用命名空间
using namespace placeholders;
int sub(int a, int b)
{
	return a - b;
}
void Test()
{
	//_1代表第一个参数,_2代表第二个参数
	auto b1 = bind(sub, _1, _2);
	cout << b1(10, 20) << endl;
}

如果交换位置呢?达到了调整参数顺序的目的

bind调整参数个数(常用)

在对某些对象调用时,有些参数是我们不期望改变的,我们可以直接使用bind将这些参数绑死,这样调用的时候就不用传递了。

例如:在包装调用成员函数时,我们每次调用都需要显示传递对象或者对象的指针,我们可以使用bind将这个参数绑定,就不需要每次显示传递了。

//使用命名空间
using namespace placeholders;
class Calculate
{
public:
	//成员函数
	double div(double x, double y)
	{
		return x / y;
	}
};
void Test()
{
	//_1代表第一个参数,_2代表第二个参数
	//绑死成员函数,以及传递的对象,只需要传递成员函数所需的参数。
	function<double(double, double)> f1 = bind(&Calculate::div, Calculate(), _1, _2);
	cout << f1(10, 20) << endl;
}

又或者我们提供给用户的一些接口,需要将某些参数固定,其余的参数交给用户去填写,例如计算银行不同理财产品获利的接口。我们可以将利率和年限绑死,本金交给用户,不同的理财产品提供多种不同的存期给用户计算利息。

using namespace placeholders;
void Test()
{
	// 计算复利的lambda表达式
	auto func = [](double rate, double money, int year)->double
		{
			double ret = money;
			for (int i = 0; i < year; i++)
			{
				ret += ret * rate;
			}
			return ret - money;
		};

	//将不同的利率绑死和存期绑死,留下本金给用户使用
	//存期3年 年利率 1.5%
	function<double(double)> func3_1_5 = bind(func, 0.015, _1, 3);
	//存期5年 年利率 1.5%
	function<double(double)> func5_1_5 = bind(func, 0.015, _1, 5);
	//存期10年 年利率 2.5%
	function<double(double)> func10_2_5 = bind(func, 0.025, _1, 10);
	//存期15年 年利率 3.5%
	function<double(double)> func20_3_5 = bind(func, 0.035, _1, 15);
	
	//本金用户来填
	int x;
	//填写本金计算利息
	cin >> x;
	cout << func3_1_5(x) << endl;
	cout << func5_1_5(x) << endl;
	cout << func10_2_5(x) << endl;
	cout << func20_3_5(x) << endl;
}

五、智能指针

前面的章节我们已经有过详细的讲解了,忘记的同学自行复习吧!

C++11 智能指针:优化资源管理,规避内存泄漏的利器-优快云博客https://blog.youkuaiyun.com/bite_zwy/article/details/143693033?spm=1001.2014.3001.5501

六、总结

        到这里关于C++11的重点内容我们就全部讲解完了,后续如果还有需要补充的,我会第一时间更新博文为大家讲解,创作不易,还请多多支持,如果觉得对你有帮助,可以关注博主,后续会为提供更多优质内容!在成为C++高手的路上与你一路相伴! 上述如有差错,还请各位大佬指点斧正!

评论 53
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一整颗红豆

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

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

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

打赏作者

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

抵扣说明:

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

余额充值