C++进阶:C++11

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 能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更 强大,而且能提升程序员的开发效率 C++11 增加的语法特性非常篇幅非常多,这里没办法一 一讲解,所以本节主要讲解实际中比较实用的语法。

 统一的列表初始化

{}初始化

C++98 中,标准允许使用花括号 {} 对数组或者结构体元素进行统一的列表初始值设定。比如:
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 ;
}
C++11 扩大了用大括号括起的列表 ( 初始化列表 ) 的使用范围,使其可用于所有的内置类型和用户自
定义的类型,使用列表初始化时,可添加等号(=) ,也可不添加
struct Point
{
int _x ;
int _y ;
};
int main ()
{
int x1 = 1 ;
int x2 { 2 };
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 ;
}

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

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 ()
{
Date d1 ( 2022 , 1 , 1 ); // old style
// C++11 支持的列表初始化,这里会调用构造函数初始化
Date d2 { 2022 , 1 , 2 };
Date d3 = { 2022 , 1 , 3 };
return 0 ;
}

std::initializer_list

std::initializer_list的介绍文档:

initializer_list - C++参考 (cplusplus.com)

std::initializer_list 是什么类型:
int main ()
{
// the type of il is an initializer_list
auto il = { 10 , 20 , 30 };
cout << typeid ( il ). name () << endl ;
return 0 ;
}
std::initializer_list 使用场景:
std::initializer_list 一般是作为构造函数的参数, C++11 STL 中的不少容器就增加
std::initializer_list 作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为 operator=
的参数,这样就可以用大括号赋值。
即:容器想用不固定的{}数据个数初始化,initializer_list支持

声明

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

auto

C++98 auto 是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局
部的变量默认就是自动存储类型,所以 auto 就没什么价值了。 C++11 中废弃 auto 原来的用法,将
其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初
始化值的类型。
int main ()
{
int i = 10 ;
auto p = & i ;
auto pf = strcpy ;
cout << typeid ( p ). name () << endl ;
cout << typeid ( pf ). name () << endl ;
map < string , string > dict = { { "sort" , " 排序 " }, { "insert" , " 插入 " } };
//map<string, string>::iterator it = dict.begin();
auto it = dict . begin ();
return 0 ;
}

 decltype

关键字 decltype 将变量的类型声明为表达式指定的类型。
int main()
{
	list<int>::iterator it1;

	// typeid推出时一个单纯的字符串
	cout << typeid(it1).name() << endl;
	// 不能用来定义对象
	//typeid(it1).name() it2;

	// 可以用来定义对象
	decltype(it1) it2;
	cout << typeid(it2).name() << endl;

	auto it3 = it1;
	cout << typeid(it3).name() << endl;

	auto ret3 = func1();
	B<decltype(ret3)> bb1;

	map<string, string> dict2 = { {"sort", "排序"}, {"insert", "插入"} };
	auto it4 = dict2.begin();

	B<decltype(it4)> bb2;
	B<std::map<std::string, std::string>::iterator> bb2;
	// auto和decltype有些地方增加代码读起来难度

	return 0;
}

nullptr

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

STL中一些变化

新容器
用橘色圈起来是 C++11 中的一些几个新容器,但是实际最有用的是 unordered_map
unordered_set 。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可。

容器中的一些新方法
如果我们再细细去看会发现基本每个容器中都增加了一些 C++11 的方法,但是其实很多都是用得
比较少的。
比如提供了 cbegin cend 方法返回 const 迭代器等等,但是实际意义不大,因为 begin end 也是
可以返回 const 迭代器的,这些都是属于锦上添花的操作。
实际上 C++11 更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本。
但是这些接口到底意义在哪?网上都说他们能提高效率,他们是如何提高效率的?
请看下面的右值引用和移动语义章节的讲解。另外 emplace 还涉及模板的可变参数,也需要再继
续深入学习后面章节的知识。

右值引用和移动语义

左值引用和右值引用

