【C++从0到王者】C++11(全文三万字,超详解)

文章目录

  • 一、 统一的初始化列表
    • 1.{}列表初始化
    • 2.initializer_list
  • 二、声明
    • 1.auto
    • 2.decltype
    • 3.nullptr
  • 三、范围for
  • 四、智能指针
  • 五、STL中的一些变化
    • 1.新容器
    • 2.新接口
  • 六、右值引用和移动语义
    • 1.左值引用和右值引用
    • 2.右值引用的使用场景和意义
    • 3.左值引用和右值引用的价值和场景
    • 4.完美转发
  • 七、lambda表达式
    • 1.对类数组排序的一个例子
    • 2.lambda表达式语法
    • 3.函数对象与lambda表达式
  • 八、可变参数模板
    • 1.可变参数模板
    • 2.emplace系列
  • 九、新的类功能
    • 1.新增的默认成员函数
    • 2.一些新的关键字
  • 十、包装类
    • 1.function包装器
    • 2.bind

一、 统一的初始化列表

1.{}列表初始化

在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
{
	Point(int x,int y)
		:_x(x)
		,_y(y)
	{
		cout << "Ponint(int x, int y)" << endl;
	}

	int _x;
	int _y;
};

int main()
{
	int x = 1;
	int y = { 2 };
	int z{ 3 };

	int a1[] = { 1,2,3 };
	int a2[]{ 1,2,3 };

	Point p0(1, 2);
	Point p1 = { 1, 2 };
	Point p2{ 1,2 };
	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第1张图片

而且使用这样的{}会去调用构造函数

同时这样的话就可以应用于new中

	int* ptr1 = new int[3] {1, 2, 3};
	Point* ptr2 = new Point[2]{p0,p1};
	Point* ptr3 = new Point[2]{ {1,1},{0,0} };

【C++从0到王者】C++11(全文三万字,超详解)_第2张图片

当然建议日常定义的时候,还是不要去掉=,因为可能显得比较奇怪,降低了一点可读性。

其实上面这些用{}的本质是一个多参数的隐式类型转换,因为之前string中的单参数的隐式类型转换

string s = "xxxxx";

如果我们在类的构造函数前加一个explicit关键字,那么就无法使用{}这样进行初始化了,因为explicit关键字可以防止隐式类型转换

【C++从0到王者】C++11(全文三万字,超详解)_第3张图片

再比如,我们可以使用引用进行验证,如果没有explicit关键字,这个引用还可以编译通过

【C++从0到王者】C++11(全文三万字,超详解)_第4张图片

这里我们还必须要加const,因为隐式类型转换要产生一个临时对象,这个临时对象具有常性

2.initializer_list

我们先来看一下下面两段代码是同一个语法吗?

struct Point
{
	Point(int x, int y)
		:_x(x)
		,_y(y)
	{
		cout << "Ponint(int x, int y)" << endl;
	}

	int _x;
	int _y;
};

int main()
{
	vector<int> v = { 1,2,3,4,5,6,7 };

	Point p = { 1,2 };
	return 0;
}

其实不是的,对于vector,它后面的花括号参数是可以改变的,而对于Point,它后面的花括号参数是不可以改变的。

所以说,这两个其实是利用不同的规则进行初始化的。

第二个我们好理解,就是多参数的隐式类型转换。

那么第一个是咋回事呢?其实C++11做了这样一件事。它新增了一个类型,叫做initializer_list

【C++从0到王者】C++11(全文三万字,超详解)_第5张图片

【C++从0到王者】C++11(全文三万字,超详解)_第6张图片

它只有三个成员函数,也就是迭代器和size

【C++从0到王者】C++11(全文三万字,超详解)_第7张图片

那么initializer_list是如何实现的呢?

其实我们可以认为它的底层是这样实现的

template<class T>
class initializer_list
{
private:
	const T* _start;
    const T* _finish;
}

然后我们赋值时候所给的数组其实是存储在常量区的,当我们赋值的时候,这两个指针其实一个指向常量区的开始,一个指向常量区的结尾

【C++从0到王者】C++11(全文三万字,超详解)_第8张图片

所以当我们打印这个类型的大小的时候,我们会发现,在32位下是8字节

【C++从0到王者】C++11(全文三万字,超详解)_第9张图片

还有一点需要切记的是,这样做编译器是不支持的,虽然字符串支持这样操作,我们可以认为这样会与initializer_list产生冲突,因为{}已经被识别为了initializer_list了

【C++从0到王者】C++11(全文三万字,超详解)_第10张图片

其实上面的{}赋值给initializer_list本质上还是调用它的构造函数

那么vector为什么可以直接接收initializer_list的类型呢?

其实本质上是vector写了一个构造函数,即支持使用initializer_list初始化的构造函数。

image-20231016174447529

这个构造函数也是非常好想的

vector(initializer_list<value_type> il)
{
	reserve(il.size());
	for(auto& e : il)
	{
		push_back(e);
	}
}

所以现在也解释了为什么vector看上去使用{}初始化可以有任意个类型,其实是两次构造函数得到的

如下是在我们原本的vector容器中进行改造的。

【C++从0到王者】C++11(全文三万字,超详解)_第11张图片

不仅仅是vector中可以这样使用,在map中也有initializer_list初始化

image-20231016184354846

image-20231016184513602

这样在map中这样用其实比较有点意思,首先map的插入需要的是pair类型,所以实际上里层的两个花括号是多参数的隐式类型转换,将两个字符串转化为pair类型,然后外层的花括号就是initializer_list了

二、声明

1.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::iterator it = dict.begin();
	auto it = dict.begin();
	//cout << typeid(dict).name() << endl;
	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第12张图片

2.decltype

关键字decltype将变量的类型声明为表达式指定的类型。

有时候我们需要用一个变量的类型,来声明另外一个变量

但是我们千万不可以这样做,因为这个typeid出来的仅仅只是一个字符串,而不是类型

【C++从0到王者】C++11(全文三万字,超详解)_第13张图片

为了达到我们的目的,我们可以这样做,不过这样做的缺陷就是它还必须得进行定义,但是我们有时候是不需要进行赋值的。

【C++从0到王者】C++11(全文三万字,超详解)_第14张图片

所以就有了这个decltype关键字,它可以取出类型

【C++从0到王者】C++11(全文三万字,超详解)_第15张图片

还有一种场景是在类里面的

【C++从0到王者】C++11(全文三万字,超详解)_第16张图片

还有这样的场景,在类模板的场景

【C++从0到王者】C++11(全文三万字,超详解)_第17张图片

而且decltype还可以推导表达式的类型

【C++从0到王者】C++11(全文三万字,超详解)_第18张图片

总结:

  1. typeid推出的类型仅仅是一个字符串,只能看不能用
  2. decltype推出对象的类型,可以定义变量,模板传参

3.nullptr

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

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

如下就是NULL的缺陷

【C++从0到王者】C++11(全文三万字,超详解)_第19张图片

主要原因还是在于NULL使用宏定义的

在effective中也提到了尽量使用const enum inline去替代宏

三、范围for

关于这一点,在之前实现STL的时候已经十分熟练了,所以就不做介绍了,我们只需要知道它是C++11的就可以了

四、智能指针

这里我们后面介绍,这里仅需知道它是C++11的

五、STL中的一些变化

1.新容器

用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set。

【C++从0到王者】C++11(全文三万字,超详解)_第20张图片

array对标的其实就是普通的数组,它两在用法上几乎没有任何区别,甚至是字节大小都一样,唯一不同的就是他们两个的类型不同。这俩个都是静态的数组

int main()
{

	int a1[10];
	array<int, 10> a2;

	cout << sizeof(a1) << endl;
	cout << sizeof(a2) << endl;

	cout << typeid(a1).name() << endl;
	cout << typeid(a2).name() << endl;

	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第21张图片

虽然这两个用起来没有任何区别,但是array对于[]运算符重载要比普通的更严格一些

下面代码是普通数组,越界却没有任何报错,因为其本质是指针的解引用

【C++从0到王者】C++11(全文三万字,超详解)_第22张图片

下面代码是对于array的使用,其越界后会强制报错,主要原因就是它的[]运算符本质是operator[]函数的调用,内部会有检查的

【C++从0到王者】C++11(全文三万字,超详解)_第23张图片

不过总体说array还是比较鸡肋的,因为我们更喜欢使用vector,而且它还可以初始化

vector<int> v(10,0);

还有一个就是forward_list

【C++从0到王者】C++11(全文三万字,超详解)_第24张图片

Forward_list是序列容器,允许在序列中的任何位置进行常量时间的插入和擦除操作。

转发列表被实现为单链表;单链表可以将它们包含的每个元素存储在不同且不相关的存储位置。顺序是通过链接到序列中下一个元素的每个元素的关联来保持的。

forward_list容器和列表容器在设计上的主要区别在于,前者在内部只保留一个指向下一个元素的链接,而后者为每个元素保留两个链接:一个指向下一个元素,一个指向前一个元素,允许在两个方向上进行有效的迭代,但每个元素都要消耗额外的存储空间,插入和删除元素的时间开销略高。因此,Forward_list对象比列表对象更有效,尽管它们只能向前迭代。

与其他基本标准序列容器(array、vector和deque)相比,forward_list在插入、提取和移动容器内任意位置的元素方面通常表现更好,因此在大量使用这些元素的算法(如排序算法)中也表现更好。

与其他序列容器相比,forward_lists和lists的主要缺点是它们无法通过位置直接访问元素;例如,要访问forward_list中的第六个元素,必须从开始迭代到该位置,这需要在两者之间的距离上花费线性时间。它们还消耗一些额外的内存来保存与每个元素相关联的链接信息(对于包含小元素的大型列表来说,这可能是一个重要因素)。

forward_list类模板在设计时考虑到了效率:通过设计,它与简单的手写c风格单链表一样高效,事实上,它是唯一一个出于效率考虑而故意缺少size成员函数的标准容器:由于其作为链表的性质,拥有一个花费常量时间的size成员将要求它为其大小保留一个内部计数器(就像list一样)。这将消耗一些额外的存储空间,并使插入和删除操作的效率稍微降低。要获取forward_list对象的大小,可以使用带有开始和结束的距离算法,这是一个需要线性时间的操作。

它的接口如下

【C++从0到王者】C++11(全文三万字,超详解)_第25张图片

它只支持头插和头删除,因为尾插尾删效率太低。

如果非要用可以使用insert和erase。但是这两个是往该节点后面插入或删除的。

2.新接口

第一大变化就是增加了cbegin系列的迭代器

【C++从0到王者】C++11(全文三万字,超详解)_第26张图片

这些迭代器可以返回const迭代器,但是实际上begin也可以返回const迭代器,所以这个也是比较鸡肋的

新接口的第二大变化就是所有容器均支支持{}列表初始化的构造函数

这个主要是由initializer_list容器支持的。

第三大变化就是emplce接口,不过这里涉及到右值引用,和模板的可变参数,后序会介绍

【C++从0到王者】C++11(全文三万字,超详解)_第27张图片

除了emplace以外,还升级了push_back接口,因为使用了右值引用,使得性能提高了

【C++从0到王者】C++11(全文三万字,超详解)_第28张图片

第四大变化就是,新容器增加了移动构造和移动赋值,部分接口的性能得到了极大的提升

【C++从0到王者】C++11(全文三万字,超详解)_第29张图片

六、右值引用和移动语义

1.左值引用和右值引用

传统的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;
}

但是对于一个字符串,比如下面的,它是一个左值,因为它可以取地址,返回首元素的地址

"xxxxxx";
const char* p = "xxxxxx";

【C++从0到王者】C++11(全文三万字,超详解)_第30张图片

而且这个字符串上的字符也是左值

【C++从0到王者】C++11(全文三万字,超详解)_第31张图片

总之:

左值一定可以取地址,右值无法取出地址

左值引用和右值引用

左值引用就是给左值取别名

右值引用就是给右值取别名

如下所示,右值引用就是使用两个&&即可

int main()
{
	int a = 0;
	int& r = a;
	//右值引用
	int&& rr = 0;
	double x = 1.1;
	double y = 2.2;
	double&& ret = x + y;
	return 0;
}

其实除了左值引用给左值取别名,右值引用给右值取别名以外。

还可以左值引用给右值取别名,只需要加上一个const就可以了,但是绝不可以直接使用左值引用去引用右值,这会直接报错的

int main()
{
	const int& r = 0;
	return 0;
}

那么能否右值引用给左值取别名呢?首先可以确定的是不可以直接使用,但是可以给左值加上move就可以了,但是move可能会对这个左值造成一些其他影响

int main()
{
	//左值引用给右值取别名
	const int& r = 0;
	//右值引用给左值取别名
	int a = 0;
	int&& r1 = move(a);

	return 0;
}

总结

  1. 左值和右值的区别就是能否取地址,左值可以取地址,右值不可以取地址
  2. 左值引用可以给左值取别名,不可以引用右值;右值引用可以给右值取别名,但是不可以引用左值
  3. const左值引用可以给右值取别名,右值引用可以给move以后的左值取别名

2.右值引用的使用场景和意义

我们先来看左值引用的使用场景和价值

左值引用的使用场景和价值

使用场景: 1、做参数(输出型参数) 2、做返回值

价值:减少拷贝

然后左值引用有一种场景还没有解决:那就是返回局部对象不可以使用左值引用。即下面的场景,我们只能去使用传值返回,不能传左值引用返回,因为无论是左值引用返回还是const左值引用返回其实本质上都是引用那块空间,而这块空间出了作用域就销毁了。销毁了以后我们还拿到这个别名的话,那么问题就大了,因为就相当于野指针了。我们没有权限去访问

【C++从0到王者】C++11(全文三万字,超详解)_第32张图片

所以这个地方在之前只能使用传值返回

而传值返回的话,如果编译器不加任何优化,那么func返回的时候要产生一个临时对象,这是一次拷贝构造,然后用这个临时对象在拷贝构造给s,这又是一次拷贝构造,代价实在太大了。如果这个str字节有十几万的话,代价很大的。所以编译器将这里给优化为了一次拷贝构造

为了方便讨论,我们使用下面的这个string

namespace Sim
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			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)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		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
	};
}

