C++11重点语法

1、列表初始化

在C语言中对数组支持使用{}进行初始化,而在C++98对于vector这样的自定义类型不支持使用{}初始化,每次使用vector就必须先定义在使用for循环进行初始化,某些场景下是及其不方便的,在C++11中对很多自定义类型都支持使用{}进行初始化。

1)对于内置类型和STL容器的列表初始化

int main()
{
	//内置类型的列表初始化
	int x1 = { 10 };
	int x2{ 10 };
	int x3 = { 2 + 5 };
	int x4{ 2 + 5 };
	//char等其他内置类型也是如此
	char ch1 = { 'c' };
	char ch2{ 'c' };

	//数组---静态数组
	int arry1[] = {1,2,3,4,5};
	int arry2[]{1,2,3,4,5};
	int arry3[5]{1, 2, 3, 4, 5};

	//数组---动态数组
	int* array = new int[]{1, 2, 3, 4, 5};

	//STL容器
	vector<int> v{ 1, 2, 3, 4, 5 };
	map<int, int> mp{ {1,1}, {2,2}, {3,3} };
	return 0;
}

2)对于自定义类型的列表初始化

单个对象的列表初始化

class test
{
private:
	int a;
	int b;
public:
	test(int _a, int _b) :a(_a), b(_b)
	{}
};

int main()
{
	//和使用()调用构造函数没啥区别
	test t{ 1, 2 };
	return 0;
}

使用initializer_list对多个对象的列表初始化

initializer_list是一个模板类,它支持bengin()和end()迭代器以及size()获取其中元素个数,它的特点就是必须使用{}进行构造且支持变参数的构造。

initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。并且,拷贝或赋值一个initializer_list对象不会拷贝列表中的元素,其实只是引用而已,原始列表和副本共享元素。

和使用vector一样,我们也可以使用迭代器访问initializer_list里的元素

void error_msg(initializer_list<string> il)
{
   for(auto beg=il.begin();beg!=il.end();++beg)
      cout<<*beg<<" ";
   cout<<endl;
}

如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:

//expected和actual是string对象
if(expected != actual)
   error_msg({"functionX",expectde,actual});
else
   error_msg({"functionX","okay"});

initializer_list的作用:

template<class T>
class Vector {
public:
	// ...
	Vector(initializer_list<T> l) : _capacity(l.size()), _size(0)
	{
		_array = new T[_capacity];
		for (auto e : l)
			_array[_size++] = e;
	}
	Vector<T>& operator=(initializer_list<T> l) {
		delete[] _array;
		size_t i = 0;
		for (auto e : l)
			_array[i++] = e;
		return *this;
	}
	// ...
private:
	T* _array;
	size_t _capacity;
	size_t _size;
};

int main()
{
	return 0;
	//我们自己实现的类,也可以支持可变参数个数的构造了
	Vector<int> v{1,2,3,4,5};
}

也就是说,之前我们在使用STL库中使用vector<int> v{1,2,3,4,5}构造一个vector对象时,实际上编译器会自动帮助我们构造一个initializer_list对象,在调用vector<T>(initializer_list<T>)的构造函数构造出v对象。

 C++11中STL容器都支持了initializer_list构造

 2、变量类型推导

在定义变量时需要指定变量的类型,但是在C++中有些类型是非常复杂的(string迭代器std::string::iterator)甚至有时候我们无法确定变量的类型,这就需要编译器自己进行类型推导。

#include <map>
#include <string>
int main()
{
	short a = 32670;
	short b = 32670;
	// c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
	short c = a + b;
	std::map<std::string, std::string> m{ { "apple", "苹果" }, { "banana", "香蕉" } };
	// 使用迭代器遍历容器, 迭代器类型太繁琐
	std::map<std::string, std::string>::iterator it = m.begin();
	while (it != m.end())
	{
		cout << it->first << " " << it->second << endl;
		++it;
	}
	return 0;
}

C++11中使用auto关键字定义变量,编译器会根据变量的初始化值自动推到类型。

int main()
{
	auto a = 10;
	//自动推导出是int类型
	cout << sizeof(a) << endl;

	map<string, string> mp;
	map<string, string>::iterator it1;
	auto it2 = mp.begin();//使用auto定义变量声明和初始化不能分离
	return 0;
}

decltyoe类型推导

decltype是根据表达式返回计算后的类型推演出定义变量所需的类型,例如

推演表达式的类型作为变量定义的类型

