C++11

前言

这一篇博客我们讲C++11,C++11就是在原来的C++上面增加了一些内容,我们重点讲有用的

1. 统一的列表初始化

1.1 {}初始化

统一的列表初始化就是所有的东西都可以用大括号来初始化,不管是自定义类型还是内置类型

struct Point
{
	int _x;
	int _y;
};
int main()
{
	int arr[] = { 1,1,1,1,1 };
	int a = { 2 };
	int b { 2 };
	Point p1 = { 1,2 };
	Point p2  { 1,2 };
	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;
};
	Date b = { 2,2,2 };

这个大括号走的也是初始化,走的三个参数的初始化,三个值分别传给三个参数来初始化

1.2 std::initializer_list

	list<int> l = { 1,2,3,4,5,6,7,8 };
	map<string, string> m = { {"left","左"},{"right","右"},{"up","上"}};

std新增加的接口函数,模版可以用大括号来初始化,也就是initializer_list来初始化
list的每个成员是int,所以大括号里面就是int
map的每个成员是pair,是make_pair,所以大括号里面就是这个,然后make_pair可以由{}隐式类型转换而来

2. decltype

这个函数和typeid很类似,都是求出一个参数的类型,只不过typeid求出参数类型之后,返回一个字符串,而decltype求出的参数类型却可以直接使用

	int a = 0;
	decltype(a) d = 9;

比如这样,decltype(a)就是int的意思了
而且decltype里面的括号除了是变量,还可以是表达式等等

	decltype(&a) p =&a;
	decltype(a + d) c = 10;

decltype(&a)就是int*
decltype(a + d)就是int

3. 右值引用和移动语义

3.1 右值

一般来说右值就是不能取地址,然后不能修改的值
而左值就是能修改的(除了const类型的),能取地址的变量
所以重点就是看能不能取地址

int a = 0;
const int b = 0;
10;
a + b;
string("111");

看前两个就是左值
后面三个就是右值
一般来说,常量,临时变量,和匿名对象都是右值,这种一般没什么用,取不到地址,a+b是临时变量,因为会把a+b的值存在一个临时变量里面,所以a+b就是一个临时变量

3.2 右值引用

右值引用就是对右值的引用,右值一般发挥不了什么作用,对它右值引用就可以发挥作用了

	int&& x1 = 10;
	int&& x2= a + b;
	string&& x3= string("111");

两个&&就是右值引用

	int m = 0;
	int& pm = m;

这个就是左值引用,就是以前的引用
然后右值引用和左值引用都是一样的道理,右值引用指向的是那个临时对象在内存中的空间,语法上上看似没有开辟空间,没有浪费空间,实际上底层还是指针,还是开辟了空间的了

	string&& x3= string("111");
	x3 += "qq";
	cout << x3 << endl;

在这里插入图片描述
然后就是右值引用,虽然指向的是临时变量,是常量,是匿名对象,但是它指向的这个空间却是可以修改了,原本是不能修改的,右值引用后,也是相当于取别名嘛,这个别名就表示这个空间可以修改了,意思就是虽然x3的类型是右值引用的类型,但是功能却是左值引用的功能,或者说属性是左值引用的属性,就相当于它变成了左值引用

3.3 左值引用和右值引用的互相使用

	const int& p1 = 10;
	const int& p2 = a+b;
	const string& p2 = string("111");

对于右值,左值引用不能直接引用,但是加上const就可以了
当然,加上了const,肯定也不能修改了
在这里插入图片描述

	int a = 1;
	int&& p = move(a);
	p = 10;
	cout << p << endl;

在这里插入图片描述
对于左值,右值引用肯定也是不能直接引用的
但是加上move就可以了
当然这样还是可以修改的

	int&& p = (int&&)a;

move的作用就相当于把a强制类型转换为了int&&,当然move得底层肯定更为复杂,所以还可以像这样直接强制类型转换的写,都是一样的

3.4 左值引用的优化

namespace bit
{
	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);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			reserve(s._size);
			for (auto x : s)
			{
				push_back(x);
			}
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			reserve(s._size);
			for (auto x : s)
			{
				push_back(x);
			}
			return *this;
		}
		
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				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)
		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
	};
}
bit::string func()
{
	bit::string s("123456789");
	return s;
}

int main()
{
	bit::string ret = func();
	return 0;
}

我们先来分析一下这个
在这里插入图片描述
最原始的也就是没有优化的时候就是,现在func函数里面普通构造s,然后在return那里,会创建一个临时变量x来接收s,这里又有一个拷贝构造,然后x在赋值给ret,这就相当于在return那里,有两个拷贝构造,这是最初的
其实这个我们前面是讲过的
在这里插入图片描述
然后有一个优化就是(VS2022之前)return那里的两个拷贝构造就会优化为一个拷贝构造,就是直接把s拷贝给ret,在这里都还有一个普通构造和一个拷贝构造
然后最后一个优化就是,也就是我们现在VS2022能看到的优化就是编译器把ret直接拿进去,一个普通构造和一个拷贝构造就直接变为了一个普通构造,也就是s相当于ret的别名了
最后就变为了一个普通构造
在这里插入图片描述
上面的分析都是建立在没有写右值引用类型的拷贝构造的前提下的

