【C++11】

【知识预告】

  1. C++11简介
  2. 统一的列表初始化
  3. 变量类型推导
  4. 范围for循环
  5. 智能指针
  6. 新增加容器—静态数组array、forward_list以及unordered系列
  7. 右值引用
  8. 新的类功能
  9. 可变参数模板
  10. lambda表达式

1 C++11简介

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。

插入一个小故事

1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。

2 统一的列表初始化

2.1 {}初始化

struct Point
{
	int _x;
	int _y;
};

// 列表初始化
int main()
{
	int array1[] = { 1, 2, 3, 4, 5 };
	int array2[5] = { 0 };
	Point p = { 1, 2 };
	return 0;
}
// 一切都可以列表初始化
// 并且可以省略掉赋值符号=
int main()
{
	int x1 = 1;
	int x2 = { 2 };
	int x3{ 3 };

	int array1[]{ 1, 2, 3, 4, 5 };
	int array2[5]{ 0 };
	Point p{ 1, 2 };

	// C++11中列表初始化也可以适用于new表达式中
	int* pa = new int[4] { 0 };
	return 0;
}
int main()
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}

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

int main()
{
	// C++11支持的列表初始化,这里会调用构造函数初始化
	Date d1(2022, 1, 1); // old style

	// 类型转换  构造+拷贝构造-》优化直接构造
	Date d2{ 2022, 1, 2 };
	Date d3 = { 2022, 1, 3 };

	Date* p1 = new Date[3]{ d1, d2, d3 };
	Date* p2 = new Date[3]{ {2025, 5, 4}, {2025, 5, 5}, {2025, 5, 6} };

	return 0;
}

2.2 std::initializer_list

std::initializer_list的介绍文档

2.2.1 std::initializer_list是什么类型:

int main()
{
	// the type of il is an initializer_list 
	auto il1 = { 10, 20, 30, 40, 50 };
	cout << typeid(il1).name() << endl;  // 打印il1的类型
	// class std::initializer_list<int>
	
	return 0;
}
int main()
{
	auto il1 = { 10, 20, 30, 40, 50 };

	initializer_list<int> il2 = { 10, 20, 30 };

	initializer_list<int>::iterator it2 = il2.begin();
	while (it2 != il2.end())   // 10 20 30
	{
		cout << *it2 << " ";
		++it2;
	}
	cout << endl;

	//for (int e : il2)
	for (auto e : il2)   // 10 20 30
	{
		cout << e << " ";
	}
	cout<< endl;

	return 0;
}

2.2.2 std::initializer_list使用场景

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。

说白了initializer_list最大的意义是:支持容器用列表的方式去初始化(本质是由initializer_list的构造函数支持的)

int main()
{
	pair<string, string> kv1("sort", "排序");
	map<string, string> dict = {{"insert", "插入"}, {"get","获取"} };
	for (auto& kv : dict)
	{
		cout << kv.first << ":" << kv.second << endl;
	}

	return 0;
}

3 声明

c++11提供了多种简化声明的方式,尤其是在使用模板时。

3.1 auto

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

// 最大的意义就是自动识别类型
int main()
{
	int i = 10;
	auto p = &i;
	cout << typeid(p).name() << endl;
	// int * __ptr64

	map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
	//map<string, string>::iterator it = dict.begin();
	auto it = dict.begin();
	
	return 0;
}

3.2 decltype

关键字decltype将变量的类型声明为表达式指定的类型。(大白话:decltype可以推导对象的类型,这个类型是可以用,主要用于模板实参,或者再定义对象

int main()
{
	int i = 1;
	double d = 2.2;

	// 类型以字符串形式获取到
	cout << typeid(i).name() << endl;  // int
	cout << typeid(d).name() << endl;  // double

	// typeid(i).name() j;  报错
	auto j = i;

	// 假设我不知道ret的类型
	auto ret = i * d;
	//decltype(ret) x;

	// 但是我又需要使用这个类型,怎么办?
	vector<decltype(ret)> v;  // 用类型去初始化
	// decltype可以推导对象的类型。这个类型是可以用
	// 用来模板实参,或者再定义对象

	v.push_back(1);
	v.push_back(1.1);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

3.3 nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

4 范围for循环

请参考前面的博客类容,会用就行,底层原理是迭代器。

5 智能指针

这个也是C++11的新东西,后面会写一个博客,详细介绍智能指针。

6 STL中一些变化

用红色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set
在这里插入图片描述

7 右值引用和移动语义

7.1 左值引用和右值引用

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

问题1、什么是左值?什么是左值引用?

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

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;   // b是左值
	int a = b;   // 左值也可能在右边
	const int c = 2;

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

问题2、什么是右值?什么是右值引用?

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

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);

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

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。

int main()
{
	int i = 0;
	int j = i;
	
	// 左值引用能否给右值取别名?不能,但是const左值引用可以
	const int& r1 = 10;
	const int& r2 = i + j;

	int&& rr1 = 10;
	rr1 = 20;   // 不报错

	int&& rr2 = i+j;

	// 右值引用能否给左值取别名?不能,但是右值引用可以给move(左值)取别名
	// int&& rr3 = i;   // 报错
	int&& rr3 = move(i);  // 不报错

	return 0;
}

问题3、左值引用的短板是什么?

答:但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

而右值引用和移动构造解决上述问题,**移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。**移动构造中没有新开空间,也没有拷贝数据,所以效率提高了

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

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

int main()
{
	int a = 1;

	// int&& aa = move a;
	// 报错,因为a是一个左值

	// 使用move将a的左值属性改变为右值
	int&& aa = move(a);

	return 0;
}

7.3 完美转发

  • 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
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;
}

在这里插入图片描述