传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以从现在开始我们
之前学习的引用就叫做左值引用。 无论左值引用还是右值引用,都是给对象取别名
什么是左值?什么是左值引用?
左值是一个表示数据的表达式 ( 如变量名或解引用的指针 ) 我们可以获取它的地址 ,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边 。定义时 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 ;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值 ( 这个不能是左值引
用返回 ) 等等, 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址 。右值引用就是对右值的引用,给右值取别名。
右值分为纯右值(内置类型)和将亡值(自定义类型)
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 ()
{
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 ;   // ra a 的别名
    //int& ra2 = 10;   // 编译失败,因为 10 是右值
    // const 左值引用既可引用左值,也可引用右值。
    const int & ra3 = 10 ;
    const int & ra4 = a ;
    return 0 ;
}
右值引用总结:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以 move 以后的左值。
int main ()
{
// 右值引用只能右值,不能引用左值。
int && r1 = 10 ;
// error C2440: “ 初始化 ”: 无法从 “int” 转换为 “int &&”
// message : 无法将左值绑定到右值引用
int a = 10 ;
int && r2 = a ;
// 右值引用可以引用 move 以后的左值
int && r3 = std::move ( a );
return 0 ;
}

右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么 C++11 还要提出右值引
用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
意义:减少拷贝,提高效率。
左值引用返回值的问题没有彻底解决,如果返回值是函数中的局部对象,不能用引用返回。
且效率提升,针对的是自定义类型的深拷贝的类,因为深拷贝的类才有转移资源的移动系列函数
对于内置类型,和浅拷贝自定义类型,没有移动系列函数。
namespace xx
{
	class string
	{
	public:
		//...
		
		
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}

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

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}

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

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

左值引用的使用场景:
做参数和做返回值都可以提高效率。
void func1 ( bit::string s )
{}
void func2 ( const bit::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 ;
}
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,
只能传值返回。例如: bit::string to_string(int value) 函数中可以看到,这里只能使用传值返回,
传值返回会导致至少 1 次拷贝构造 ( 如果是一些旧一点的编译器可能是两次拷贝构造 )

 

namespace xx
{
	bit::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		bit::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str += ('0' + x);
		}
		if (flag == false)
		{
			str += '-';
		}
		std::reverse(str.begin(), str.end());
		return str;
	}
}
int main()
{
	// 在bit::string to_string(int value)函数中可以看到,这里
	// 只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷
	贝构造)。
	bit::string ret1 = bit::to_string(1234);
	bit::string ret2 = bit::to_string(-1234);
	return 0;
}