3.5 右值引用的优化

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

我们增加一个函数,这个构造函数的参数是右值引用
因为s指向的内容是临时的,是没有声明用的,所以可以直接swap交换掉,反正也没用,交换一下也没事,因为s的属性是string&,所以可以交换修改的,所以对应的swap的参数也没有问题

bit::string func()
{
	bit::string s("123456789");
	return s;
}

int main()
{
	bit::string ret = func();
	return 0;
}

还是这个程序,最初的就是,一个普通构造,然后是一个拷贝构造给临时对象,临时对象在走移动构造,因为移动构造没有什么消耗,所以很赚
第二次优化就是return那里直接走移动构造
这里就很巧妙了,编译器会自动把要返回的s看为右值类型,然后就直接可以移动构造了,所以这个就比上面没写移动构造时效率高
这里就相当于编译器自动添加了move或者强制类型转换为了右值类型
第三次优化就是,直接只有一个普通构造,直接把s看为ret的别名
又因为stl里面的模版里面很多都有右值引用,所以再也不用担心函数返回类型为局部拷贝时影响效率的问题了

bit::string ret;
ret = func();

这个程序呢
我们先正常分析
bit::string ret;是一个普通构造
没有优化时
bit::string s(“123456789”);普通构造
return那里是先拷贝构造,在赋值拷贝
第一次优化就是return那里直接就赋值拷贝
然后就不会存在像刚刚那样ret在func里面构造的情形,因为已经构造出来了
所以总共就是普通构造,普通构造,赋值拷贝

// 赋值重载
string& operator=(const string& s)
{
	cout << "string& operator=(string s) -- 赋值拷贝" << endl;
	reserve(s._size);
	for (auto x : s)
	{
		push_back(x);
	}
	return *this;
}

在这里插入图片描述
如果我们写了移动赋值,就是这个函数

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

这种赋值被称为移动赋值,就和移动构造是一样的
还是这个程序,那么就会变为
没有优化时
就是先普通构造,在普通构造
return那里就是拷贝构造和移动赋值
优化为直接移动赋值,还是相当于把s看为了右值类型
效率肯定是提高了的
在这里插入图片描述

3.6 其它接口也具有右值类型的参数

除了拷贝构造,和赋值重载有右值类型的参数,其实其它函数也具有,比如说push_back,insert等等
在这里插入图片描述

	list<bit::string> l;
	l.push_back("11111111");

在这里插入图片描述
这里就会有两个构造。第一个是传参的时候,会生成临时变量,所以会有一个普通构造,这个是无法避免的
第二个移动构造就是在赋值的时候了

#include"list.h"

int main()
{
	bit::list<bit::string> l;
	l.push_back("1111");
	return 0;
}

在这里插入图片描述
现在我们引入我们以前写的list
就会发现除了拷贝还是拷贝,没有移动构造,现在我们来写移动构造list的push_back

// 在pos位置前插入值为val的节点
iterator insert(iterator pos, const T& val)
{
	Node* NewNode = new Node(val);
	Node* prev = pos._node->_prev;//这就是为什么要给node设置struct,还有迭代器也设置struct
	Node* next = pos._node;//next就是他本身
	NewNode->_prev = prev;
	NewNode->_next = next;
	next->_prev = NewNode;
	prev->_next = NewNode;
	return iterator(NewNode);//返回插入位置的迭代器,然后这个迭代器不会失效,但还是返回吧
}

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

以前是这样的,我们只需要在添加一个push_back函数的重载就可以了

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

还有insert也要写一个

iterator insert(iterator pos, T&& val)
{
	Node* NewNode = new Node(val);
	Node* prev = pos._node->_prev;//这就是为什么要给node设置struct,还有迭代器也设置struct
	Node* next = pos._node;//next就是他本身
	NewNode->_prev = prev;
	NewNode->_next = next;
	next->_prev = NewNode;
	prev->_next = NewNode;
	return iterator(NewNode);//返回插入位置的迭代器,然后这个迭代器不会失效,但还是返回吧
}

在这里插入图片描述
但是呢结果却不是我们想要的结果
这里还要解释一下,前面两个拷贝的原因是list的初始化就是要创建一个头结点,所以才产生的,这个是无法避免的

list()
{
	buynode();//this指针传进去,是相互联通的
}

然后就是第三个构造即普通构造,也是隐式类型转换时产生临时变量的时候产生的普通构造
对了对了还要注意,隐式类型转换时产生的临时变量是普通的构造,不是拷贝构造,普通构造的临时变量在用右值引用给参数

