C++11

目录

一.   C++11的大优势

二.   列表初始化

2.1   内置类型初始化

2.2   自定义类型初始化

三.   变量类型推导

3.1   为什么需要类型推导

3.2   auto

3.3   decltype

 四.   STL中容器新变化

4.1   新增容器

4.2   新增方法

五.   右值引用与移动语义(炒鸡重要)

5.1   左值引用与右值引用

5.1.1   左值与左值引用

5.1.2   右值与右值引用

5.1.3   左值引用与右值引用总结与比较

5.1.4   右值引用的意义

 5.2   移动语义

5.2.1   移动拷贝 

5.2.2   移动赋值 

 5.2.3   右值被右值引用之后,其属性为左值

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

 六.   完美换发

 七.   新的类功能

 八.   关键词delete

九.    可变参数模板

9.1   递归函数方式展开参数包

9.2   逗号表达式展开参数包

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

十.   lambda表达式

10.1   具体语法 

10.2   捕获列表说明

10.3   函数对象与lambda表达式

十一.   包装器

11.1   function包装器

11.2   包装器对解题的帮助

 11.3   bind


一.   C++11的大优势

相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中 约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,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中,对这一个规则进行了更进一步的规定,扩大了它的使用范围,使其可以为所有内置类型或者自定义类型初始化值,使用初始化列表时,可添加等号(=),也可不添加。

2.1   内置类型初始化

{}可以初始化内置类型变量。

int main()
{
	int a = 0;
	int b = { 1 };
	int c{ 1 };//建议使用上一行的初始化方式

    int arr1[]={1,2,3,4,5};
	int arr2[]{ 1,2,3,4,5 };

    vector<int> v{1,2,3,4,5};
}

2.2   自定义类型初始化

{}更重要的是对自定义类型的初始化。

(1)单参数构造

struct Point
{
	int _x;
	int _y;
};

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()
{
	Point p{ 1,2 };

	//多参数
	//C++ 构造
	Date d1(2021, 10, 1);

	//C++11
	//构造+拷贝构造-》优化:直接构造
	//多参数的隐式类型转换
	Date d2 = { 2024,10,12 };
	Date d3{ 2021,10,1 };

	//单参数
	//构造
	string s1("1111");
	//构造+拷贝构造-》优化:直接构造
	string s2 = "1111";

	//Date d4 = (2021, 10, 21);  不支持


	Date* d7 = new Date(2021, 10, 1);
	Date* d8 = new Date{ 2012,12,1 };
	return 0;
}

(2)多参数构造

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* d5 = new Date[3]{ d1,d2,d3 };
	Date* d6 = new Date[3]{ {2021,10,1},{2021,10,2},{2021,10,3} };
    //C++11为标准的容器都添加了initializer_list构造参数
    vector<Date> vd = { { 2022, 1, 17 }, Date{ 2022, 1, 17 }, { 2022, 1, 17 } };
    return 0;
}

为什么我们能用{}初始化,是因为有着initializer_list这位大哥存在。 

initializer_list是C++11新增的一个模板参数。其介绍为:

在容器中的应用:

三.   变量类型推导

3.1   为什么需要类型推导

我们平时在定义变量的时候,必须先知道他的变量类型才能定义,但是有时候我们并不知道其具体的类型,或者变量的类型太复杂了,这时就需要类型推导。

int main()
{
    //发生函数嵌套引用的时候,如果每个函数里面代码太多,此时并不能直接知道函数返回类型

    // 使用迭代器遍历容器, 迭代器类型太繁琐
	std::map<std::string, std::string>::iterator it = m.begin();
	while (it != m.end())
	{
		cout << it->first << " " << it->second << endl;
		++it;
	}
    return 0;
}

3.2   auto

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

int main()
{
	map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };
    //此时用auto就节省了很多
	auto it = dict.begin();
    while (it != m.end())
	{
		cout << it->first << " " << it->second << endl;
		++it;
	}
	return 0;
}

注意:不能将auto作为函数返回值,不然会导致混淆。 

3.3   decltype

decltype的出现是为了补齐auto 不支持对于表达式的类型推导的缺陷的, 经常适用于后置返回类型的推导. 