当我们使用如下代码的时候,我们可以看到代价还是比较大的

Sim::string func()
{
	Sim::string str("xxxxxxxxxxxxxxx");
	return str;
}

int main()
{
	Sim::string ret = func();

	Sim::string s;
	s = func();

	return 0;
}

这里的第一次深拷贝是编译器优化后的,将两次深拷贝合二为一为一次深拷贝

而下面是我们先定义一个string变量,然后我们去使用func去创建一个string对象,然后返回他,此时我们只能去构造一个临时对象,这是一次拷贝构造,然后我们将这个临时对象赋值给s对象,由于我们的赋值运算符重载复用了拷贝构造,所以最终的代价是两次拷贝构造

【C++从0到王者】C++11(全文三万字,超详解)_第33张图片

我们先不管上面的,先看下面的代码

下面的代码是否构成函数重载?

【C++从0到王者】C++11(全文三万字,超详解)_第34张图片

当然构成函数重载,参数不同的类型

那么下面的是否构成函数重载呢?

【C++从0到王者】C++11(全文三万字,超详解)_第35张图片

其实也会的。

但是这里const左值引用是可以引用右值,而下面的也能引用右值。那么当我们写出下面的这段代码的时候,会发生什么呢?

【C++从0到王者】C++11(全文三万字,超详解)_第36张图片