ListNode(const T& t=T())
	:_val(t)
	, _prev(nullptr)
	,_next(nullptr)
{}

然后最后一个构造就是节点在构造的时候用的,就是拷贝构造
为什么会这样呢
那是因为在push_back的时候
void push_back(T&& t)
虽然是这样,但是t的属性是T&的,所以会匹配到
iterator insert(iterator pos, const T& val)
而不是
iterator insert(iterator pos, T&& val)
要解决问题的话,就要在传参的时候添加上move

void push_back(T&& t)
{
	insert(end(), move(t));
}

就会走这个我们想要的insert

iterator insert(iterator pos, T&& val)
{
	Node* NewNode = new Node(move(val));
	Node* prev = pos._node->_prev;//这就是为什么要给node设置struct,还有迭代器也设置struct
	Node* next = pos._node;//next就是他本身
	NewNode->_prev = prev;
	NewNode->_next = next;
	next->_prev = NewNode;
	prev->_next = NewNode;
	return iterator(NewNode);
}

然后走new Node
这里面会走拷贝构造,是bit::string的
所以还要传move,免得走向T&的参数了
反正就是要一直传move
直到string的拷贝构造那里

ListNode(T&& t)
	:_val(move(t))
	, _prev(nullptr)
	, _next(nullptr)
{}

在这里插入图片描述
这样才有我们想要的效果

3.7 万能引用

template<class T>
void PerfectForward(T&&t)
{
	;
}

这里我们定义2一个函数模版,这个函数的形参为T&&t
看似为右值引用
但实际是万能引用
什么是万能引用呢
就是你传入左值,那么T&&t中就是T&t
你传入右值,那么就是T&&t,但是t的属性还是左值,但t是右值

	PerfectForward(1);
	int a = 0;
	PerfectForward(a);

这里PerfectForward(1);
那么t就是右值
PerfectForward(a);
这里t就是左值

最后一个要注意的一点就是像这种万能引用只有在函数模版才有,在其他场景的时候就是右值引用

iterator insert(iterator pos, T&& val)

所以说我们在类里面的这种函数里面的参数就不是万能引用
第一它不是函数模版,而是类模板的函数
第二就是在类的实例化时,这个类就已经实例化了
那么这个函数中的T就是特定的类型的,不是函数模板

3.7 完美转发

上面我们说了如何验证万能引用呢

void fund(int &a)
{
	cout << "void fund(int &a)" << endl;
}

void fund(int&& a)
{
	cout << "void fund(int &&a)" << endl;
}

template<class T>
void PerfectForward(T&&t)
{
	func(t);
}
PerfectForward(1);
int a = 0;
PerfectForward(a);

在这里插入图片描述

我们就传int类型来验证
但是这样是不行的
因为就算是右值类型的话,它的属性也是左值类型,所以会一直走左值的函数
这个时候我们就可以加入完美转发了

template<class T>
void PerfectForward(T&&t)
{
	func(forward<T>(t));
}

在这里插入图片描述
完美转发就是forward这个模板,使用就是上面的使用
使用了完美转发,那么如果是右值引用,那么属性还是右值引用
是左值引用,属性还是左值引用
所以有了万能引用和完美转发
那么push_back这个函数对于右值和左值就不用写两个了
写一个就可以了

void push_back(T&& t)
{
	insert(end(), move(t));
}

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

以前是这样的
现在可以这样

template<class T>
void push_back(T&& t)
{
	insert(end(), forward<T>(t));
}

在这里插入图片描述
但是库里面还是写的两个,这个原因主要是因为以前没有右值引用这些概念,写了左值引用的,也不好意思去掉,所以就这样了

4. 新的类功能

4.1 新增默认成员函数

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

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载(不重要)
  6. const 取地址重载(不重要)
    但是C++11过后,又加了两个
    新加的这两个默认成员函数就是移动构造函数和移动赋值

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

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

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

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

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

在这里插入图片描述
看这个程序就可以解释。因为没有拷贝构造,没有析构,没有赋值,而且我们没有实现移动构造
所以编译器就会自己构造一个默认的移动构造
对于内置类型直接拷贝,对于自定义就会调用它的移动构造,有的话
在这里插入图片描述
看吧,因为把s1move了,所以s1就看为了将亡值,然后就swap,然后s1就架空了

为什么要求没有拷贝构造,没有析构,没有赋值,要要求三个呢
其实呢,这三个是一体的
什么意思呢
就是会额外调用资源的时候,肯定就会自己实现,析构,拷贝构造,赋值
所以是三个一体
然后额外调用资源的时候,还要自己实现移动构造,移动赋值,可以看为五位一体
意思就是额外调用资源的时候,什么默认函数都要自己实现
不额外调用资源的时候,默认函数全由编译器实现都可以

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

	~Person()
	{}
