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

news2024/10/7 4:22:13

文章目录

  • 一、 统一的初始化列表
    • 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;
}

image-20231016165727210

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

同时这样的话就可以应用于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} };

image-20231016170042960

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

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

string s = "xxxxx";

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

image-20231016170625069

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

image-20231016170829482

这里我们还必须要加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

image-20231016172715305

image-20231016172901775

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

image-20231016172951220

那么initializer_list是如何实现的呢?

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

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

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

image-20231016173641072

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

image-20231016173741875

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

image-20231016174042597

其实上面的{}赋值给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容器中进行改造的。

image-20231016183431726

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

image-20231016185553536

2.decltype

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

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

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

image-20231016190227633

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

image-20231016190356177

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

image-20231016190540475

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

image-20231016190907405

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

image-20231016191114469

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

image-20231016191658926

总结:

  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的缺陷

image-20231016192404814

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

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

三、范围for

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

四、智能指针

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

五、STL中的一些变化

1.新容器

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

image-20231017174034247

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

image-20231017174635231

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

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

image-20231017174823580

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

image-20231017174915887

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

vector<int> v(10,0);

还有一个就是forward_list

image-20231017175705936

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

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

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

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

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

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

它的接口如下

image-20231017181358441

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

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

2.新接口

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

image-20231017181845358

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

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

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

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

image-20231017183845367

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

image-20231017184008624

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

image-20231017184148166

六、右值引用和移动语义

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

image-20231017210633584

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

image-20231017210853028

总之:

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

左值引用和右值引用

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

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

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

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左值引用返回其实本质上都是引用那块空间,而这块空间出了作用域就销毁了。销毁了以后我们还拿到这个别名的话,那么问题就大了,因为就相当于野指针了。我们没有权限去访问

image-20231017213113544

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

而传值返回的话,如果编译器不加任何优化,那么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对象,由于我们的赋值运算符重载复用了拷贝构造,所以最终的代价是两次拷贝构造

image-20231017220250351

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

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

image-20231017220938275

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

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

image-20231017221219215

其实也会的。

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

image-20231017221505503

他们会走向最匹配的函数

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

image-20231017221548637

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

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

image-20231018121430062

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

image-20231018123036808

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

image-20231018123235014

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

其实关于右值:

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

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

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

image-20231018124512505

也就是说

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

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

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

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

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

image-20231018125937322

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

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

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

image-20231018130035430

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

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

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

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

image-20231018132419211

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

image-20231018132607409

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

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

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

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

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

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

image-20231018134528560

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

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

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

image-20231018134940143

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

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

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

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

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

image-20231018135911489

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

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

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

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

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

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

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

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

image-20231020195136096

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

此时str的地址后四位是d9d8

image-20231020195447388

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

image-20231020195541409

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

image-20231020200420712

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

image-20231020200725828

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

image-20231020200830507

image-20231020200902986

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

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

image-20231020201447935

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

如下图所示

image-20231020203833083

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

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

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

image-20231020204826608

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

image-20231020205016957

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

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

image-20231020205147084

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

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

image-20231020205633537

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

image-20231020210102737

即便是在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;
}

但是当我们运行的时候

运行结果为如下所示

image-20231020214817067

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

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

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

image-20231020215241936

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

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

image-20231020215603166

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

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

image-20231020215859900

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

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

image-20231021174422752

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

所以说s是一个左值。

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

所以说

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

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

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

image-20231021175318051

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

image-20231021175501880

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

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

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

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

image-20231021175929666

image-20231021175940751

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

image-20231021180233396

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

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<class T>
	//struct __list_const_iterator
	//{
	//	typedef list_node<T> Node;
	//	Node* _node;

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

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

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

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

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

	//	bool operator==(const __list_const_iterator<T>& 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<T> 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;
	};
}

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

image-20231021183427378

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

image-20231021183657660

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

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

首先是push_back中的

image-20231021185513812

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

image-20231021185550665

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

image-20231021185627670

最后运行结果如下图所示

image-20231021201645302

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

image-20231021190041602

七、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表达式的简单例子

image-20231021203509283

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

image-20231021204023611

总而言之:

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

仿函数---------类重载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;
}

image-20231022151255213

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

image-20231022152424085

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

image-20231022152535311

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

image-20231022152825753

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

image-20231022153433966

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

  • 捕捉列表描述了上下文中那些数据可以被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

image-20231022154911289

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

image-20231022155018067

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

image-20231022155213015

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

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

image-20231022160456123

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

image-20231022160553772

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

image-20231022160942419

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

3.函数对象与lambda表达式

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

image-20231022162418144

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

image-20231022162638706

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

其实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()

image-20231022163353062

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

image-20231022163530085

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

八、可变参数模板

1.可变参数模板

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

image-20231022184602589

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

这里面其实就相当于有一个数组把这个实参存起来,然后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;
}

image-20231022194839587

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

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

image-20231022195100636

我们需要这样访问

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