他们会走向最匹配的函数

而且如果没有下面的,编译器也是可以跑的

【C++从0到王者】C++11(全文三万字,超详解)_第37张图片

所以在这里,如果有右值引用的版本,就会走右值引用版本,会走最匹配的

然后现在我们再去回过头来看前面的代码,我们知道,前面的代码在编译器不优化的情况下,代价有点太大了

【C++从0到王者】C++11(全文三万字,超详解)_第38张图片

在这里一共涉及到了三次深拷贝,第一次是str创建的时候,第二次是str返回的时候会产生一个临时对象,第三次是将这个临时对象给s时候,还要发生一次深拷贝。

【C++从0到王者】C++11(全文三万字,超详解)_第39张图片

然而其中有两块空间可以说是浪费掉了,如下图打×的部分都是被浪费掉了

【C++从0到王者】C++11(全文三万字,超详解)_第40张图片

那么有没有什么办法可以进行优化呢?

其实关于右值:

我们可以把它分为内置类型的右值和自定义类型的右值

而内置类型的右值我们一般也称为纯右值,自定义类型的右值一般也称为将亡值

而函数返回值,表达式返回值也是一个右值。并且对于我们上面s字符串的操作,比如说s1+s2,to_string,其实本质都是函数调用,(s1+s2是一个运算符重载,其实本质也是函数调用),而这些函数调用返回的都是将亡值。

【C++从0到王者】C++11(全文三万字,超详解)_第41张图片

也就是说

s = 左值  //只能老老实实进行深拷贝
s = 右值将亡值  //可以进行移动拷贝

这里的移动拷贝其实就是交换资源,如下所示,就是移动拷贝。

		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动拷贝" << endl;
			swap(s);
			return* this;
		}

我们来分析一下,由于func函数返回的是一个临时对象,这个临时对象就是一个右值,既然是右值,那么我们就使用右值引用,正好交换资源。即可,相比使用const左值引用要减少了一次深拷贝。

使用const左值引用的话,const对象无法被修改,所以只能去使用一次拷贝构造去创建一个可以交换资源的对象,然后再进行交换。而对于右值引用,就不存在无法被修改的问题了。所以可以直接去交换资源

【C++从0到王者】C++11(全文三万字,超详解)_第42张图片

而且由于编译器会自动走最匹配的,所以对于右值会走向第二个函数,只有当第二个函数不存在的时候,才会走向第一个函数

如下图所示,第一次深拷贝是func函数中要返回一个临时对象所造成的,第二次拷贝是移动拷贝所必须的,但是这里的移动拷贝里面仅仅只是交换资源,几乎没有消耗。

所以使用了移动拷贝的话,那么就只剩下两次消耗了,一次是func函数中要开一个str字符串,一次是要返回一个临时对象,两次消耗,但是只有一次深拷贝。在赋值这里就没有任何消耗了。而原来的就是三次消耗,即需要两次深拷贝。

【C++从0到王者】C++11(全文三万字,超详解)_第43张图片

那么上面的场景是我们不让编译器优化的场景,那么如果让编译器优化呢?

我们可以在利用右值引用写出这样的拷贝构造函数

		string(string&& s)
			:_str(nullptr)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;
			swap(s);
		}

反正右值都是将亡值,没什么用的值,那么不妨直接用来作为资源即可,即直接交换,直接就可以几乎没有任何开销了,没有深拷贝了

【C++从0到王者】C++11(全文三万字,超详解)_第44张图片

同样的,原来的const左值引用,虽然可以引用右值,但是由于const,导致我们无法直接利用这个将亡值的资源,我们只能眼睁睁看着这个将亡值自己消亡,却无法直接拿走他的资源。所以只能自己去利用它创造一个对象,用这个新的对象去交换资源,这样就多了一次深拷贝了。

【C++从0到王者】C++11(全文三万字,超详解)_第45张图片

可见直接0开销了,而前面编译器优化后还有1次开销呢,可见充分利用了右值的将亡特性。