int main()
{
	const int x = 1;
	double y = 2.2;
	cout << typeid(x).name() << endl;
	cout << typeid(string).name() << endl;

	decltype(x) z = 1;
	cout << typeid(z).name() << endl;

	const int* p1 = &x;
	cout << typeid(p1).name() << endl;
	decltype(p1) p2 = nullptr;
	cout << typeid(p2).name() << endl;
	return 0;
}

注意:这里出现了一个叫typeid(变量名).name()的东西,这个也是显示一个变量的类型名,但是与decltype不一样的是,typeid(变量名).name()返回的是一个字符串。而decltype返回的是一个具体的类型,可以用于变量的定义。

 四.   STL中容器新变化

4.1   新增容器

上面圈起来的就是C++11出现的新容器,但是真正有用的是unordered_map和unordered_set两个容器,其他两个容器显得很鸡肋,这也算是C++11的一个败笔 。

4.2   新增方法

C++11中还新增了一些新方法,比如cbegin和cend返回迭代器的const版本。

但是这些都是一些锦上添花的操作,实际意义不大,因为begin和end本来就可以返回const版本。

五.   右值引用与移动语义(炒鸡重要)

5.1   左值引用与右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性。无论左值引用还是右值引用,都是给对象取别名。

5.1.1   左值与左值引用

所谓左值,就是一个表示数据的表达式。如:变量名和解引用的指针我们可以获取他的地址并且为他赋新值。左值可以出现在赋值符号的左边,不能出现在右边。对于const修饰的左值,不能对他赋新值。

那么所谓左值引用就是对左值进行引用,给左值起别名。

int& func2()
{
	static int x = 0;
	return x;
}
int main()
{
	//以下的p,b,c,*p,func2()返回值 都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

    //可以对左值取地址
	const int* ptr1 = &c;
	int* ptr2 = &func2();

    //对左值进行左值引用
    int*& rp=p;
    int& bb=b;
    const int& cc=c;
	return 0;
}

5.1.2   右值与右值引用

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

所谓右值引用就是对右值进行引用,对右值起别名。

int func1()
{
	static int x = 0;
	return x;
}

int main()
{
	double x = 1.1, y = 2.2;
	double& r1 = x;
	//以下都是常见的右值
	10;
	x + y;
	func1();

    //对右值进行右值引用
	int&& rr1 = 10;//给右值取别名后,会导致右值被存储到特定位置,可以取到该位置的地址
	double&& rr2 = x + y;
	int&& rr3 = func1();
	return 0;
}

5.1.3   左值引用与右值引用总结与比较

左值引用总结:

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

语法上,左值引用与右值引用都是起别名,不开空间。

底层,引用是存指针的。左值引用直接存储左值的地址,右值引用是把当前右值拷贝到栈上的一个临时空间,存的是这个临时空间的地址。

 

5.1.4   右值引用的意义

我们知道既然左值引用可以引用左值,又可以引用右值,那我们还要右值引用来干嘛呢?

我们来看看下面的示例:

namespace yjy
{
	class string
	{
	public:
		typedef char* iterator;
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

		// 拷贝赋值
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

		//...
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};


	string to_string(int value)//不能用左值引用,因为已经析构了
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		string str;
		//...

		std::reverse(str.begin(), str.end());
		return str;
	}
}

void fun1(yjy::string s)
{}
void fun2(yjy::string& s)
{}

int main()
{
	yjy::string s("1234");
	//fun1(s);
	//fun2(s);//左值引用提高了效率,不存在拷贝临时对象的问题

    //可以使用左值引用返回,这个对象还在
	s += 'a';
	//不能使用左值引用返回,这个就是左值引用的一个短板
	//函数返回对象出了作用域就不在了,就不能用左值引用返回(因为返回的是本身地址,栈帧已销毁)
	yjy::string ret = yjy::to_string(1234);
	return 0;
}

 那么右值引用就可以弥补这个短板。这就需要移动语义

 5.2   移动语义