image-20231022195523692

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

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

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

image-20231022200538273

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

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

image-20231022201131880

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

image-20231022201639583

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

这里的核心逻辑就是,在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;
}

运行结果是

image-20231022202320196

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

image-20231022202647529

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

如下代码所示,这样的话,我们就可以通过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;
}

image-20231022204939679

2.emplace系列

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

image-20231022211136400

我们先看以下代码

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

image-20231022212404206

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

image-20231022213335437

其中最为核心的原因就是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;
}

这是利用了编译器自己生成的移动拷贝

image-20231022223914593

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

image-20231022223941629

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

image-20231022224144419

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

2.一些新的关键字

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

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

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

​ 比如如下的例子

image-20231022225657578

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

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

image-20231022231732159

  • 继承和多态中的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;
}

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

image-20231022184249168

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

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

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

image-20231023000501670

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

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

逆波兰表达式求值

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

image-20231023122340122

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

运行结果为

image-20231023124944366

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

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

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

image-20231023132126484

所以说,这个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;
}

image-20231023134331290

如果我们想要将固定的参数给到前面,那就是这样的,注意一定是从_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;
}

image-20231023134754622

因为这个_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;
}


image-20231023140855761

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

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

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

image-20231023141629058

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1124051.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3BHE003855R0001 UNS2882A 用于嵌入式/工业用途的人工智能盒

3BHE003855R0001 UNS2882A 用于嵌入式/工业用途的人工智能盒. 无风扇iBOX 1200系列包括型号iBOX-1265 UE/iBOX-1245 UE/iBOX-1215 UE&#xff0c;由第12代英特尔酷睿i7/i5/i3处理器(Alder Lake-P)提供动力&#xff0c;通过英特尔Iris Xe显卡和两个DDR4 3200MHz SO-DIMM提供高达…

Gartner 2024年十大战略技术趋势,谈谈持续威胁暴露管理(CTEM)

科技云报道原创。 近日&#xff0c;Gartner发布了2024年企业机构需要探索的10大战略技术趋势。 这份连年更新的报告&#xff0c;是Gartner分析其在未来三年内如何影响企业的战略&#xff0c;以指导关键岗位决策者尽早了解探索并满足各自的业务需求。 2024年十大重要战略趋势…

内网穿透的应用-如何通过TortoiseSVN+内网穿透,实现公网提交文件到内网SVN服务器?

文章目录 前言1. TortoiseSVN 客户端下载安装2. 创建检出文件夹3. 创建与提交文件4. 公网访问测试 前言 TortoiseSVN是一个开源的版本控制系统&#xff0c;它与Apache Subversion&#xff08;SVN&#xff09;集成在一起&#xff0c;提供了一个用户友好的界面&#xff0c;方便用…

【斗破年番】官方改编用心了,彩鳞怀孕并未删,萧潇肯定登场,真相在丹药身上

【侵权联系删除】 【文/郑尔巴金】 斗破苍穹年番动画已经更新了&#xff0c;相信不少人都感觉到不可思议&#xff0c;萧炎跟随美杜莎女王回蛇人族的剧情&#xff0c;居然魔改成这样。好好的腹中孕育出新生命&#xff0c;变成了陨落心炎残余能量&#xff0c;不及时处理的话&…

android各版本权限与存储

权限问题&#xff1a; Android6&#xff08;API 23&#xff09;2015年之前 AndroidManifest.xml标注即可。在安装程序时可以看到程序所需权限&#xff0c;完全接收程序可以获取清单文件标注的权限&#xff0c;拒绝则程序安装不成功。 Android 6-10 动态申请权限&…

目标检测算法——YOLOV7——详解

1、主要贡献 主要是现有的一些trick的集合以及模块重参化和动态标签分配策略&#xff0c;最终在 5 FPS 到 160 FPS 范围内的速度和准确度都超过了所有已知的目标检测器。 当前目标检测主要的优化方向&#xff1a;更快更强的网络架构&#xff1b;更有效的特征集成方法&#xff1…

小家电亚马逊METI备案

亚马逊日本站要求有PSE认证和METI备案&#xff0c;也就是电子电气产品要出口日本&#xff0c;怎么办理&#xff0c;申请流程&#xff0c;费用等&#xff1f; 日本PSE认证的检测标准是什么&#xff1f; J60950检测标准 J62133和J60335。 哪些检测机构可以办理METI和PSE认证&a…

让家长、学生轻松掌握学业表现

亲爱的老师们&#xff01;又到了定期发布成绩的时候啦&#xff01;您是否为繁琐的操作而烦恼&#xff1f;不用担心&#xff0c;我来教给您如何使用成绩查询系统&#xff0c;让您的工作变得轻松又高效&#xff01; 成绩查询系统是什么&#xff1f; 成绩查询系统是一种高效、便捷…

成都瀚网科技有限公司:抖音小店选品策略引领电商潮流