不过在这里还有一些疑惑的点就是,func要返回的时候,str是一个左值,那么他就必须得用来拷贝构造来构造一个临时对象,这个临时对象确实可以零开销的进行拷贝构造了,但是这里应该还有一次拷贝构造啊?为什么打印结果里没有呢?

其实本身编译器就会做出一些优化:即连续的构造、拷贝构造会进行合二为一,甚至是合三为一。而编译器在这里做出的优化其实就是直接用str去拷贝构造ret。即不需要借助中间的临时变量了。那么func返回的其实就是str本身。而str虽然是一个左值,但是他本身符合将亡值的特征。因为出了作用域,它即将销毁,所以编译器此时做出了第二个优化:把str本身识别为右值。相当于给move了一下,右值引用去引用左值。

总的来说编译器直接进行了两次优化

  1. 连续的构造、拷贝构造合二为一(不需要临时变量了,只有传值返回才可以)
  2. 编译器把str识别为了右值(因为str虽然是左值,但是符合将亡值特征,相当于进行了一次特殊处理)

而且还需要注意的是,这里千万不可以写传引用返回

【C++从0到王者】C++11(全文三万字,超详解)_第46张图片

首先就是传引用返回的话,那么这块空间已经被销毁了,就出现了野指针的问题了。其次只有传值返回的时候,编译器才会进行优化,如果不传值返回,编译器就不会进行上面的优化了。

而且传值返回所造成的第一个优化,即不需要临时变量的本质其实就是把拷贝放在了str还没有销毁的时候,即在函数内部。而传引用返回就一定不可以了

在上面如果个str加上一共move,相当于我们也将他认为是右值了,这样其实也是可以的

【C++从0到王者】C++11(全文三万字,超详解)_第47张图片

一旦我们加上了拷贝构造的右值引用,那么对于编译器无法第一种优化的场景也可以使用第二次种优化。

在这里因为我们并不是连续的拷贝和拷贝构造。而是一次拷贝构造和一次赋值运算符重载。

拷贝构造是由于func要返回一个临时对象,但是这个我们可以将str识别为将亡值,就可以使用移动拷贝了。将str的资源转移到临时对象中去

然后这个临时对象又进一步的使用赋值运算符重载,这里又是一次移动拷贝,因为刚好这个临时对象是一个右值。又一次的转移资源。

最终整个过程没有任何的消耗,仅仅只是两次转移资源,代价极低

【C++从0到王者】C++11(全文三万字,超详解)_第48张图片

3.左值引用和右值引用的价值和场景

对于右值引用的移动拷贝,实际上我们一般只将其用于自定义类型中,尤其是深拷贝的场景,比如vector>这种拷贝代价极大的场景,而对于内置类型,对其使用右值引用的移动拷贝其实意义并不是很大,或者说没有任何意义。并不能带来一丝的优化。甚至对于浅拷贝的自定义类型也没有任何价值。只有深拷贝的自定义类型才有价值。

左值引用的核心价值就是减少拷贝,提高效率

而右值引用的核心就是价值就是进一步减少拷贝,弥补左值引用没有解决的场景。如:传值返回。

那么右值引用的场景有哪些呢?

这个场景一就是:自定义类型中深拷贝的类,且必须传值返回的场景

而我们之前所演示的,正式满足上面两个条件的情形

像下面这个就不可以了,因为ret并不是右值。就只能老老实实拷贝构造了

【C++从0到王者】C++11(全文三万字,超详解)_第49张图片

我们可以同时对比满足和不满足的场景

此时str的地址后四位是d9d8

【C++从0到王者】C++11(全文三万字,超详解)_第50张图片

当出了移动拷贝结束后,此时ret1的地址后四位是d9d8

【C++从0到王者】C++11(全文三万字,超详解)_第51张图片

所以最终结果为交换了资源,而下面这个是不会交换资源的

【C++从0到王者】C++11(全文三万字,超详解)_第52张图片

像下面这种move也是不会转移走ret的资源的

【C++从0到王者】C++11(全文三万字,超详解)_第53张图片

但是move这样使用会转移走资源

【C++从0到王者】C++11(全文三万字,超详解)_第54张图片

【C++从0到王者】C++11(全文三万字,超详解)_第55张图片

我们可以这样去理解move,这个move会返回一个和ret一样的右值,它的资源都是一样的。所以才能导致调用移动拷贝

上面的不仅仅是我们实现的是这样的, 库里面的也是这样的会进行转移资源

【C++从0到王者】C++11(全文三万字,超详解)_第56张图片

场景二:容器的插入接口,如果插入对象是右值,可以利用移动构造转移资源给数据结构中的对象,也可以减少拷贝

如下图所示

【C++从0到王者】C++11(全文三万字,超详解)_第57张图片

在这里我们先来研究一下在何时发生的拷贝

如下所示,在尾插的时候,会先创建一个新节点,这个新节点在new的时候会调用它的拷贝构造,它的拷贝构造会走一个初始化列表,在这个初始化列表中调用了string中的拷贝构造,从而达到了深拷贝

而下面的尾插一共move后的str2时候,会调用右值引用,所以最终会调用移动拷贝,可以直接转移资源了

【C++从0到王者】C++11(全文三万字,超详解)_第58张图片

不过上面的写法会导致str2的资源被拿走

【C++从0到王者】C++11(全文三万字,超详解)_第59张图片

所以我们一般情况不会向上面那种写法

我们一般直接这样写,这样写也会调用移动拷贝,而且不会像中间那种写法使得str2的资源被转移走,导致str2失效

【C++从0到王者】C++11(全文三万字,超详解)_第60张图片

这样做的原理就是,3333333333333333这个字符串会先构造成一个临时对象或者说匿名对象,总之是具有常性的。是一个将亡值,就会导致它会去调用右值引用。

从而会去调用移动拷贝,减少拷贝,提高效率

【C++从0到王者】C++11(全文三万字,超详解)_第61张图片

如果没有右值引用的话,即没有移动构造的话,那么三个都将是拷贝+拷贝构造。(我们这里只是没有将拷贝打印出来而已)

【C++从0到王者】C++11(全文三万字,超详解)_第62张图片

即便是在STL的其他容器中,基本都是有右值引用的。用来提高效率

4.完美转发

万能引用

如下代码所示

下面代码是一个模板,在这个模板中,有一个看上去像右值引用的存在,但是实际上,它并不是右值引用,而是万能引用。

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

万能引用:它既可以接收左值,又可以接收右值

当实参为左值的时候,它就是左值引用,我们也称为引用折叠

当实参为右值的时候,它就是右值引用