上面的to_string函数在返回str这个右值时,会创建一个临时变量,这时出了作用域str局部变量就销毁了。C++11称str这种变量为将亡值(还是右值),我们对将亡值传值拷贝返回定义了专门的移动构造,对将亡值的赋值定义了专门的移动赋值。我们的移动语义就包括移动构造和移动赋值。

5.2.1   移动拷贝 

在str拷贝到临时对象的时候,会调用拷贝构造,将str内的资源转移到临时对象中。再将临时对象拷贝到ret变量的时候,会调用移动构造,将临时对象的资源转移到ret中,发生两次拷贝(这里某些先进版本的编译器会优化此过程,不会产生临时对象,直接将str内的资源转移到ret中,只有一次移动构造)。

C++11之前拷贝构造:

 C++11移动构造:

5.2.2   移动赋值 

上面是进行构造,如果我们是进行赋值呢?

yjy::string ret;
ret = yjy::to_string(1234);//赋值重载多了一次拷贝构造

同样的我们也为将亡值引进了移动赋值。

在str拷贝到临时对象的时候,会调用拷贝构造,将str内的资源转移到临时对象中。再将临时对象拷贝到ret变量的时候,会调用移动赋值,将临时对象的资源转移到ret中,发生一次拷贝加一次赋值(这里不会进行优化)。

C++11之前拷贝构造+拷贝赋值:

C++11拷贝构造+移动赋值:

 

 要注意的是:

  • 浅拷贝的类不需要移动构造,深拷贝的类才需要移动构造。
  • C++11提供右值引用,本质是为了参数匹配时区分左值和右值。
  • C++11之后,所有容器都增加了移动拷贝和移动赋值。

以上就是右值引用的一个使用场景。

还有一种使用场景是在容器的插入操作的时候引入右值引用实参,则可以转移他的资源,减少拷贝。

int main()
{	yjy::list<yjy::string> lt;
	yjy::string s1("11111");

	lt.push_back(s1);
	cout << "----------" << endl;

    //以下插入的都是右值,使用移动构造
	lt.push_back(yjy::string("2222"));
	cout << "----------" << endl;

	lt.push_back("2222");
	cout << "----------" << endl;
	return 0;
}

 5.2.3   右值被右值引用之后,其属性为左值

这是特别需要注意的一点!!!

我们先来看一个规则:有现成的吃现成的。

void func(const int& x)
{
	cout << "void func(const int& x)" << endl;
}

void func(int&& x)
{
	cout << "void func(int&& x)" << endl;
}

int main()
{
	const int x = 0;
	func(x);//左值
	func(10);//右值

	return 0;
}

 

可以看见是先去找有没有现成的,有现成的就用现成的。

为了证明“右值被右值引用之后,其属性为左值”这一规则,我们用之前的list容器来演示。

namespace yjy
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;

		ListNode(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}

		ListNode(T&& x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
    };

    template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
        typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

        void push_back(const T& x)
		{
			insert(end(), x);
		}

		void push_back(T&& x)
		{
			insert(end(), x);
		}
        
        iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			//return iterator(newnode);
			return newnode;
		}

		iterator insert(iterator pos, T&& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			//return iterator(newnode);
			return newnode;
		}

        private:
		Node* _head;
	};
}

int main()
{
	yjy::list<yjy::string> lt;
	yjy::string s1("11111");

	lt.push_back(s1);
	cout << "----------" << endl;

	lt.push_back(yjy::string("2222"));
	cout << "----------" << endl;

	lt.push_back("2222");
	cout << "----------" << endl;
	return 0;
}

我们对左值引用与右值引用不同的时候定义了不同的代码。

运行结果为:

是不是懵逼了,按理来说应该是移动构造才对,怎么是深拷贝呢?

就是因为我们在push_back的右值引用版本之后,我们的属性已经变成了左值,所以以后一直照着左值的版本运行。我们需要在每次右值引用之后,手动move一下,让其属性继续保持右值