private:
	bit::string _name;
	int _age;
};

在这里插入图片描述
如上图,我们用了析构函数,破坏了编译器默认生成移动构造的条件
所以就没有默认的移动构造了
但是有默认的拷贝构造,因为默认的拷贝构造的条件是我们只是没有显示实现

用了析构函数,编译器就没有生成默认的移动构造
但是我们可以让编译器去实现

	Person(Person&& s) = default;

这样就可以了
但是构造函数,赋值,移动赋值,如果要用的话也要有一个,因为它们是一体的,你把移动构造的默认函数都实现了,写了default就相当于在上面写了,你写了这个,因为五位一体,你要用其他的话,也要实现才行,或者也写default

4.2 delete

在C++98中,也就是以前的C++,如果要禁止使用某个默认成员函数,就要把它只声明不实现,然后设置为私有
在这里插入图片描述
但是在C++11中,可以把它设置为delete,这样就不可以使用了
在这里插入图片描述

5. 可变参数模板

所谓可变参数模版就是像printf那样可以无限输入参数

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

int main()
{
	ShowList();
	ShowList("222", 1.2, 34);
	return 0;
}

这个就是 可变参数模板,以前的函数模版是参数类型可以变,参数数量不能变,而现在是类型和数量都可以变
Args是模版参数包,args函数形参参数包
参数包支持0~无限个参数
这里的Args只是名字而已,也可以取其他名字

但其实底层还是编译器在搞,它帮忙实现的这些不同参数的函数

void ShowList()
{
	;
}

void ShowList(char*arr,double d,int i)
{
	;
}

其实底层就是编译器实现了这两个函数,这两个函数的实现是在编译的时候实现的,不是在运行的时候实现的,因为函数肯定是在编译的时候实现的啊

template<class...Args>
void ShowList(Args...args)
{
	cout << sizeof...(args) << endl;
}

在这里插入图片描述
sizeof…(args)这样写就可以得到参数包中有几个参数

但是如何打印变量呢?
如何像printf那样打印很多呢
有很多的方法,我们这里就只介绍两个方法

template<class T, class...Args>
void print(T t,Args...args)
{
	cout << t << " ";
	print(args...);
}

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

int main()
{
	ShowList();
	ShowList("222", 1.2, 34);
	return 0;
}

首先我们这样写肯定是不可以的
我们这个程序的思路就是,参数包往下传递的时候,会把第一个参数传给t,其余的就传给下一个参数包,说实话,这就是个编译时递归,通过这个过程编译器来构造函数

因为这个编译时递归没有终止条件,所以肯定是有问题的,问题就是最后参数包为0个参数的时候,无法传给t了,所以有问题

注意参数包要往下传递的话,还要传那三个点

template<class T, class...Args>
void print(T t,Args...args)
{
	cout << t << " ";
	if (sizeof...(args) == 0)
	{
		return;
	}
	print(args...);
}

但是这样写呢,也是有问题的,因为这是编译时递归,所以只会走要构造函数的步骤,意思是递归的时候就不会走sizeof这一步,因为这是运行时的逻辑,所以写了相较于编译时递归就和没写一样,还是有问题

void print()
{
	cout << endl;
}

template<class T, class...Args>
void print(T t, Args...args)
{
	cout << t << " ";
	print(args...);
}

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

int main()
{
	ShowList();
	ShowList(1,1,1,1);
	ShowList("222", 1.2, 34);
	return 0;
}

在这里插入图片描述
这才是正确的思路,增加一个0个参数的print函数就可以了

参数包作为函数参数传递下去的时候,后面要紧跟上三个点

下面介绍第二种方法

template<class T>
int print(T t)
{
	cout << t << " ";
	return 0;
}

template<class...Args>
void ShowList(Args...args)
{
	int arr[] = {print(args)...};
	cout << endl;
}

在这里插入图片描述
这就是第二种方法,当然这种方法不支持参数包为0的情况,自己手动添加一个无参print就可以了

写的这一步int arr[] = {print(args)…};
就不是和以前那样是把参数包传递下去了,如果是传递参数包下去,就是args紧挨着三个点,才是将参数包传递下去,这样写的话,就是将参数包的每个参数挨着挨着展开

int arr[] = { print("222"),print(1.2),print(34) };

就会变成这样,但是这样的前提就是print的返回值也是整型
那万一不是整型呢

template<class T>
void print(T t)
{
	cout << t << " ";
}

template<class...Args>
void ShowList(Args...args)
{
	int arr[] = {(print(args),0)...};
	cout << endl;
}

如果返回值不是整型的话,就可以使用逗号表达式,因为逗号表达式的值就是最后一个的值

int arr[] = { (print("222"),0),(print(1.2),0),(print(34),0) };

int arr[] = {(print(args),0)…};这句话就相当于是这样的