所以对于下面的代码,我们就可以知道,这些实际调用的都是右值,左值,右值,const左值,const右值。他们调用的实际上不是同一个函数

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

但是当我们运行的时候

运行结果为如下所示

【C++从0到王者】C++11(全文三万字,超详解)_第63张图片

我们会发现结果其实不符合我们的预期

这是什么情况?难道全折叠了?理论上应该不可能的吧。

我们先用下面这段代码来观察一下

【C++从0到王者】C++11(全文三万字,超详解)_第64张图片

我们发现这怎么也是左值引用呢?

我们可以用下面这段代码来发现一些问题。我们发现虽然r左值引用了a,rr右值引用了a,但是他们两个本身却是左值,因为他们可以取出地址。

【C++从0到王者】C++11(全文三万字,超详解)_第65张图片

我们知道,右值有两个属性:第一个是不可以取地址,第二个是不可以修改。

而这里rr不仅可以取地址,还可以进行修改。

【C++从0到王者】C++11(全文三万字,超详解)_第66张图片

而右值引用似乎却可以进行修改?其实它也必须得修改,如果它不支持修改,那么它就完蛋了

我们看我们实现右值引用的部分

【C++从0到王者】C++11(全文三万字,超详解)_第67张图片

我们会发现,我们在当func返回的这个临时变量对象进行调用移动拷贝的时候,这里s是右值引用了str,但是s居然可以被修改,而且这个s还可以传递给一个左值引用去修改。

所以说s是一个左值。

所以这里我们可以这样理解,虽然它是一个右值,我们也使用了右值引用,但是这个引用可以认为开了一块空间,把这个右值给存起来

所以说

右值引用变量的属性会被编译器识别成左值(相当于一个特殊处理)

否则在移动构造的场景下,无法完成资源转移,必须要修改

所以说这里的就是t无论它引用的是一个左值还是右值,它本身的属性就是一个左值

image-20231021175318051

所以现在,我们再来看这里,我们就可以看懂了

【C++从0到王者】C++11(全文三万字,超详解)_第68张图片

那么如何让这个调用函数的时候,让它保持原有的属性呢?

C++在这里搞出来了一个完美转发

当t是左值的时候,保持左值属性

当t是右值的时候,保持右值属性

【C++从0到王者】C++11(全文三万字,超详解)_第69张图片

【C++从0到王者】C++11(全文三万字,超详解)_第70张图片

想到完美转发,我们可以突然意识到前面有一个问题似乎编译器底层应该用的就是完美转发了,即下面的val本来是左值,但是我们需要它的右值属性,所以可以使用完美转发

【C++从0到王者】C++11(全文三万字,超详解)_第71张图片

然后我们可以去尝试修改一下我们之前所是实现的链表,如下是之前的链表

namespace Sim
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

		list_node(const T& val = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_val(val)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;

		Node* _node;

		__list_iterator(Node* node)
			:_node(node)
		{}

		Ref operator*()
		{
			return _node->_val;
		}

		Ptr operator->()
		{
			return &_node->_val;
		}

		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator!=(const self & it) const
		{
			return _node != it._node;
		}

		bool operator==(const self & it) const
		{
			return _node == it._node;
		}
	};

	//template
	//struct __list_const_iterator
	//{
	//	typedef list_node Node;
	//	Node* _node;

	//	__list_const_iterator(Node* node)
	//		:_node(node)
	//	{}

	//	const T& operator*() 
	//	{
	//		return _node->_val;
	//	}

	//	__list_const_iterator& operator++() 
	//	{
	//		_node = _node->_next;
	//		return *this;
	//	}

	//	__list_const_iterator operator++(int) 
	//	{
	//		__list_const_iterator tmp(*this);
	//		_node = _node->_next;
	//		return tmp;
	//	}

	//	bool operator!=(const __list_const_iterator& it) 
	//	{
	//		return _node != it._node;
	//	}

	//	bool operator==(const __list_const_iterator& it) 
	//	{
	//		return _node == it._node;
	//	}
	//};

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:

		typedef __list_iterator<T, T&, T*> iterator;
		//typedef __list_const_iterator const_iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;
		iterator begin()
		{
			//return _head->_next //单参数的构造函数支持隐式类型转换
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}
		const_iterator begin() const
		{
			//return _head->_next //单参数的构造函数支持隐式类型转换
			return const_iterator(_head->_next);
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		list()
		{
			//_head = new Node;
			//_head->_next = _head;
			//_head->_prev = _head;
			//_size = 0;
			empty_init();
		}

		list(const list<T>& lt)
		{
			//_head = new Node;
			//_head->_next = _head;
			//_head->_prev = _head;
			//_size = 0;
			empty_init();

			for (auto& e : lt)
			{
				push_back(e);
			}
		}
		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}
		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		void push_back(const T& val)
		{

			insert(end(), val);
			//Node* newnode = new Node(val);
			//Node* tail = _head->_prev;

			//tail->_next = newnode;
			//newnode->_prev = tail;

			//newnode->_next = _head;
			//_head->_prev = newnode;
		}
		void push_front(const T& val)
		{
			insert(begin(), val);
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

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

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

			++_size;

			return newnode;
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			delete cur;
			cur = nullptr;

			prev->_next = next;
			next->_prev = prev;

			--_size;

			return next;
		}

		size_t size()
		{
			//size_t sz = 0;
			//iterator it = begin();
			//while (it != end())
			//{
			//	it++;
			//	sz++;
			//}
			//return sz;
			return _size;
		}

		~list()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

	private:
		Node* _head;
		size_t _size;
	};
}

当我们的代码与以前的链表相结合的时候,发现调用的全部都是深拷贝,而且还多了一次,深拷贝,多的那一次与我们的实现有关,因为链表里面有个头节点。

【C++从0到王者】C++11(全文三万字,超详解)_第72张图片

而对于STL库里里面的代码来说就是正常的移动拷贝了

【C++从0到王者】C++11(全文三万字,超详解)_第73张图片

主要原因就是因为,list的push_back接口只有const左值引用版本,为了解决这个问题,我们只能去使用一个右值引用版本的来处理

所以我们现在来进行修改list

首先是push_back中的

image-20231021185513812

由于要调用insert,所以进一步修改

【C++从0到王者】C++11(全文三万字,超详解)_第74张图片

由于这里还涉及到Node,所以进一步修改

image-20231021185627670

最后运行结果如下图所示

【C++从0到王者】C++11(全文三万字,超详解)_第75张图片