namespace yjy
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;

		ListNode(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}

		ListNode(T&& x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(x))//手动move,让其属性保持右值
		{}
    };

    template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
        typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

        void push_back(const T& x)
		{
			insert(end(), x);
		}

		void push_back(T&& x)
		{
			insert(end(), move(x));//手动move,让其属性保持右值
		}
        
        iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			//return iterator(newnode);
			return newnode;
		}

		iterator insert(iterator pos, T&& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(move(x));//手动move,让其属性保持右值

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			//return iterator(newnode);
			return newnode;
		}

        private:
		Node* _head;
	};
}

int main()
{
	yjy::list<yjy::string> lt;
	yjy::string s1("11111");

	lt.push_back(s1);
	cout << "----------" << endl;

	lt.push_back(yjy::string("2222"));
	cout << "----------" << endl;

	lt.push_back("2222");
	cout << "----------" << endl;
	return 0;
}

运行结果为:

这样是不是就对啦!

所以右值被右值引用之后,其属性为左值,要让其属性继续保持右值,必须在每个右值引用的地方手动move一下。

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

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

int main()
{
 yjy::string s1("hello world");
 // 这里s1是左值,调用的是拷贝构造

 yjy::string s2(s1);
 // 这里我们把s1 move处理以后, 会被当成右值,调用移动构造

 // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的

 // 资源被转移给了s3,s1被置空了。

 yjy::string s3(move(s1));
 return 0;
}

所以不要轻易的将左值move为右值,除非你准备让这个左值资源被拿走。

STL也增加了右值引用版本:

 六.   完美换发

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

void Func(int x)
{
	cout << x << endl;
}
template<typename T>
void Perfect_Forward(T&& t)
{
	Func(t);
}

完美转发的核心概念在于,如果Perfect_Forward() 函数接收到的参数t为左值,那么该函数传递给 Func() 的参数 t 也应该是左值;反之,如果 Perfect_Forward() 函数接收到的参数 t 为右值,那么传递给 Func() 函数的参数 t 也必须是右值。这样参数的原始类型就得到了保留

 我们再来了解一下万能引用

1、模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值

2、模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。

3、但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,

4、我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

 C++11我们用forward函数来实现万能引用:

//void fun(int& x) { cout << "void fun(int& x)  左值" << endl; }
//void fun(int&& x) { cout << "void fun(int&& x)  右值" << endl; }
//void fun(const int& x) { cout << "void fun(const int& x)  const左值" << endl; }
//void fun(const int&& x) { cout << "void fun(const int&& x)  const右值" << endl; }

//万能引用
template<typename T>
void PerfectForward(T&& t)
{
	//forward
	// forward 保持属性->本身是左值,就不变;
	// 本身是右值,右值引用后,属性是左值,转成右值,相当于move一下
	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;
}

 右值引用右值之后,会导致属性变为左值,使用完美转发之后可以保持其属性。

 七.   新的类功能

默认成员函数:

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

1. 构造函数

2. 析构函数

3. 拷贝构造函数

4. 拷贝赋值重载

5. 取地址重载

6. const 取地址重载

重要的是前面四个函数,后面的两个并不是很重要。 

C++11之后,新增了两个成员函数,移动构造和移动赋值函数

 针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

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

 八.   关键词delete

在C++11之前,我们要让一个类不能被拷贝,除了只定义不实现拷贝构造函数之外,还要私有化拷贝构造函数。因为如果我们只定义不实现,还可以通过类外实现的方式使其能被拷贝。所以还要使拷贝构造函数私有化。

但是C++11添加了关键词delete,作用是:禁止生成默认构造函数。称被delete修饰的函数为删除函数

//不希望A对象被拷贝
class A
{
public:
	A() = default;
private:
	//C++98
	//只声明不实现
	//放到私有
	A(const A& a);
	int a = 0;
};
int main()
{
	A aa1;
	//A aa2 = aa1;
	return 0;
}

//C++11
class A
{
public:
	A() = default;
	A(const A& a)=delete;
private:
	int a = 0;
};
int main()
{
	A aa1;
	//A aa2 = aa1;
	return 0;
}

九.    可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板。

下面是一个基本的可变参数的函数模板:

// Args是一个模板参数包,args是一个函数形参参数包

// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。

template <class ...Args>

void ShowList(Args... args)
{}

 上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数 包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。