所以说,如果三个点没有紧跟args,而是在特定的表达式后面加上了三个点,就不是传参数包了,而是传的是每个参数,然后将这每个参数给展开

6. emplace

emplace就是插入的意思,list的emplace接口是emplace_back
意思就是尾插的意思
在这里插入图片描述
提前说一下emplace的效率是比push_back这种函数的效率高的

6.1 emplace的使用

emplace的参数是一个参数包,意思是可以传很多种类型的东西,然后还是万能引用,意思是左值和右值的传入有所区别

list<bit::string> l;
bit::string s1("11111");
l.emplace_back(s1);
l.emplace_back(bit::string("33333"));

在这里插入图片描述
这个就体现了万能引用,左值就用拷贝构造,右值就用移动构造
第三个构造其实就是匿名对象的构造

list<bit::string> l;
bit::string s1("11111");
l.emplace_back(s1);
l.emplace_back(move(s1));

在这里插入图片描述
直接将s1move就没有匿名对象的构造了,直接就是匿名对象了。直接就可以移动构造了

	list<bit::string> l;
	l.emplace_back("11111");

在这里插入图片描述
看这个我们就知道了,如果传的是string的左值或者右值,或多或少都要先普通构造,在拷贝构造或者移动构造,但是如果传的是,就是传的不是list的成员值,不是string类型的,就会用你传的那个值直接去函数里面去构造,但前提是你要有你传的参数类型的构造
比如我传的是字符串"11111",然后string恰好就有字符串的构造函数,所以就可以在list的emplace函数中直接构造string在list里面,就方便了很多

list<pair<bit::string,int>> l;
pair<bit::string, int > s("2222", 2);
l.emplace_back(s);
l.emplace_back(make_pair("1111", 1));

在这里插入图片描述
像这个就和刚刚的成员为string时类似的,第三个就是用make_pair直接去构造

	l.emplace_back("3333", 3);

在这里插入图片描述
还可以这样,因为pair支持这样的构造嘛,所以就可以直接用"3333", 3去list里面构造pair

list<bit::string> l;
l.emplace_back(3, 'k');

在这里插入图片描述
其实list的成员为string时还可以这样插入,为什么呢,因为我们实现了string的int,char类型的构造,这样就可以直接去里面构造了

	string(int num,char c)
	{
		cout << "string(char* str)普通构造" << endl;
		reserve(num);
		int i = 0;
		for (; i < num; i++)
		{
			_str[i] = c;
		}
		_str[i] = '\0';
	}

所以说,empalce有参数包,这个的意思并不是说可以插入多个值,插入的还是一个值,只不过你可以传各种,或者多个参数,只要对应的类型有这种构造函数就可以了

因为emplace可以直接去里面构造,所以说效率的话就要比push要高,所以以后所用这个

emplace的实现

template<class...Args>
void emplace_back(Args&&...args)
{

}

这个emplace_back肯定是要把参数包传递下去的,所以以前的push_back还不行,不能调用,要自己实现一个

template<class...Args>
void emplace_back(Args&&...args)
{
	insert(end(), args...);
}

这里肯定是要传递参数包,而不是展开参数包,所以紧跟三个点

template<class...Args>
iterator insert(iterator pos, Args&&...args)
{

	Node* NewNode = new Node(args...);
	Node* prev = pos._node->_prev;
	Node* next = pos._node;
	NewNode->_prev = prev;
	NewNode->_next = next;
	next->_prev = NewNode;
	prev->_next = NewNode;
	return iterator(NewNode);
}

new Node就会调用Node的构造函数
所以Node的构造函数也要写成参数包的形式

template<class...Args>
ListNode(Args...args)
	:_val(args...)
	, _prev(nullptr)
	, _next(nullptr)
{}

_val(args…)这里的话参数包为 什么形式就会调用所对应的的构造函数,因为这就是在调用_val的构造函数

bit::list<bit::string> l;
bit::string s1("11111");
l.emplace_back(s1);
l.emplace_back(move(s1));

在这里插入图片描述
但是这样还是不行,因为怎么全是构造函数,而不存在移动构造呢
这是因为右值引用的参数包传递时,属性还是会变为左值的
所以还要弄完美转发
在这里插入图片描述
不添加forward还好,直接在后面加上三个点就可以了,因为这样既是传参数包也是可以对参数包展开
但是加上了forward就麻烦了,因为到底是对参数包forward,还是对展开的forward呢,这里我们是对参数包展开forward,因为每个参数都需要保持原来的属性

insert(end(), forward<Args>(args)...);

所以要这样写
就等价于

		insert(end(), forward<Args>s1, forward<Args>s2);

反正就是要分别对每个参数forward
就要这样写

template<class...Args>
void emplace_back(Args&&...args)
{
	insert(end(), forward<Args>(args)...);
}

所以参数包的传递每个都这样写,就可以保证右值属性不变了
在这里插入图片描述

7. lambda表达式