在抖音小店日益繁荣的电商环境中&#xff0c;选品显得尤为重要。一个好的产品可以带来稳定的流量和可观的销售额&#xff0c;而一个错误的选择可能导致店铺的运营陷入困境。那么&#xff0c;如何在抖音小店进行正确的选品呢&#xff1f;本文将为你揭示抖音小店选品的秘密通道。…

序列式容器——vector

1、vector是动态分配的数组&#xff0c;不必程序员手动去扩充数组大小&#xff0c;其原理&#xff1a;填充vector就像扔垃圾&#xff0c;家里的垃圾桶不够放&#xff0c;就倒到小区的大垃圾桶&#xff0c;小区大垃圾桶满了&#xff0c;就有垃圾车来回收&#xff0c;每次都是&am…

Redis的神奇之处:为什么它如此快速?【redis第三部分】

Redis的神奇之处&#xff1a;为什么它如此快速&#xff1f; 前言第一&#xff1a;redis为什么使用单线程第二&#xff1a;深入探讨Redis内存存储&#xff0c;包括内存布局、数据存储和索引机制1. 内存布局&#xff1a;2. 数据存储&#xff1a;3. 索引机制&#xff1a; 第三&…

神器抓包工具 HTTP Analyzer v7.5 的下载,安装,使用,破解说明以及可能遇到的问题

文章目录 1、HTTP Analyzer 工具能干什么&#xff1f;2、HTTP Analyzer 如何下载&#xff1f;3、如何安装&#xff1f;4、如何使用&#xff1f;5、如何破解&#xff1f;6、Http AnalyzerStd V7可能遇到的问题 1、HTTP Analyzer 工具能干什么&#xff1f; A1&#xff1a;HTTP A…

@RequestMapping运用举例(有源码) 前后端如何传递参数?后端如何接收前端传过来的参数,传递单个参数,多个参数,对象,数组/集合(有源码)

目录 一、RequestMapping 路由映射 二、参数传递 1、传递单个参数 2、传递多个参数 3、传递对象 4、后端参数重命名 一、RequestMapping 路由映射 指定请求访问的路径既可以修饰类&#xff0c;又可以修饰方法 RequestMapping支持Get、Post、Delete等多种请求方式 Re…

微信小程序开发源码系统集合版:含15大类别小程序功能 包升级更新

随着微信小程序的日益普及&#xff0c;越来越多的开发者投入到了小程序的开发工作中。为了帮助开发者更高效地进行小程序开发&#xff0c;给大家介绍分享一款微信小程序开发源码集合版&#xff0c;小程序开发平台包含15大类别的小程序功能。 一、微信小程序开发源码集合版概述…

React-Redux总结含购物车案例

React-Redux总结含购物车案例 reduc简介 redux是react全家桶的一员&#xff0c;它为react给i共可预测化的状态管理机制。redux是将整个应用状态存储到一个地方&#xff0c;成为store,里面存放着一颗树状态(state,tree),组件可以派发dispatch行为action给store,而不是直接通知其…

内衣洗衣机有必要买吗?口碑好的小型洗衣机测评

在近年以来&#xff0c;由于人们对健康的认识和生活质量的不断改善&#xff0c;使得内衣洗衣机这一类的产品在近年来得到了飞速的发展&#xff0c;洗烘一体机、洗烘套装的价格总体下降&#xff0c;功能和性能都得到了改善&#xff0c;往往更多的用户会选择一台或者多台洗衣机来…

java--基本的算术运算符、+符号做连接符

运算符是对变量、字面量进行运算的符号 1.基本的算术运算符 注意&#xff1a;如果是整数相除&#xff0c;得到的还是整数&#xff0c;会舍去小数点后面的数的 取余最后得到的是两个数相除的到的余数 2.“”符号可以做连接符的 1.“”符号与字符串运算的时候是用作连接符的&am…

雷电模拟器端口号 adb连接

在尝试adb连接雷电模拟器时&#xff0c;网上查询了一下端口号&#xff0c;发现说是5555. 但是自己尝试&#xff0c;会提示&#xff1a; cannot connect to 127.0.0.1:5555: 由于目标计算机积极拒绝&#xff0c;无法连接。 (10061) 终于发现&#xff0c;因为我打开的模拟器&am…

观察者模式java

观察者模式是一种常见的设计模式&#xff0c;用于在对象之间建立一对多的依赖关系。在该模式中&#xff0c;一个主题&#xff08;被观察者&#xff09;维护了一个观察者列表&#xff0c;并在自身状态发生变化时通知所有观察者进行相应的更新。 观察者模式的核心概念包括以下几…

阿里企业邮箱域名解析MX记录表

阿里企业邮箱配置需要为域名添加MX解析记录&#xff0c;不只是MX域名解析记录值&#xff0c;还需要为域名添加pop3、imap、smtp及mail等CNAME解析类型&#xff0c;阿里云百科aliyunbaike.com分享阿里云企业邮箱域名MX解析记录类型、记录值及服务器地址&#xff1a; 新版阿里企…