除此之外,我们还可以这样做,这样做的话,也就是说是,只需要使用一个万能引用就可以了。不过这个函数我们必须加上模板,不然的话对于const类型是无法进行构造的。

【C++从0到王者】C++11(全文三万字,超详解)_第76张图片

七、lambda表达式

1.对类数组排序的一个例子

如下代码所示,当我们想要对一个类中的数据进行排序的时候,我们想要使用sort的话,显然我们是无法直接进行排序的,当然我们可以使用运算符重载来支持直接排序,但是这里会出现一个问题,那就是一个类有很多的成员,我们如果想要对这个成员排序完成以后,还想要对其他成员进行排序,这个时候我们就只能使用仿函数了,来进行各种各样的排序,如下代码所示:

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;
	}
};
struct CompareEvaluateGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._evaluate > gr._evaluate;
	}
};
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()); //价格降序
	sort(v.begin(), v.end(), CompareEvaluateGreater()); //评价降序
}

但是上面代码还有一些问题,那就是假如一个命名不规范等问题出现的时候,会非常麻烦

有没有更好的办法呢?当然有,那就是lambda表达式

如下所示,就是一个lambda表达式的简单例子

【C++从0到王者】C++11(全文三万字,超详解)_第77张图片

如下所示也是一个简单的样子

【C++从0到王者】C++11(全文三万字,超详解)_第78张图片

总而言之:

函数指针 ------能不用就不用

仿函数---------类重载operator(),对象可以像函数一样使用

lambda表达式------匿名函数对象,函数内部可以直接定义使用。

2.lambda表达式语法

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

各部分说明

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

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

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

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

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

如下代码所示