7.1 潜在问题

struct Goods
{
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		,_price(price)
		, _evaluate(evaluate)
	{}
	string _name; //名字
	double _price; // 价格
	int _evaluate; // 评价
};
struct CmpPriceLess
{
	bool operator()(Goods g1, Goods g2)
	{
		return g1._price < g2._price;
	}
};
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), CmpPriceLess());

在这里插入图片描述

以前我们排序的话,如果是不能直接排序的类型,排序我们就要自己写仿函数,还有没有更简单的方法呢,有是有的

7.2 lambda表达式

在这里插入图片描述
这个就是lambda的语法形式
[capture-list] (parameters) mutable -> return-type { statement}
其中[capture-list] 和mutable 我们暂时先不管
(parameters) 这个是参数类型,和你定义函数时写的形参格式是一样的
-> return-type指的是函数返回值类型
{ statement}就是函数体

auto func1 = [](int a, int b)->int {return a + b; };
cout << func1(1, 34) << endl;

在这里插入图片描述
这就是lambda的写法
虽然[capture-list] 和mutable 我们暂时先不管,但是[]还是要写上
然后就是[](int a, int b)->int {return a + b; }
返回的是一个匿名函数的对象,这个对象是可以接受的,只不过我们不知道是什么类型,所以用auto来接收
这样的话,func1就可以像仿函数,可以像函数那样正常使用了

auto func1 = [](int a, int b)->int {
	return a + b;
};

如果函数体太大的话,还可以这样写,这样写就好看点了

(parameters)这个如果不用传参的话,就可以连着()省略不写

然后就是->returntype,如果没有返回值可以不写,如果返回值可以编译器自己推出,也可以不写

auto func2 = [] {
	cout << "hahahhha" << endl;
	return 0;
	};
cout<<func2()<<endl;

在这里插入图片描述
上面这个例子就省略了形参和返回值类型
但平时还是鼓励写上这两个东西,因为方便理解

vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](Goods g1, Goods g2)->bool
	{
		return g1._price < g2._price;
	});

在这里插入图片描述
上面的问题就可以很简单的解决了,这样就可以不用传仿函数了,因为lambda是匿名的函数对象,所以没有问题

注意的一点就是,有lambda的调试的时候,断点不要打在lambda函数内部,不然会出现一直在里面的情况

7.3 [capture-list] 和mutable

[capture-list]是捕捉列表
何谓捕捉列表呢,就是你传入了数据,就可以捕捉它,就可以使用它

int a = 10;
int b = 8;
auto swap1 = [a, b](){
	cout << a << "   " << b << endl;
};
swap1();

在这里插入图片描述
像这样,我们在捕捉列表写上了a和b
那么我们就可以使用a和b了
这就是捕捉列表的作用
我们再写个交换a和b的lambda
在这里插入图片描述
看上面我们这样写是不行的,因为捕捉列表中这样写,a和b是默认const类型的,不能修改值,

auto swap1 = [a, b]()mutable {
	int tmp = a;
	a = b;
	b = tmp;
	};
swap1();

但是加上mutable就可以了,没错,mutable的作用就是取消掉const,所以mutable并没有什么用

int a = 10;
int b = 8;
auto swap1 = [a, b]()mutable {
	int tmp = a;
	a = b;
	b = tmp;
	cout << a << " " << b << endl;
	};
swap1();
cout << a << " " << b << endl;

这里额外说明一下,捕捉的东西除了当前作用域的,还可以捕捉全局的,除此就不能捕捉其他的了
在这里插入图片描述
看这个结果,我们虽然实现swap,但是好像并没有swap,哪里出问题了呢
其实这里的捕捉列表中的a和b,只是外面的临时拷贝罢了,就是相当于形参的临时拷贝,所以它里面的修改,并不会影响外面的值

所以这个捕捉的swap就相当于这样写的

auto swap1 = [](int a, int b)mutable {
	int tmp = a;
	a = b;
	b = tmp;
	cout << a << " " << b << endl;
	};
swap1(a,b);

所以里面的修改并不会影响外面的值

但是传引用就可以修改外面的值了

int a = 10;
int b = 8;
auto swap1 = [&a, &b]()mutable {
	int tmp = a;
	a = b;
	b = tmp;
	cout << a << " " << b << endl;
	};
swap1();
cout << a << " " << b << endl;

在这里插入图片描述
这里的引用就要写在a和b的前面

而这里的引用不是默认为const的,只有普通的引用才是const
所以说mutable没什么用呢。不用写呢
因为只有普通的捕捉才是const,而且如果你要修改的话,为什么不传引用

除了上面两种捕捉方式,还有其他的捕捉方式

int a = 10;
int b = 8;
auto swap1 = [=]()mutable {
	int tmp = a;
	a = b;
	b = tmp;
	cout << a << " " << b << endl;
	};
swap1();
cout << a << " " << b << endl;