int main()
{
	int a = 10;
	int b = 20;
	// 用decltype推演a+b的实际类型,作为定义c的类型
	decltype(a + b) c;
	//达因变量的类型
	cout << typeid(c).name() << endl;
	return 0;
}

推演函数返回值类型

void* GetMemory(size_t size)
{
	return malloc(size);
}
int main()
{
	// 如果没有带参数,推导函数的类型
	cout << typeid(decltype(GetMemory)).name() << endl;
	// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
	cout << typeid(decltype(GetMemory(0))).name() << endl;
	return 0;
}

3、范围for

在C++11中,STL中的容器都支持范围for,简化程序的编写

int main()
{
    vector<int> a{1,2,3,4,5};
    //范围for
    for(auto e:a)
        cout<<e<<endl;

    //还可以这样修改a
    for(auto& e:a)
        e = 1;
    return 0;
}

实际上,范围for的底层就是迭代器,只要支持迭代器的容器都可以支持迭代器。

注意:下面代码只是为了证明只要带迭代器都可以支持范围for,代码中存在一些问题请忽略,足以验证问题。

#include<initializer_list>
template<class T>
class Vector
{
public:
	typedef int* iterator;
	iterator begin()
	{
		return a;
	}
	iterator end()
	{
		//为了方便特殊处理了一下
		return a+5;
	}
	Vector(initializer_list<T> _a)
	{
		a = new T[_a.size()];
		int i = 0;
		for (auto e : _a)
		{
			a[i] = e;
			i++;
		}
	}
private:
	int *a;
};

int main()
{
	Vector<int> v{1,2,3,4,5};
	for (auto e : v)
		cout << e << endl;
	return 0;
}

4、final与override

在继承关系中,使用final修饰类或类中的方法,该方法不能被子类重写。

override则表示当前函数重写了父类中的函数,如果没有重写则会报错。

5、智能指针

智能指针是C++11中非常重要的一个特性,具体请参考:https://blog.csdn.net/qq_47406941/article/details/119313983

6、新增容器

静态数组array 

vector是动态数组,数组的大小可以动态增长,vector的数据是保存在堆上的。array静态数组它是一个模板类,是一个静态的数组,数组的大小不可以改变。

#include<array>
int main()
{
	//定义一个含有10个int类型的静态数组
	array<int,10> a;
	return 0;
}

单链表forward_list

C++STL中的list是一个双向循环链表,而forward_list是一个单链表

unordered_map和unordered_set

unordered_map和unordered_set底层是hash表,相比于map和set时间效率更高。

7、默认成员函数的控制

在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成
1)显式缺省函数

在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
 

class A
{
public:
    A(int a): _a(a)
    {}
    // 显式缺省构造函数,由编译器生成
    A() = default;
    // 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
    A& operator=(const A& a);
private:
    int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
    A a1(10);
    A a2;
    a2 = a1;
    return 0;
}

2)删除默认函数

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
 

class A
{
public:
	A(int a) : _a(a)
	{}
	// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
	A(const A&) = delete;
	A& operator(const A&) = delete;
private:
	int _a;
};
int main()
{
	A a1(10);
	// 编译失败,因为该类没有拷贝构造函数
	//A a2(a1);
	// 编译失败,因为该类没有赋值运算符重载
	A a3(20);
	a3 = a2;
	return 0;
}

8、右值引用

1)左值和右值

左值和右值是C语言中的概念,但是关于左值和右值并没有严格的规定。一般认为能放在=左边或者可以取地址的就是左值,而只能放在=右边或者不能取地址的就叫做右值,但是这都不是一定的。例如const常量只能在等号右边,但是他可以进行取地址。

  • 普通类型的变量,有名字可以取地址,都认为是左值。
  • const修饰的常量,c++11中认为其是左值
  • 临时变量或对象是右值,例如表达式的运行结果。
  • 如果表达式的运行结果是一个引用则认为是左值
  • 将亡值:表达式的中间结果,函数的非引用和指针返回值,它们在该表达式或函数执行完后就会被释放,像这样的值就称为将亡值。将亡值也可以做右值

2)引用和右值引用

右值引用是指引用右值的引用,它和引用相似,都是变量的别名。

区别:

  • 普通类型的引用只能引用左值不能引用右值,例如:int& a = 10;//编译错误
  • const普通引用既可以引用左值也可以引用右值。const int& a = 10;//正确
  • 右值引用只能直接引用右值,引用左值需要使用move