int main()
{
	int a = 0;
	int b = 2;
	auto add1 = [](int x, int y) ->int {return x + y; };
	auto add2 = [](int x, int y) {return x + y; };

	cout << add1(a, b) << endl;
	cout << add2(a, b) << endl;

	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第79张图片

除此以外还可以写多行语句等等

【C++从0到王者】C++11(全文三万字,超详解)_第80张图片

但是要注意的是,我们如果直接去调用其他的局部的lambda表达式的话,会报错的

【C++从0到王者】C++11(全文三万字,超详解)_第81张图片

但是如果是一个全局的,是可以的

【C++从0到王者】C++11(全文三万字,超详解)_第82张图片

那么有没有办法可以使用局部的呢?其实是有的,那就是捕捉列表,比如下面的代码

【C++从0到王者】C++11(全文三万字,超详解)_第83张图片

那么捕捉列表有哪些捕捉方式呢?

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

[var]:表示值传递方式捕捉变量var,捕捉后为const类型

[=]:表示值传递方式捕获所有父作用域中的变量(包括this),捕捉后为const类型

[&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表达式之间不能相互赋值,即使看起来类型相同

首先需要特别注意的是捕捉列表出来的是不可以修改的,如下代码所示

image-20231022154736970

如果真的想修改捕捉到的变量,可以加上mutable

【C++从0到王者】C++11(全文三万字,超详解)_第84张图片

不过这里的mutable仅仅只是让这个变量可以被修改了。但是这里是传值的,里面的修改并不会影响外面的。实际上这个用处不大

【C++从0到王者】C++11(全文三万字,超详解)_第85张图片

如果想修改外面的,可以使用引用捕捉

【C++从0到王者】C++11(全文三万字,超详解)_第86张图片

我们还可以试一下下面的代码

int main()
{
	int a = 0;
	int b = 1;
	int c = 2;
	int d = 3;
	int e = 4;
	cout << a << " " << b << " " << c << " " << d << " " << e << endl;
	auto func = [&] {
		a++;
		b++;
		c++;
		d++;
		e++;
	};
	func();
	cout << a << " " << b << " " << c << " " << d << " " << e << endl;

	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第87张图片

除此之外,还可以混合着来,下面代码是错的,因为a不可以被修改,意思是除了a以外所有的变量使用引用捕捉,而a用值传递的方式捕捉。而a值捕捉以后是不可被修改的,所以错误

【C++从0到王者】C++11(全文三万字,超详解)_第88张图片

而且引用捕捉是可以捕捉const变量的。只不过捕捉以后无法修改,但是可以进行访问,他们的地址都是一样的

【C++从0到王者】C++11(全文三万字,超详解)_第89张图片

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

3.函数对象与lambda表达式

当我们写出这样的代码的时候,我们会发现报错了

【C++从0到王者】C++11(全文三万字,超详解)_第90张图片

于是我们打印出他们类型来观察一下

【C++从0到王者】C++11(全文三万字,超详解)_第91张图片

我们会发现其实这两个对象的类型其实是不一样的。所以当然无法赋值。

其实lambda表达式的底层就是仿函数,这里的f1,f2都是一些仿函数对象,只不过他们的类型是编译器自己生成的。我们看不到而已。

我们这里通过f1去调用的其实都是仿函数的调用

我们可以用如下代码来进行观察

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

下面是对于仿函数的,可以看到调用了构造函数和operator()

【C++从0到王者】C++11(全文三万字,超详解)_第92张图片

下面是对于lambda表达式的,我们也可以看到调用了构造函数和operator()

【C++从0到王者】C++11(全文三万字,超详解)_第93张图片

所以lambda表达式底层其实就是仿函数,就像范围for的底层是迭代器一样

八、可变参数模板

1.可变参数模板

我们知道,printf这个函数就是一个可变参数的

【C++从0到王者】C++11(全文三万字,超详解)_第94张图片

这里的三个点就代表了,可以写任意个参数

这里面其实就相当于有一个数组把这个实参存起来,然后printf会依次访问数组里面的元素。

以上就是函数的可变参数

而模板参数和函数参数是很类似的,模板参数传递的是类型,函数参数传递的是对象。函数的可变参数是传多个对象,而模板的可变参数就是可以传多个类型

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

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

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

如下代码所示可以计算出有多少个可变参数

template<class T, class ...Args>
void Showlist(T value, Args... args)
{
	cout << sizeof...(args) << endl;
}

int main()
{
	Showlist(1);
	Showlist(1, 1.1);
	Showlist(1, 1.1, 1.2);
	Showlist(1, 1.1, 1.3, 1.2);

	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第95张图片

我们还需要注意的是,如果我们想要访问参数包的话

不可以想当然的以为这样可以取出参数包的内容,这样是错的代码,编译不通过。

【C++从0到王者】C++11(全文三万字,超详解)_第96张图片

我们需要这样访问

template<class T>
void Showlist(T value)
{
	cout << value << endl;
}

template<class T, class ...Args>
void Showlist(T value, Args... args)
{
	cout << value << " ";
	Showlist(args...);

}

int main()
{
	Showlist(1);
	Showlist(1, 1.1);
	Showlist(1, 1.1, 1.2);
	Showlist(1, 1.1, 1.3, 1.2);

	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第97张图片

它这里其实就用了一个编译时的递归。

一开始会将第一个参数传给T,然后剩下的参数包都传给下一层函数。最上面就是结束条件。

在库里面就有一个类似的接口

【C++从0到王者】C++11(全文三万字,超详解)_第98张图片

不过它的参数只有一个参数包,那么应该如何传递呢?其实我们可以使用一个子函数

void _Showlist()
{
	cout << endl;
}

template<class T, class ...Args>
void _Showlist(T value, Args... args)
{
	cout << value << " ";
	_Showlist(args...);

}

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

int main()
{
	Showlist(1);
	Showlist(1, 1.1);
	Showlist(1, 1.1, 1.2);
	Showlist(1, 1.1, 1.3, 1.2);

	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第99张图片

其实像上面的几个函数组合起来,就相当于一个C++版本的print了,可以自动打印

【C++从0到王者】C++11(全文三万字,超详解)_第100张图片

关于这个打印,其实还可以这样玩

这里的核心逻辑就是,在Showlist中,会将参数包传给PrintArg这个函数,这个函数只会解析第一个参数,后面的逗号是一个逗号表达式,用于初始化数组,后面的三个点就是有几个参数就会相当于调用了几次PrintArg这个函数

void Showlist()
{
	cout << endl;
}
template<class T>
void PrintArg(T t)
{
	cout << t << " ";
}
template<class ...Args>
void Showlist(Args... args)
{
	int a[] = { (PrintArg(args),0)... };
	cout << endl;
}

int main()
{
	Showlist(1);
	Showlist(1, 1.1);
	Showlist(1, 1.1, 1.2);
	Showlist(1, 1.1, 1.3, 1.2, string("xxxxx"));

	return 0;
}

运行结果是

【C++从0到王者】C++11(全文三万字,超详解)_第101张图片

不过这段代码其实还可以稍微简化一下

【C++从0到王者】C++11(全文三万字,超详解)_第102张图片

不过上面的方法是一次一次取出来的,能不能一次性全部取出来呢?方便我们进行初始化等操作

如下代码所示,这样的话,我们就可以通过Create函数去调用各种情况的构造函数了,还有拷贝构造函数也可以去调用。特别灵活

class Date
{
public:
	Date(int year = 0, int month = 0, int day = 0)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

template<class ...Args>
Date* Create(Args... args)
{
	Date* ret = new Date(args...);
	return ret;
}

int main()
{
	Date* p1 = Create();
	Date* p2 = Create(2023);
	Date* p3 = Create(2023, 10);
	Date* p4 = Create(2023, 10, 22);
	Date d(2023, 10, 1);
	Date* p5 = Create(d);
	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第103张图片

2.emplace系列

如下接口所示,在C++11以后,很多库里面都加上了emplace系列接口

【C++从0到王者】C++11(全文三万字,超详解)_第104张图片

我们先看以下代码

int main()
{
	std::list< std::pair<int, char> > mylist;
	mylist.push_back(make_pair(40, 'd'));
	mylist.push_back({ 50, 'e' });
	for (auto e : mylist)
		cout << e.first << ":" << e.second << endl;
	return 0;
}

这些代码都是我们之前的正常的尾插

现在有了emplace以后,我们就可以下面的写法了。

这是因为与前面的Date的实现是一样的,通过可变参数模板实现的。

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

也许我们也会听说emplace_back的效率更高一些,但是在上面的场景是看不出来的,下面的场景可以感受出来

int main()
{
	// 下面我们试一下带有拷贝构造和移动构造的Sim::string,再试试呢
	// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
	// 是先构造,再移动构造,其实也还好。
	std::list< std::pair<int, Sim::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;
}

【C++从0到王者】C++11(全文三万字,超详解)_第105张图片

虽然emplace效率稍高一些,但是其实还好,因为并没有太大差距,因为移动拷贝的效率很低

要是真要说的,反倒是内置类型和浅拷贝的效率可以提高一些,因为深拷贝的量级基本在一个量级。而浅拷贝是就显得差距比较大了

class Date
{
public:
	Date(int year = 0, int month = 0, int day = 0)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year = 0, int month = 0, int day = 0)" << endl;
	}
	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date& d)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	std::list< Date > mylist;
	mylist.push_back(Date(2023, 10, 23));
	cout << endl;
	mylist.emplace_back(2023, 10, 23);
	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第106张图片

其中最为核心的原因就是emplace可以传参数包,这就导致了它可以传对象,可以传对象过去。而push_back只能传对象

九、新的类功能

1.新增的默认成员函数

默认成员函数

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

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

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

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

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

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

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

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

我们可以用下面的代码来进行验证

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:
	Sim::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++从0到王者】C++11(全文三万字,超详解)_第107张图片

当这些类都写了的时候,调用深拷贝

【C++从0到王者】C++11(全文三万字,超详解)_第108张图片

如果屏蔽三个中的一个,依然是深拷贝

【C++从0到王者】C++11(全文三万字,超详解)_第109张图片

事实上,一般而言,我们只需判断拷贝构造、赋值重载、析构中的任意一个就可以了。因为他们三个如果要实现一般都是一起实现的,共存亡的。因为一旦写析构了必然涉及到资源的释放,涉及到了资源就必然涉及到了深拷贝。所以我们一般只要其中的一个没写那么三个基本上都不会写的。

2.一些新的关键字

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

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

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

​ 比如如下的例子

【C++从0到王者】C++11(全文三万字,超详解)_第110张图片

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

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

【C++从0到王者】C++11(全文三万字,超详解)_第111张图片

  • 继承和多态中的final与override关键字:final用于防止类被继承,不能被重写。override用于必须重写该虚函数

十、包装类

1.function包装器

lambda表达式很好用,但是它也有一些缺陷,那就是他的类型我们不知道,所以导致传参的时候非常难弄

面对C++中各种各样的类型,比如函数指针,仿函数,lambda表达式,有没有什么办法可以将他们统一起来呢?

我们先看下面的代码

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

从运行结果上来看,这个模板被实例化成了三份

【C++从0到王者】C++11(全文三万字,超详解)_第112张图片

可见他们的类型各不相同,我们能否找一种办法使得只实例化成一份呢?

也就是说,将可调用对象存储到一个容器中

std::function在头文件<functional>
// 类模板原型如下
template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret : 被调用函数的返回类型
Args…:被调用函数的形参

所以我们可以改善前面的代码

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;

	function<double(double)> f1 = f;
	function<double(double)> f2 = Functor();
	function<double(double)> f3 = [](double d)->double { return d / 4; };

	vector<function<double(double)>> v = { f1,f2,f3 };
	double n = 3.3;
	for (auto f : v)
	{
		cout << f(n++) << endl;
	}
	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第113张图片

所以这里就完美的解决了可调用对象的类型问题

我们可以将包装器用于下面题目的改造

逆波兰表达式求值

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        map<string,function<int(int,int)>> cmdFuncMap = {
            {"+",[](int left, int right){return left + right;}},
            {"-",[](int left, int right){return left - right;}},
            {"*",[](int left, int right){return left * right;}},
            {"/",[](int left, int right){return left / right;}}
        };
        for(auto& str : tokens)
        {
            if(cmdFuncMap.count(str))
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();

                st.push(cmdFuncMap[str](left,right));
            }
            else
            {
                st.push(stoi(str));
            }
        }
        return st.top();
    }
};

而且这样改造之后,如果要添加运算,只需要去往map里面加数据即可,不需要做出其他改动

还是对于前面的代码,有了包装器,我们就可以将类只实例化出一份,因为可以统一成一个类型了。

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()
{
	function<double(double)> f1 = f;
	function<double(double)> f2 = Functor();
	function<double(double)> f3 = [](double d)->double { return d / 4; };

	cout << useF(f1, 11.11) << endl;
	// 函数对象
	cout << useF(f2, 11.11) << endl;
	// lamber表达式
	cout << useF(f3, 11.11) << endl;

	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第114张图片

2.bind

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

可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

调用bind的一般形式:auto newCallable = bind(callable,arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推

下面就是一个使用bind的例子

int Sub(int x, int y)
{
	return x - y;
}
int main()
{
	function<int(int, int)> rSub1 = bind(Sub, placeholders::_1, placeholders::_2);
	cout << rSub1(10, 5) << endl;

	function<int(int, int)> rSub2 = bind(Sub, placeholders::_2, placeholders::_1);
	cout << rSub2(10, 5) << endl;

	return 0;
}

运行结果为

【C++从0到王者】C++11(全文三万字,超详解)_第115张图片

我们来详解分解一下这段代码

其实placeholders是一个命名空间,里面有很多的变量,我们先不用仔细考虑。会用就可以了

在rSub这一层第几个参数传递给下标是几的参数,但是在由进一步传入Sub的时候,是按照顺序传递的,所以导致了传递顺序的不同,从而导致了结果的不同。

【C++从0到王者】C++11(全文三万字,超详解)_第116张图片

所以说,这个bind的价值就是交换传递的参数顺序。

因为有一些函数的接口我们需要调整一下顺序,这时候bind就起到了很大的作用了。

而且当我们对于3个参数的函数,如果我们只想要传递两个参数,那么我们也可以用bind

double Sub(int x, int y, double rate)
{
	return (x - y) * rate;
}

int main()
{
	function<double(int, int)> rSub1 = bind(Sub, placeholders::_1, placeholders::_2, 4.2);
	function<double(int, int)> rSub2 = bind(Sub, placeholders::_1, placeholders::_2, 4.3);
	function<double(int, int)> rSub3 = bind(Sub, placeholders::_1, placeholders::_2, 4.4);

	cout << rSub1(10, 5) << endl;
	cout << rSub2(10, 5) << endl;
	cout << rSub3(10, 5) << endl;


	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第117张图片

如果我们想要将固定的参数给到前面,那就是这样的,注意一定是从_1开始的下标

double Sub(int x, int y, double rate)
{
	return (x - y) * rate;
}

double RSub(double rate, int x, int y)
{
	return (x - y) * rate;
}


int main()
{
	function<double(int, int)> rSub1 = bind(Sub, placeholders::_1, placeholders::_2, 4.2);
	function<double(int, int)> rSub2 = bind(Sub, placeholders::_1, placeholders::_2, 4.3);
	function<double(int, int)> rSub3 = bind(Sub, placeholders::_1, placeholders::_2, 4.4);

	cout << rSub1(10, 5) << endl;
	cout << rSub2(10, 5) << endl;
	cout << rSub3(10, 5) << endl;

	function<double(int, int)> rSub4 = bind(RSub, 4.5, placeholders::_1, placeholders::_2);
	function<double(int, int)> rSub5 = bind(RSub, 4.2, placeholders::_1, placeholders::_2);

	cout << rSub4(10, 5) << endl;
	cout << rSub5(10, 5) << endl;

	return 0;
}

【C++从0到王者】C++11(全文三万字,超详解)_第118张图片

因为这个_1和_2其实是给rsub看的。只有他们才会去看这些下标,后面的都是直接传递的

如下所示的场景中,

我们需要注意的是,如果某个函数是某个类域里面的,我们还要记得写上访问限定符,因为它只能访问到这个局部域和全局域中的。

对于静态的函数,写上类域就可以了,但是对于非静态的,它的地址还需要加上取地址符号,静态的可以加也可以不加,除此之外,还需要传递一个this指针,为此我们需要定义一个对象,才能传过去,或者直接传递一个对象也是可以的

class SubType
{
public:
	static int Sub(int x, int y)
	{
		return (x - y);
	}
	int SSub(int x, int y, double rate)
	{
		return (x - y) * rate;
	}
};


int main()
{

	function<int(int, int)> rSub5 = bind(SubType::Sub, placeholders::_1, placeholders::_2);
	SubType sb;
	function<int(int, int)> rSub6 = bind(&SubType::SSub, &sb, placeholders::_1, placeholders::_2, 3);

	function<int(int, int)> rSub7 = bind(&SubType::SSub, SubType(), placeholders::_1, placeholders::_2, 3);

	cout << rSub5(10, 5) << endl;
	cout << rSub6(10, 5) << endl;
	cout << rSub7(10, 5) << endl;


	return 0;
}


【C++从0到王者】C++11(全文三万字,超详解)_第119张图片

对于类域中的非静态,传地址我们可以理解,但是为什么可以传对象呢?

其实bind的底层其实也是仿函数,在这个变量这里重载了operator(),可以根据传入的是对象还是指针去决定最终传递哪一个。所以可以可以传一个对象过去

不过要切记,不可以给匿名对象取地址,因为右值无法取地址

【C++从0到王者】C++11(全文三万字,超详解)_第120张图片

你可能感兴趣的:(【C++】,c++,windows,开发语言,c语言,数据结构,面试)