在这里插入图片描述

这个=就是对可以捕捉的所有变量(当前局部域,全局域,this)全部进行普通的捕捉

int a = 10;
int b = 8;
auto swap1 = [&]()mutable {
	int tmp = a;
	a = b;
	b = tmp;
	cout << a << " " << b << endl;
	};
swap1();
cout << a << " " << b << endl;

这个就是全部进行引用捕捉了

在这里插入图片描述
除了单一的引用捕捉和普通捕捉,还可以混合起来,比如这样就是除了对c普通捕捉,对其他的都是引用捕捉
在这里插入图片描述
这样就是除了对c引用捕捉,对其他的都普通捕捉
在这里插入图片描述
但是不能这样写,因为重复了,&就已经包括了&c

7.4 lambda底层

auto func1 = [](int a, int b)->int {
	return a + b;
};

lambda底层就是生成了一个类,拿这个举例,类的成员变量就是a和b,然后类还生成了一个仿函数,仿函数的实现逻辑就是函数体的内容
然后这个类的名字是很乱的,是独一无二的,就和身份证一样,这就是为什么我们不知道auto的类型是什么的原因

func1(1,3);

所以这里底层就是调用了仿函数

auto func1 = [](int a, int b)->int {
	return a + b;
};

auto func2 = [](int a, int b)->int {
	return a + b;
	};

所以说这里呢,func1和func2就是不同的类型,因为func1的类型就是那个类,是独一无二的,func2也是独一无二的,所以是不同的类型,所以更不能相互赋值

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的使用完全一样,这也可以一定程度上解释为什么lambda底层是仿函数

8. 包装器

所谓包装器也是适配器
适配器有两个,一个function包装器,一个是bind

8.1 function

我们直接看function的使用
首先function在头文件functional

#include<functional>
using namespace std;
int f(int a, int b)
{
	return a + b;
}

struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};

int main()
{
	function<int(int, int)> t1 = f;
	function<int(int, int)> t2 = Functor();
	cout << t1(1, 2) << endl;
	cout << t2(1, 2) << endl;
	return 0;
}

在这里插入图片描述
这就是function的使用,function接受的东西是一个使用()就可以调用对应函数的东西,比如函数指针,和仿函数,后面加上括号和参数,就可以使用对应函数的内容了,然后就可以把这个加()加参数就可以使用函数内容的特性就交给function指向的内容
function是一个模版,只能接收一个
然后就是这个模版的类型括号外面的int是指的函数的返回值
括号里面的int就是函数的参数类型
这样做的好处就是可以用function作为类型,然后去使用对应函数
以前要使用的话,要写明类型,要写函数指针那些,比较麻烦,这样用function的话,就不用写那些东西了

function就是包装的可调用对象
这个东西就是可以使用括号的那种

function<int(int, int)> t1 = f;
function<int(int, int)> t2 = Functor();
function<int(int, int)> t3 = [](int a, int b)->int {return a + b; };
cout << t1(1, 2) << endl;
cout << t2(1, 2) << endl;
cout << t3(1, 2) << endl;

在这里插入图片描述
这样写也可以,因为匿名函数对象也是可以调用括号的

下面我们来讲一下包装类里面的函数

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

	double plusd(double a, double b)
	{
		return a + b;
	}
};
function<int(int, int)> f1 = Plus::plusi;
cout << f1(1, 2) << endl;

在这里插入图片描述
静态的成员函数就和我们包装普通的函数是一样的
在这里插入图片描述
但是对于非静态的成员函数我们就不能这样写了

第一是因为非静态的成员函数的函数的地址不是函数名,如果要取函数地址的话,就还要加上&
在这里插入图片描述
加上都还不行
因为它还有一个隐藏的this这个参数
而静态的函数就没有this指针

this指针的类型是什么呢,那就是Plus*类型的

function<double(Plus*, double, double)> f2 = &Plus::plusd;
Plus p;
cout << f2(&p,2, 3) << endl;

在这里插入图片描述
function的定义是写好了,但是它的使用还是要传一个this指针,但这个时候就要传一个对象去了

function<double(Plus, double, double)> f2 = &Plus::plusd;
Plus p;
cout << f2(p,2, 3) << endl;

在这里插入图片描述
但是还可以这样写
为什么呢,this指针的类型不是Plus啊?
这个我们就要回到底层了
底层就是通过p对象来调用对应函数,底层还是会建立一个仿函数,根据p的那个函数来建立,所以这样写是可以实现逻辑的,所以可以这样写

8.2 练习:逆波兰表达式求值

在这里插入图片描述
在这里插入图片描述