右值引用和移动语义解决上述问题:
bit::string 中增加移动构造, 移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
// 移动构造
string ( string && s )
: _str ( nullptr )
, _size ( 0 )
, _capacity ( 0 )
{
cout << "string(string&& s) -- 移动语义 " << endl ;
swap ( s );
}
int main ()
{
bit::string ret2 = bit::to_string (- 1234 );
return 0 ;
}
再运行上面 bit::to_string 的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用
了移动构造,原因:str被编译器自动识别成右值,即将亡值(出了作用域str就销毁)。移动构造中没有新开空间,拷贝数据,所以效率提高了。
不仅仅有移动构造,还有移动赋值:
bit::string 类中增加移动赋值函数,再去调用 bit::to_string(1234) ,不过这次是将
bit::to_string(1234) 返回的右值对象赋值给 ret1 对象,这时调用的是移动构造。
// 移动赋值
string & operator = ( string && s )
{
cout << "string& operator=(string&& s) -- 移动语义 " << endl ;
swap ( s );
return * this ;
}
int main ()
{
bit::string ret1 ;
ret1 = bit::to_string ( 1234 );
return 0 ;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义
这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象
接收,编译器就没办法优化了。 bit::to_string 函数中会先用 str 生成构造生成一个临时对象,但是
我们可以看到,编译器很聪明的在这里把 str 识别成了右值,调用了移动构造。然后在把这个临时
对象做为 bit::to_string 函数调用的返回值赋值给 ret1 ,这里调用的移动赋值。
STL 中的容器都是增加了移动构造和移动赋值:

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

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能
真的需要用右值去引用左值实现移动语义。 当需要用右值引用引用一个左值时,可以通过 move
函数将左值转化为右值 C++11 中, std::move() 函数 位于 头文件中,该函数名字具有迷惑性,
并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
template < class _Ty >
inline typename remove_reference < _Ty > :: type && move ( _Ty && _Arg ) _NOEXCEPT
{
// forward _Arg as movable
return (( typename remove_reference < _Ty > :: type && ) _Arg );
}
int main ()
{
bit::string s1 ( "hello world" );
// 这里 s1 是左值,调用的是拷贝构造
bit::string s2 ( s1 );
// 这里我们把 s1 move 处理以后 , 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现 s1
// 资源被转移给了 s3 s1 被置空了。
bit::string s3 ( std::move ( s1 ));
return 0 ;
}

STL容器插入接口函数也增加了右值引用版本:

list::p ush_back - C++参考 (cplusplus.com)

vector::p ush_back - C++参考 (cplusplus.com)

void push_back ( value_type && val );
int main ()
{
list < bit::string > lt ;
bit::string s1 ( "1111" );
// 这里调用的是拷贝构造
lt . push_back ( s1 );
// 下面调用都是移动构造
lt . push_back ( "2222" );
lt . push_back ( std::move ( s1 ));
return 0 ;
}
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义

完美转发 

函数模板中的 && 万能引用
(将T&&看成一个整体)

 
 

// 函数模版里面,这里可以叫万能引用

// 实参传左值,就推成左值引用

// 实参传右值,就推成右值引用

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 完美转发在传参的过程中保留对象原生类型属性

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

 完美转发实际中的使用场景:

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<bit::string> lt;
	lt.PushBack("1111");
	lt.PushFront("2222");
	return 0;
}

确保参数层层传递的过程中保持右值属性!!!不然传一层就成左值了

新的类功能

默认成员函数
原来 C++ 类中,有 6 个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
最后重要的是前 4 个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
意一个(条件严格,这三个通常要写一起写)那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。 ( 默认移动赋值跟上面移动构造
完全类似 )
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
默认 移动构造函数和默认移动赋值应用场景:
// 以下代码在 vs2013 中不能体现,在 vs2019 下才能演示体现上面的特性。
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& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
/*~Person()
{}*/
private :
xx::string _name ;
int _age ;
};
int main ()
{
Person s1 ;
Person s2 = s1 ;
Person s3 = std::move ( s1 );
Person s4 ;
s4 = std::move ( s2 );
return 0 ;
}
类成员变量初始化
C++11 允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这
个我们在类和对象默认就讲了,这里就不再细讲了。
强制生成默认函数的关键字 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 :
xx::string _name ;
int _age ;
};
int main ()
{
Person s1 ;
Person s2 = s1 ;
Person s3 = std::move ( s1 );
return 0 ;
}
禁止生成默认函数的关键字 delete:
如果能想要限制某些默认函数的生成,在 C++98 中,是该函数设置成 private ,并且只声明补丁
已,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需在该函数声明加上 =delete
可,该语法指示编译器不生成对应函数的默认版本,称 =delete 修饰的函数为删除函数。
class Person
{
public :
Person ( const char* name = "" , int age = 0 )
: _name ( name )
, _age ( age )
{}
Person ( const Person & p ) = delete ;
private :
xx::string _name ;
int _age ;
};
int main ()
{
Person s1 ;
Person s2 = s1 ;
Person s3 = std::move ( s1 );
return 0 ;
}
继承和多态中的 final override 关键字
这个我们在继承和多态章节已经进行了详细讲解这里就不再细讲,需要的话去复习继承和多态章
节吧。

lambda表达式

C++98 中的一个例子
C++98 中,如果想要对一个数据集合中的元素进行排序,可以使用 std::sort 方法。
#include <algorithm>
#include <functional>
int main ()
{
int array [] = { 4 , 1 , 8 , 5 , 3 , 7 , 0 , 9 , 2 , 6 };
// 默认按照小于比较,排出来结果是升序
std::sort ( array , array + sizeof ( array ) / sizeof ( array [ 0 ]));
// 如果需要降序,需要改变元素的比较规则
std::sort ( array , array + sizeof ( array ) / sizeof ( array [ 0 ]), greater < int > ());
return 0 ;
}

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

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 表达式。