模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

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

// 函数模板:万能引用
template<typename T>
void PerfectForward(T&& t)
{
	Fun(std::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;
}

在这里插入图片描述

8 新的类功能

原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

最重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

C++11 新增了两个:移动构造函数和移动赋值运算符重载

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

8.1 类成员变量初始化

C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化。

8.2 强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}

	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}

	Person(Person&& p) = default;   // 显示指定移动构造生成

private:
	string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

8.3 禁止生成默认函数的关键字delete

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

8.4 继承和多态中的final与override关键字

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名,字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果时,通过debug才会发现,这样得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写
// final修饰类,不能被继承
// final修饰虚函数,不能被重写
class Car
{
public:
	virtual void Drive() final 
	{}
};

class Benz : public Car
{
public:
	virtual void Drive()   // 报错,final修饰则不能继承
	{ 
		cout << "Benz-舒适" << endl; 
	}
};
  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car 
{
public:
	virtual void Drive() 
	{}
};

class Benz :public Car 
{
public:
	// override修饰派生类的虚函数
	// 检查是否完成重写
	virtual void Drive() override 
	{ 
		cout << "Benz-舒适" << endl;
	}
};

9 可变参数模板

C++11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段掌握一些基础的可变参数模板特性就够用了。

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

int main()
{
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	ShowList(1, 2.2, 'x', 3.3);

	return 0;
}

下面方法可以展开打印参数包:

void _ShowList()
{
	cout << endl;
}

// 编译时的递归推演
// 第一个模板参数依次解析获取参数值
template <class T, class ...Args>
void _ShowList(const T& val, Args... args)
{
	cout << val << " ";
	_ShowList(args...);
}

template <class ...Args>
void ShowList(Args... args)
{
	_ShowList(args...);
}

int main()
{
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	ShowList(1, 2.2, 'x', 3.3);

	return 0;
}

10 lambda表达式

如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

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;
	}
};

// sort的头文件是#include<algorithm>
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());

	return 0;
}

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

10.1 lambda表达式语法

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

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

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

// 格式:[capture-list] (parameters) mutable -> return-type { statement }
int main()
{
	// 匿名函数对象
	// class <lambda_ba37a3eb9b8e2495e3aae6ef76d9eed2>
	// <lambda_uuid>
	auto f1 = [](int x)->int {cout << x << endl; return 0; };
	f1(1);
	cout << typeid(f1).name() << endl;

	//class <lambda_8c5f46db939004e569f343c4f7d08c27>
	auto f2 = [](int x)
	{
		cout << x << endl;
		return 0;
	};
	f2(2);

	//f1 = f2;   // 报错,因为类型不一样

	cout << typeid(f2).name() << endl;

	return 0;
}

所以上面的代码可以改进为:

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)->bool {return g1._price < g2._price; });

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate < g2._evaluate; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate > g2._evaluate; });

	return 0;
}

10.2 lambda表达式使用

场景(一) 普通使用

int main()
{
	int x = 0, y = 1;
	cout << x << " " << y << endl;

	auto f1 = [](int& r1, int& r2) {
		int tmp = r1;
		r1 = r2;
		r2 = tmp;
	};

	f1(x, y);
	cout << x << " " << y << endl << endl;

	return 0;
}

场景(二)

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

    • [var]:表示值传递方式捕捉变量var
    • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
    • [&var]:表示引用传递捕捉变量var
    • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
    • [this]:表示值传递方式捕捉当前的this指针
int main()
{
	int x = 0, y = 1;
	cout << x << " " << y << endl;

	// 捕捉过来的参数,只能用但是不能修改
	// auto f2 = [x, y]() { int tmp = x; x = y; y = tmp; };
	 
	// 加个mutable,表示可以修改
	// auto f2 = [x, y]() mutable { int tmp = x; x = y; y = tmp; };
	
	// 但是由于是传值捕捉,形参的修改不会影响实参
	// 所以得引用捕捉
	// 这里的引用容易和取地址符号混淆,C++的弊端
	// 下面就是正确的写法
	auto f2 = [&x, &y]() mutable {
		int tmp = x;
		x = y;
		y = tmp;
	};

	f2();
	cout << x << " " << y << endl << endl;

	return 0;
}
class AA
{
public:
	void func()
	{
		/*auto f1 = [this]() {
			cout << a1 << endl;
			cout << a2 << endl;
		};*/

		// 捕获所有,包括this指针
		auto f1 = [=]() {
			cout << a1 << endl;
			cout << a2 << endl;
		};

		f1();
	}
private:
	int a1 = 1;
	int a2 = 1;
};
int main()
{
	int x = 0, y = 1, z = 2;

	// 混着用,值捕获所有,其中z是引用捕获
	auto f1 = [=, &z]() {
		z++;

		cout << x << endl;   // 0
		cout << y << endl;   // 1
		cout << z << endl;   // 3
	};

	f1();

	return 0;
}

11 bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。(头文件是functional)。

int Sub(int a, int b)
{
	return a - b;
}

int main()
{
	function<int(int, int)> f1 = Sub;
	cout << f1(10, 5) << endl;   // 5

	// 调整参数顺序
	function<int(int, int)> f2 = bind(Sub, placeholders::_2, placeholders::_1);
	cout << f2(10, 5) << endl;  // -5

	// 调整参数个数,有些参数可以bind时写死
	function<int(int)> f3 = bind(Sub, 20, placeholders::_1);
	cout << f3(5) << endl;   // 15

	return 0;
}
void func(int a, int b, int c)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

int main()
{
	function<void(int, int)> f1 = bind(func, placeholders::_1, 10, placeholders::_2);
	f1(1, 3);   // 输出结果:1   10   3

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值