9.1   递归函数方式展开参数包

编译时递归返回条件
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...);
}

//实例化以后,推演生成的过程
//void Showlist(int val1, char ch, std::string s)
//{
//	_Showlist(val1, ch, s);
//}

//void _Showlist(const int& val, char ch, std::string s)
//{
//	cout << val << " ";
//	_Showlist(ch, s);
//}

//void _Showlist(const char& val, std::string s)
//{
//	cout << val << " ";
//	_Showlist(s);
//}

//void _Showlist(const std::string& val)
//{
//	cout << val << " ";
//	_Showlist();
//}

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

需要说明的是,递归 展开参数包并不是可以一直递归下去。

template<size_t N>
void func()
{
	cout << N << " ";
	func<N - 1>();
}

template<>
void func<1>()
{
	cout << 1 << endl;
}
int main()
{
	func<2000>();
	return 0;
}

 以VS2022为例,差不多递归2000次就到极限了。

9.2   逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg

不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。

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

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

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

 emplace系列,直接给插入对象参数的时候:

  • 深拷贝的类对象,减少一次移动构造
  • 浅拷贝的类对象,减少一次拷贝构造

十.   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& g1, const Goods& g2)
	{
		return g1._price < g2._price;
	}
};

struct ComparePriceGreater
{
	bool operator()(const Goods& g1, const Goods& g2)
	{
		return g1._price > g2._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());
	return 0;
}

但是这样太复杂了,所以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) ->bool {
		return g1._price < g2._price;
		});
	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) ->bool {
		return g1._evaluate < g2._evaluate;
		});
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) ->bool {
		return g1._evaluate > g2._evaluate;
		});
	return 0;
}

10.1   具体语法 

我们来看看lambda表达式的具体语法:

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

各部分说明:

  • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略。

  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

  • ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回 值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获 到的变量。

int main()
{
    []{} //最简单的lambda表达式,不能做任何事情
	//局部的匿名函数对象
	//auto add = [](int a, int b) ->int{return a + b; };
	/*auto add = [](int a, int b) {return a + b; };
	cout << add(1, 2) << endl;*/

	auto swap1 = [](int& a, int& b) ->void {
		int tmp = a;
		a = b;
		b = tmp;
	};
	int x = 1;
	int y = 2;
	swap1(x, y);

	auto func1 = [] {
		cout << "hello world" << endl;
	};
	func1();
	return 0;
}

要注意的是:必须用auto接受lambda表达式的返回值,因为表达式没有具体的类型,是在每次编译时才确定的。

 此处说明一下mutable的作用:

int x = 1, y = 2;
    //每次输入一个值跟x换
    //传值捕捉,捕捉到的是当前对象的拷贝
    auto swap1 = [x, y](){
        int tmp = x;
        cin >> x;
        y = tmp;
//此处是错的,因为不能改变x和y,这时就需要加上mutable
    }
    };

下面的代码就是对的: 

auto swap1 = [x, y]()mutable {
        int tmp = x;
        cin >> x;
        y = tmp;
    };