lambda表达式例子

例1:


int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };


	//方法一:
	auto priceLess = [](const Goods& g1, const Goods& g2)
	{
		return g1._price < g2._price;
	};
	sort(v.begin(), v.end(), priceLess);

	//cout << typeid(priceLess).name() << endl;


	//方法二:
	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;
}

上述代码就是使用 C++11 中的 lambda 表达式来解决,可以看出 lambda 表达式实际是一个匿名函
例2:
int main()
{
	// lambda
	auto add1 = [](int a, int b)->int {return a + b; };
	// 返回值可以省略
	auto add2 = [](int a, int b) {return a + b; };
	// 没有参数,参数列表可以省略
	auto func1 = [] {cout << "hello world" << endl; };

	cout << typeid(add1).name() << endl;
	cout << typeid(add2).name() << endl;
	cout << typeid(func1).name() << endl;

	cout << add1(1, 2) << endl;
	func1();

	return 0;
}

lambda表达式语法

lambda 表达式书写格式: [capture-list] (parameters) mutable -> return-type { statement
}
1. lambda 表达式各部分说明
[capture-list] : 捕捉列表 ,该列表总是出现在 lambda 函数的开始位置, 编译器根据 [] 判断接下来的代码是否为 lambda 函数 捕捉列表能够捕捉上下文中的变量供 lambda 函数使用
(parameters) :参数列表。与 普通函数的参数列表一致 ,如果不需要参数传递,则可以连同() 一起省略。
mutable :默认情况下, lambda 函数总是一个 const 函数, mutable 可以取消其常量性。使用该修饰符时,参数列表不可省略( 即使参数为空 )。  (mutable主要用来可以修改传值捕捉对象
->returntype :返回值类型 。用 追踪返回类型形式声明函数的返回值类型 ,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推
{statement} :函数体 。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
lambda 函数定义中, 参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为
。因此 C++11 最简单的 lambda 函数为: []{} ; lambda 函数不能做任何事情。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

int main()
{
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[] {};

	// 省略参数列表和返回值类型,返回值类型由编译器推导为int
	int a = 3, b = 4;
	[=] {return a + 3; };

	// 省略了返回值类型,无返回值类型
	auto fun1 = [&](int c) {b = a + c; };
	fun1(10);
		cout << a << " " << b << endl;

	// 各部分都很完善的lambda函数
	auto fun2 = [=, &b](int c)->int {return b += a + c; };
	cout << fun2(10) << endl;

	// 复制捕捉x
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;
	return 0;
}

通过上述例子可以看出, lambda 表达式实际上可以理解为无名函数,该函数无法直接调
用,如果想要直接调用,可借助 auto 将其赋值给一个变量。
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 表达式之间不能相互赋值 ,即使看起来类型相同
void ( * PF )();
int main ()
{
auto f1 = []{ cout << "hello world" << endl ; };
auto f2 = []{ cout << "hello world" << endl ; };
    // 此处先不解释原因,等 lambda 表达式底层实现原理看完后,大家就清楚了
//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 表达式,编译器会自动生成一个类,在该类中重载了 operator()

可变参数模板

C++11 的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比
C++98/03 ,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改
进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现
阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大
家如果有需要,再可以深入学习。
下面就是一个基本可变参数的函数模板
// Args 是一个模板参数包, args 是一个函数形参参数包
// 声明一个参数包 Args...args ,这个参数包中可以包含 0 到任意个模板参数。
template < class ... Args >
void ShowList ( Args ... args )
{}
上面的参数 args 前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为 参数
,它里面包含了 0NN>=0 )个模版参数。我们无法直接获取参数包 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 ;
}

逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在 expand 函数体中展开的 , printarg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式
实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。expand函数中的逗号表达式: (printarg(args), 0) ,也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果 0 。同时还用到了 C++11 的另外一个特性 —— 初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...} 将会展开成 ((printarg(arg1),0), (printarg(arg2),0),(printarg(arg3),0), etc... ),最终会创建一个元素值都为 0 的数组int arr[sizeof... (Args)] 。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分 printarg(args) 打印出参数,也就是说在构造 int 数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在 数组构造的过程展开参数包
template < class T >
void PrintArg ( T t )
{
cout << t << " " ;
}
// 展开函数
template < class ... Args >
void ShowList ( Args ... args )
{
int arr [] = { ( PrintArg ( args ), 0 )... };
cout << endl ;
}
int main ()
{
ShowList ( 1 );
ShowList ( 1 , 'A' );
ShowList ( 1 , 'A' , std::string ( "sort" ));
return 0 ;
}

STL容器中的empalce相关接口函数:

vector::emplace_back - C++参考 (cplusplus.com)

list::emplace_back - C++参考 (cplusplus.com)

template < class ... Args >
void emplace_back ( Args && ... args );
首先我们看到的 emplace 系列的接口,支持模板的可变参数,并且万能引用。那么相对 insert
emplace 系列接口的优势到底在哪里呢?
例子一:
int main ()
{
std::list < std::pair < int , char > > mylist ;
// emplace_back 支持可变参数,拿到构建 pair 对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和 push_back 没什么太大的区别
mylist . emplace_back ( 10 , 'a' );
mylist . emplace_back ( 20 , 'b' );
mylist . emplace_back ( make_pair ( 30 , 'c' ));
mylist . push_back ( make_pair ( 40 , 'd' ));
mylist . push_back ({ 50 , 'e' });
for ( auto e : mylist )
cout << e . first << ":" << e . second << endl ;
return 0 ;
}

例子二: 

int main ()
{
// 下面我们试一下带有拷贝构造和移动构造的 bit::string ,再试试呢
// 我们会发现其实差别也不到, emplace_back 是直接构造了, push_back
// 是先构造,再移动构造,其实也还好。
std::list < std::pair < int , bit::string > > mylist ;
mylist . emplace_back ( 10 , "sort" );
mylist . emplace_back ( make_pair ( 20 , "sort" ));
mylist . push_back ( make_pair ( 30 , "sort" ));
mylist . push_back ({ 40 , "sort" });
return 0 ;
}

例子三: 

int main()
{
	bit::list<pair<bit::string, int>> lt;
	cout << endl;

	pair<bit::string, int> kv1("xxxxx", 1);
	lt.push_back(kv1);
	lt.push_back(move(kv1));
	cout << endl;

	// 直接传pair的对象效果跟push_back系列是一样的
	pair<bit::string, int> kv2("xxxxx", 1);
	lt.emplace_back(kv2);
	lt.emplace_back(move(kv2));
	cout << endl;

	// 直接传构造pair的参数包,参数包一直往下传,底层直接构造
	lt.emplace_back("xxxxx", 1);

	return 0;
}

底层实现->实例化成具体函数:

主要区别:emplace接口直接传构造对象的参数包才体现出与insert接口不同的效果

insert系列接口通常接受值或者一个由值构造的对象作为参数,然后将其插入到容器中。而emplace系列接口则接受的是构造容器内元素所需的参数,并直接在容器内构造对象,而不需要进行拷贝或移动操作。

因此,emplace系列接口相对于insert系列接口具有以下优势:

1.减少了不必要的拷贝或移动操作:通过直接在容器内构造对象,emplace系列接口避免了将参数进行拷贝或移动到容器内的过程,可以提高性能。

2.更灵活:emplace系列接口可以接受任意数量的参数,并根据参数列表直接在容器内构造对象,因此更加灵活,能够处理更多种类的构造方式。

总的来说,相对于insert系列接口,emplace系列接口在性能上有一定的优势,并且能够提供更灵活的构造方式,特别是在处理大量元素或者需要支持移动语义的情况下,emplace系列接口会更加高效。

总结

 

包装器

function包装器

function - C++ Reference (cplusplus.com)