3)移动语义

移动语义:将一个对象的资源移动到另一个对象中。

移动语义的作用:

String operator+(const String& s)
{
    char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
    strcpy(pTemp, _str);
    strcpy(pTemp + strlen(_str), s._str);
    String strRet(pTemp);
    return strRet;
}

在上面的程序中,返回strRet时是按照值的方式进行返回。也就是说,会先使用strRet拷贝构造出一个临时对象,在使用该临时对象返回。拷贝完临时对象后,strRet就会被销毁。

如果有下面场景:string str1 = str2+str3;

也就是说,还需要使用临时对象在拷贝构造出str1,也就是说在这个过后才能中会有3个相同的str1,调用2次拷贝构造,但是最终只有str1会被保留,strRet和临时对象都会被释放,这不仅浪费空间还浪费时间。

移动语义就是说,在这种情况下,可以直接将strRet的资源移动给str1,减少了不必要的拷贝构造。

在C++11中移动语义是通过右值引用实现的,给上面的string类增加移动构造:

String(String&& s)
: _str(s._str)
{
    s._str = nullptr;
}

 这样在返回strRet编译器判断strRet是一个右值就会去调用它的移动构造,将strRet的空间移动给str1.减少了不必要的拷贝,节省空间提高效率。注意:这里的strRet就是我们所说的将亡值

  • 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
  • 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,程序员必须显式定义自己的移动构造。

4)右值引用引用左值

当需要使用右值引用引用左值时,可以使用move函数将左值转化成右值。

例如:

int a = 10;

int&& b = a;//编译错误
int&& b = move(a);

注意:

  • 被转化的左值的声明周期并不会随着被转化而改变。
  • STL中也有一个move函数,它的作用是将一个范围中的元素搬到另一个位置。
int main()
{
    String s1("hello world");
    String s2(move(s1));
    String s3(s2);
    return 0;
}

上面的代码中,将s1move后就会调用移动构造,最终s1就会变成空。 

5)完美转发

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

void Func(int x)
{
    // ......
}
template<typename T>
void PerfectForward(T t)
{
    Fun(t);
}

PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
C++11通过forward函数来实现完美转发,例如:

void Fun(int &x)
{
    cout << "lvalue ref" << endl;
}
void Fun(int &&x)
{
    cout << "rvalue ref" << endl;
}
void Fun(const int &x)
{
    cout << "const lvalue ref" << endl;
}
void Fun(const int &&x)
{
    cout << "const rvalue ref" << endl;
}
template<typename T>
void PerfectForward(T &&t)
{
    Fun(std::forward<T>(t));
}
int main()
{
    PerfectForward(10); // rvalue ref
    int a;
    PerfectForward(a); // lvalue ref

    PerfectForward(std::move(a)); // rvalue ref
    const int b = 8;
    PerfectForward(b); // const lvalue ref
    PerfectForward(std::move(b)); // const rvalue ref
    return 0;
}

6)右值引用的作用

  • 实现移动语义
  • 给中间临时变量取别名
  • 实现完美转发

9、lambda表达式

lambda表达式语法

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

  • capture-list捕捉列表:该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来
    的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • parameters参数列表:与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起
    省略
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
  • ->return-type返回值类型:用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分
    可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情

int main()
{
	//普通的lambda表达式
	[](int a, int b)->int{
		return a + b;
	};
	//省略参数和返回值类型,返回值类型有编译器推导,使用捕捉列表捕捉a,b
	int a = 10;
	int b = 2;
	[a,b]{
		return a + b;
	};

	//省略参数和返回值,无返回值
	[a, b]{
		cout << a << " " << b << endl;
	};

	//省略参数和返回值类型,无返回值;捕捉列表传引用
	//捕捉列表捕捉到的是变量的拷贝,不能直接修改,修改需要使用引用
	[&a, &b]{
		int c = a;
		a = b;
		b = c;
	};
	int c = 30;
	//捕捉该函数中的所有边量
	[=]{
		cout << a << " " << b << " " << c << endl;
	};

	//定义“函数变量”进行调用
	auto add = [](int a, int b)->int{
		return a + b;
	};

	cout << add(1, 2) << endl;

	cout << a << " " << b << endl;
	auto test = [&a,&b]{
		int c = a;
		a = b;
		b = c;
	};
	//不调用不会执行
	test();

	cout << a<<" "<<b << endl;

	return 0;
}

捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

  • [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针

注意:

  • 父作用域指包含lambda函数的语句块
  • 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 、
  • 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  • 在块作用域以外的lambda函数捕捉列表必须为空。
  • 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
  • lambda表达式之间不能相互赋值,即使看起来类型相同
void(*PF)();
int main()
{
	auto f1 = []{cout << "hello world" << endl; };
	auto f2 = []{cout << "hello world" << endl; };
	//f1 = f2; // 编译失败--->提示找不到operator=()
	// 允许使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();
	// 可以将lambda表达式赋值给相同类型的函数指针
	PF = f2;
	PF();
	return 0;
}

函数对象与lambda表达式

函数对象又称仿函数,就是在类中重载了operator()

class Rate
{
public:
	Rate(double rate) : _rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};
int main()
{
	// 函数对象
	double rate = 0.49;
	Rate r1(rate);
	r1(10000, 2);
	// lamber
	auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
	r2(10000, 2);
	return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到
 

 实际上lambda底层处理和函数对象是一样的,也就是说如果定义了一个lambda,编译器就会根据该lambda表达式生成一个类,该类重载了operator()函数。

10、线程库

C++11中提供了线程库,使得使用C++编写多线程程序时,不在依靠第三方库,程序的可移植性大大提高。

相关接口

thread();

  • 函数功能:创建一个线程对象,没有关联任何线程函数。

thread(fn,args1,args2);

  • 函数功能:构造一个线程对象,并关联函数fn,args1、args2为线程函数的参数。

get_id();

  • 函数功能:获取线程id

joinable();

  • 函数功能:线程是否正在执行,joinable代表一个正在执行的线程

join();

  • 函数功能:线程等待,这里是阻塞等

detach();

  • 函数功能:线程分离

注意:

  • 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
  • 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程 
  • 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:函数指针 、lambda表达式 、函数对象
#include <iostream>
using namespace std;
#include <thread>
void ThreadFunc(int a)
{
    cout << "Thread1" << a << endl;
}
class TF
{
public:
    void operator()()
    {
        cout << "Thread3" << endl;
    }
};
int main()
{
    // 线程函数为函数指针
    thread t1(ThreadFunc, 10);
    // 线程函数为lambda表达式
    thread t2([]{cout << "Thread2" << endl; });
    // 线程函数为函数对象
    TF tf;
    thread t3(tf);
    t1.join();
    t2.join();
    t3.join();
    cout << "Main thread!" << endl;
    return 0;
}
  • thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
  • 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:采用无参构造函数构造的线程对象;线程对象的状态已经转移给其他线程对象;线程已经调用jion或者detach结束。

注意:Linux部分对线程的基本概念和操作已经详细介绍,这里不在赘述

11、锁

在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。

1)mutex

C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:

  • lock();//加锁
  • unlock();//解锁
  • try_lock();//尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。

2)lock_guard和unique_lock

lock_guard

template<class _Mutex>
class lock_guard
{
public:
    // 在构造lock_gard时,_Mtx还没有被上锁
    explicit lock_guard(_Mutex& _Mtx)
        : _MyMutex(_Mtx)
    {
        _MyMutex.lock();
    }
    // 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
    lock_guard(_Mutex& _Mtx, adopt_lock_t)
        : _MyMutex(_Mtx)
    {}
    ~lock_guard() _NOEXCEPT
    {
        _MyMutex.unlock();
    }
    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;
private:
    _Mutex& _MyMutex;
};

通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock

unique_lock

与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。

面试题:两个线程轮流打印,1-100

#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>
using namespace std;
int main()
{
	//两个线程轮流打印,顺序打印1-100
	int a = 1;
	int b = 2;
	bool flag = true;
	//一个互斥锁
	mutex mtx;
	//一个条件变量
	condition_variable cd;
	//创建两个线程A和B
	thread t1([&]{
		while(a <= 100)
		{
			unique_lock<std::mutex> lck(mtx);
			cd.wait(lck, [flag]{return flag; });
			cout << a << " ";
			a += 2;
			flag = false;
			cd.notify_one();
		}
	});

	thread t2([&]{
		while (b <= 100)
		{
			unique_lock<std::mutex> lck(mtx);
			cd.wait(lck, [flag]{return !flag; });
			cout << b << " ";
			b += 2;
			flag = true;
			cd.notify_one();
		}
	});

	t1.join();
	t2.join();

	return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

疯狂嘚程序猿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值