10.2   捕获列表说明

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

  • [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针
int main()
{
	int x = 1, y = 2;
	//每次输入一个值跟x换
	//传值捕捉,捕捉到的是当前对象的拷贝
	auto swap1 = [x, y](){
		int tmp = x;
		//cin >> x;
		//y = tmp;
	};

	auto swap1 = [x, y]()mutable {
		int tmp = x;
		cin >> x;
		y = tmp;
	};

	//传引用捕捉
	auto swap1 = [&x, &y]()mutable {
		int tmp = x;
		//cin >> x;
		x = y;
		y = tmp;
	};
	swap1();
	cout << x << endl;
	cout << y << endl;

	int m = 3, n = 4;
	//传值捕捉当前域的所有对象
	auto func1 = [=]() {
		return x + y * m + n;
	};

	cout << func1() << endl;

	//传引用捕捉当前域的所有对象
	auto func2 = [&]() {
		x++;
		m++;
		return x + y * m + n;
	};

	cout << func2() << endl;
	cout << x << endl;
	cout << m << endl;

    //语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
	//传引用捕捉当前域的所有对象,某些对象传值捕捉
	auto func3 = [&, n] {
		x++;
		m++;
		//n++; 不行
		return x + y * m - n;
	};

	cout << func3() << endl;
	cout << x << endl;
	cout << m << endl;
	return 0;
}

注意:

a. 父作用域指包含lambda函数的语句块。

b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。

比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量

[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。

比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

d. 在块作用域以外的lambda函数捕捉列表必须为空。

e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者 非局部变量都 会导致编译报错。

f. lambda表达式之间不能相互赋值,即使看起来类型相同。

10.3   函数对象与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()。

 所以这也验证了:lambda表达式实质上就是仿函数

十一.   包装器

11.1   function包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

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

通过上面的代码,我们发现useF函数实例化了三份。这样就很浪费资源,显得很赘余。

我们的function就能很好的解决这种问题。

std::function在头文件<functional>

// 类模板原型如下

template <class T> function;     // undefined

template <class Ret, class... Args>

class function<Ret(Args...)>;

//模板参数说明:

//Ret: 被调用函数的返回类型

//Args…:被调用函数的形参
int main()
{
	//函数指针
	function<double(double)> fc1 = f;
	fc1(11.11);
	cout << usef(fc1, 11.11) << endl;

	//函数对象
	function<double(double)> fc2 = Functor();
	fc2(11.11);
	cout << usef(fc2, 11.11) << endl;

	//lambda表达式
	function<double(double)> fc3 = [](double d)->double {
		return d / 4;
	};
	fc3(11.11);
	cout << usef(fc3, 11.11) << endl;
	return 0;
}

我们要注意的是特殊函数的情况:

int f(int a, int b)
{
	return a + b;
}

class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}

	double plusd(double a, double b)
	{
		return a + b;
	}
};

int main()
{
	//普通函数
	function<int(int, int)> fc1 = f;
	cout << fc1(1, 1) << endl;

	//静态成员函数
	function<int(int, int)> fc2 = Plus::plusi;
	cout << fc2(1, 1) << endl;

	//非静态成员函数
	//非静态成员函数需要对象的指针或者对象进行调用
	/*Plus plus;
	function<int(Plus*,double,double)> fc3 = &Plus::plusd;
	cout << fc3(&plus, 1, 1);*/

	function<double(Plus, double, double)> fc3 = &Plus::plusd;
	cout << fc3(Plus(), 1, 1) << endl;
	return 0;
}

11.2   包装器对解题的帮助

我们来看一道老题:​​​​​​​. - 力扣(LeetCode)

传统做法是利用栈。我们来看看function包装器是如何简化代码的:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        map<string,function<int(int,int)>> m={
            {"+",[](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;}}};
        stack<int> s;
        int number=0;
        for(auto& e:tokens)
        {
            if(m.count(e))//运算符
            {
                int right=s.top();
                s.pop();
                int left=s.top();
                s.pop();
                number=m[e](left,right);
                s.push(number);
            }
            else
            {
                s.push(stoi(e));
            }
        }
        number=s.top();
        return number;
    }
};

我们把四个操作函数用包装器包装起来作为map的value,这样就可以根据字符的情况,直接对map进行[],就可以得到对应的函数。

 11.3   bind

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

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

class plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}

	double plusd(double a, double b)
	{
		return a - b;
	}
};

int main()
{
	//调整参数顺序,了解一下,意义不大
	int x = 10;
	int y = 20;
	auto f1 = bind(sub, placeholders::_2, placeholders::_1);
	cout << f1(x, y) << endl;

	function<double(plus, double, double)> fc3 = &plus::plusd;
	cout << fc3(plus(), x, y) << endl;

	//调整参数的个数
	//某些参数绑死
	function<double(double, double)> fc4 = bind(&plus::plusd, plus(), placeholders::_1, placeholders::_2);
	cout << fc4(2, 3) << endl;

	function<double(double)> fc5 = bind(&plus::plusd, plus(), placeholders::_1, 20);
	cout << fc5(2) << endl;
	return 0;
}

总结

好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。

祝大家越来越好,不用关注我(疯狂暗示)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值