function 包装器 也叫作适配器。 C++ 中的 function 本质是一个类模板,也是一个包装器
那么我们来看看,我们为什么需要 function 呢?-》统一类型,能够以统一的方式调用。
包装器:即在函数指针、仿函数对象、lambda上再套层壳

//ret = func(x);

// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
//是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
//为什么呢?我们继续往下看
template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{// 函数名
	cout << useF(f, 11.11) << endl;
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
	return 0;
}

//结果:三次count的地址都不同,即函数模板实例化了三份

通过上面的程序验证,我们会发现 useF 函数模板实例化了三份。
包装器可以很好的解决上面的问题
std::function 在头文件 < functional >
// 类模板原型如下
template < class T > function ;     // undefined
template < class Ret , class ... Args >
class function < Ret ( Args ...) > ;
模板参数说明:
Ret : 被调用函数的返回类型
Args… :被调用函数的形参
// 使用方法如下:
#include <functional>
int f(int a, int b)
{
	return a + b;
}
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};
class Plus
{
public:
	static int plusi(int a, int b)  //静态成员函数没有this指针
	{
		return a + b;
	}
	double plusd(double a, double b)  //this 指针是 “ 成员函数 ” 第一个隐含的指针形参
	{
		return a + b;
	}
};
int main()
{
	// 函数名(函数指针)
	std::function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;

	// 函数对象
	std::function<int(int, int)> func2 = Functor();
	cout << func2(1, 2) << endl;

	// lamber表达式
	std::function<int(int, int)> func3 = [](const int a, const int b)
	{return a + b; };
	cout << func3(1, 2) << endl;

	// 类的成员函数
	// 成员函数的函数指针  规定写法(一定加上&):&类型::函数名
	std::function<int(int, int)> func4 = &Plus::plusi;
	cout << func4(1, 2) << endl;

	//这里参数Plus(或Plus*)不是传递this指针参数,因为this指针不能显示传递,而是传递对象(记住即可)
	//写法一:
	std::function<double(Plus, double, double)> func5 = &Plus::plusd;
	cout << func5(Plus(), 1.1, 2.2) << endl;

	//写法二:
	std::function<double(Plus*, double, double)> func6 = &Plus::plusd;
	Plus plus;
	cout << func6(&plus, 1.1, 2.2) << endl;

	return 0;
}

有了包装器,如何解决模板的效率低下,实例化多份的问题呢?

#include <functional>
template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	// 函数名
	std::function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;
	// 函数对象
	std::function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;
	// lamber表达式
	std::function<double(double)> func3 = [](double d)->double { return d /
		4; };
	cout << useF(func3, 11.11) << endl;
	return 0;
}

 

包装器只实例化了一份 ,效率提升!!!

包装器的其他一些场景:
// 使用包装器以后的玩法
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;

        //命令->动作(函数)
        map<string,function<int(int,int)>> opfucMap{
            {"+",[](int a,int b){return a+b;}},
            {"-",[](int a,int b){return a-b;}},
            {"*",[](int a,int b){return a*b;}},
            {"/",[](int a,int b){return a/b;}}
        };

        for(auto& str:tokens)
        {
            if(opfucMap.count(str)) // 操作符 (为1即为操作符,操作符运算,运算后的结果再入栈)
            {
                int right=st.top();
                st.pop();
                int left=st.top();
                st.pop();

                st.push(opfucMap[str](left,right));
            }
            else    // 操作数 (为0即为操作数,操作数入栈)
            {
                st.push(stoi(str));
            }
        }
        return st.top();
    }
};

bind(绑定)

​​​​​​​绑定 - C++ 参考 (cplusplus.com)

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器)

接受一个可 调用对象callable object),生成一个新的可调用对象来适应原对象的参数列表。一般而 言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M 可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺 序调整等操作。

用处:调整参数顺序和调整参数个数
// 原型如下:
template < class Fn , class ... Args >
/* unspecified */ bind ( Fn && fn , Args && ... args );
// with return type (2)
template < class Ret , class Fn , class ... Args >
/* unspecified */ bind ( Fn && fn , Args && ... args );
模板参数说明:
Ret : 被调用函数的返回类型(一般不关心,很少使用)用法:
Args… :被调用函数的形参
Fn: 接受一个可 调用对象