class Solution {
public:
int evalRPN(vector<string>& tokens) {
 stack<int> st;
    for(auto& str : tokens)
    {
        if(str == "+" || str == "-" || str == "*" || str == "/")
        {
            int right = st.top();
            st.pop();
            int left = st.top();
            st.pop();
            switch(str[0])
         {
                case '+':
                    st.push(left+right);
                    break;
                case '-':
                    st.push(left-right);
                    break;
                case '*':
                    st.push(left*right);
                    break;
                case '/':
                    st.push(left/right);
                    break;
            }
        }
        else
        {
            // 1、atoi  itoa
            // 2、sprintf scanf
            // 3、stoi  to_string  C++11
            st.push(stoi(str));
        }
    }
   return st.top();
}
};

这个是解答,然后我们融入function

class Solution {
public:
int evalRPN(vector<string>& tokens) {
 stack<int> st;
 map<string,function<int(int,int)>> cmd={
    {"+",[](int a,int b)->int{return a+b;}},
    {"-",[](int a,int b)->int{return a-b;}},
    {"*",[](int a,int b)->int{return a*b;}},
    {"/",[](int a,int b)->int{return a/b;}}
 };
    for(auto& str : tokens)
    {
        if(str == "+" || str == "-" || str == "*" || str == "/")
        {
            int right = st.top();
            st.pop();
            int left = st.top();
            st.pop();
            st.push(cmd[str](left,right));
        }
        else
        {
            // 1、atoi  itoa
            // 2、sprintf scanf
            // 3、stoi  to_string  C++11
            st.push(stoi(str));
        }
    }
   return st.top();
}
};

这个就是根据某个指示就进入某个函数,这个就比原来我们写函数指针来进入方便多了

8.3 bind

在这里插入图片描述
在这里插入图片描述
我们直接看bind怎么使用的
bind还是一个包装器,还是像function差不多的,包装以后使用就和function差不多了

int Plus(int a, int b)
{
	return a - b;
}
auto f1 = bind(Plus, placeholders::_1, placeholders::_2);
cout << f1(10, 3) << endl;

在这里插入图片描述
bind的第一个参数就是那种可调用对象,可以使用括号的东西
然后placeholders::_1就表示f1调用的时候的第一个参数,就是10
就是10传给placeholders::_1,然后placeholders::_1肯定是传给int a的
placeholders::_2就表示f1调用的时候的第2个参数

然后就是placeholders头文件是functional

在这里插入图片描述

placeholders又是functional的std命名空间里的一个命名空间,所以为了不写placeholders,可以展开命名空间

using namespace placeholders;

可以这样写

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

还可以这样写,特定展开几个

auto f1 = bind(Plus, _2, _1);
cout << f1(10, 3) << endl;

在这里插入图片描述
这样写的话,_2对应的就是实参中的第二个参数,就是3,然后传给a
_1对应的就是实参中的第1个参数,就是10,然后传给b
所以是-7
所以说bind对应实参是看_几对应的谁,对应形参就是挨着挨着对应的

其实bind的底层还是一个仿函数,只不过实现就很复杂了
看上面这个程序我们就知道了,bind可以调整参数顺序,其实还可以调整参数个数,调整的是实参的个数,而且只能减少,不能增加

还有就是注意的就是,要先有_1才有_2

auto f1 = bind(Plus, 100, _1);
cout << f1(10, 3,2) << endl;

bind的参数和形参是一一对应的,所以100对应的a,_1是第一个实参对应的是b,第一个实参就是10,所以后面写的3和2就没有什么用
在这里插入图片描述

auto f1 = bind(Plus, 100, _1);
cout << f1(10) << endl;

就是这个程序的效果

所以bind就有绑定一些固定参数的效果

int SubX(int a, int b, int c)
{
	return (a - b - c) * 10;
}
auto f1 = bind(SubX,_1, 100, _2);
cout << f1(10,2) << endl;

在这里插入图片描述
这个就绑定了形参b,b就一直是100了
以前的function调用类里面的函数,我们还要一直传入this指针好麻烦
但是现在我们就可以使用bind来绑定this指针
在这里插入图片描述
这样写还不行,因为右值是无法取地址的,所以不能这样绑定,或者自己写一个对象

	Plus p;
	auto f1 = bind(&Plus::plusd, &p, _1, _2);
	cout << f1(1.1, 1.1) << endl;

但是我们可以不传地址绑定啊,可以传对象的,可以传匿名对象

auto f1 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f1(1.1, 1.1) << endl;

在这里插入图片描述
因为我们不知道bind返回的是什么类型的,所以写auto
但是function就可以解决这个问题,function只需要写上返回值类型,参数类型,就可以接收了,也相当于bind的类型了

function<double(double, double)>  f1 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f1(1.1, 1.1) << endl;

在这里插入图片描述
所以有了bind,包装类里面的函数,就不用传this指针了

bind主要是用来绑定一些参数,function就是封装一些可调用对象,也相当于写了一些不好写的函数类型

总结

C++11还有一些内容,比如线程库,但这个要等到学了Linux才会写
下一节我们学异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值