Ret场景简单用法:

可以将 bind 函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对
象来 适应 原对象的参数列表。
调用 bind 的一般形式: auto newCallable = bind(callable,arg_list);
其中, newCallable 本身是一个可调用对象, arg_list 是一个逗号分隔的参数列表,对应给定的
callable 的参数。 当我们调用 newCallable 时, newCallable 会调用 callable, 并传给它 arg_list
的参数
arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数,这些参数是 占位符 ”( placeholder ),表示 newCallable的参数,它们占据了传递给 newCallable 的参数的 位置 。数值 n 表示生成的可调用对象中参数的位置: _1newCallable的第一个参数,_2 为第二个参数,以此类推。
例子1: 调整参数顺序
#include<iostream>
using namespace std;

#include <functional>

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

int main()
{
	//调整顺序
	auto f2 = bind(Sub, placeholders::_1, placeholders::_2);
	cout << f2(10, 5) << endl;

	auto f1 = bind(Sub, placeholders::_2, placeholders::_1);
	cout << f1(10, 5) << endl;

	return 0;
}

例子2: 调整参数个数
#include<iostream>
using namespace std;

#include <functional>

class Sub
{
public:
	Sub(int x)
		:_x(x)
	{}

	int sub(int a, int b)
	{
		return (a - b) * _x;
	}

private:
	int _x;
};

int main()
{	// 绑定,调整参数个数
	auto f4 = bind(&Sub::sub, Sub(1), placeholders::_1, placeholders::_2);
	cout << f4(10, 5) << endl;

	这么写也行
	//auto f4 = bind(&Sub::sub, &Sub(1), placeholders::_1, placeholders::_2);
	//cout << f4(10, 5) << endl;

	auto f5 = bind(&Sub::sub, &Sub(2), placeholders::_1, placeholders::_2);
	cout << f5(10, 5) << endl;
	return 0;
}

例子3:调整参数个数(参数位置任意绑定)

#include<iostream>
using namespace std;

#include <functional>

void fx(const string& name, int x, int y)
{
	cout << name << "->[" << "血量:" << x << ",蓝:" << y << ']' << endl;
}

int main()
{
以前写法,第一个参数一样的每次都要写,麻烦
//fx("王昭君", 80, 20);
//fx("王昭君", 85, 10);
//fx("王昭君", 99, 0);
//fx("王昭君", 99, 80);
//
//fx("亚瑟", 99, 80);
//fx("亚瑟", 91, 80);
//fx("亚瑟", 5, 80);



	//使用bind后的写法
	// 
	//绑定第一个参数
	auto f6 = bind(fx, "王昭君", placeholders::_1, placeholders::_2);

	f6(80, 20);
	f6(85, 10);
	f6(99, 0);
	f6(99, 80);

	auto f7 = bind(fx, "亚瑟", placeholders::_1, placeholders::_2);

	f7(80, 20);
	f7(85, 10);
	f7(99, 0);
	f7(99, 80);

	//绑定中间参数
	function<void(std::string, int)> f8 = bind(fx, placeholders::_1, 80, placeholders::_2);
	f8("武则天", 50);
	f8("韩信", 40);

	return 0;
}

例子4:与function联系
// 使用举例
#include <functional>
int Plus(int a, int b)
{
	return a + b;
}
class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};
int main()
{
	//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
	std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,placeholders::_2);

	//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
	
	//func2的类型为 function<int(int, int)> 与func1类型一样
	//表示绑定函数 plus 的第一,二为: 1, 2
	auto func2 = std::bind(Plus, 1, 2);
	cout << func1(1, 2) << endl;
	cout << func2() << endl;
	Sub s;
	// 绑定成员函数
	std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,placeholders::_1, placeholders::_2);

	// 参数调换顺序
	std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,placeholders::_2, placeholders::_1);
	cout << func3(1, 2) << endl;
	cout << func4(1, 2) << endl;
	return 0;
}

​​​​​​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值