小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

【c++】c++11新特性(右值引用和移动语义)——书接上文 详情请点击<——
本文由小编为大家介绍——【c++】c++11新特性(lambda表达式,可变参数模板,emplace系列接口测试,新增的默认成员函数)


一、lambda表达式

c++98的两个例子

  1. 如果我们要对数组内元素是内置类型的元素进行比较,那么使用sort即可,同时通过less和greater进行控制比较逻辑,less对应升序,greater对应降序
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
	int arr[] = { 1, 4, 2, 88, 4, 22, 7 };

	//升序
	sort(arr, arr + sizeof(arr) / sizeof(arr[0]), less<int>());

	for (auto e : arr)
	{
		cout << e << ' ';
	}
	cout << endl;

	//降序
	sort(arr, arr +sizeof(arr) / sizeof(arr[0]), greater<int>());

	for (auto e : arr)
	{
		cout << e << ' ';
	}

	cout << endl;

	return 0;
}

运行结果如下
在这里插入图片描述

  1. 但是如果数组内的元素是自定义类型,则需要我们通过仿函数自己实现比较逻辑,才能满足不同的需求,如果仅仅是在自定义类型内部重载>或<,再通过sort传less和greater控制,太过于局限,例如这里你使用内部重载的>或<是根据年龄进行比较,但是我需要的是通过姓名或者排名,那么则满足不了我们的需求,则需要我们自己编写仿函数控制不同场景下的比较逻辑
struct Student
{
	string _name;
	int _age;
	int _rand;

	Student(string name, int age, int rand)
		:_name(name)
		,_age(age)
		,_rand(rand)
	{}
};

struct nameless
{
	bool operator()(const Student& s1, const Student& s2)
	{
		return s1._name < s2._name;
	}
};

struct ageless
{
	bool operator()(const Student& s1, const Student& s2)
	{
		return s1._age < s2._age;
	}
};

struct randgreater
{
	bool operator()(const Student& s1, const Student& s2)
	{
		return s1._rand > s2._rand;
	}
};


int main()
{
	Student arr[] = { { "xiaoli", 18, 5 }, { "xiaowang", 20, 1 }, { "zhangsan", 19, 3 } };

	sort(arr, arr + sizeof(arr) / sizeof(arr[0]), nameless());//姓名升序
	for (const auto& e : arr)
	{
		cout << e._name << ' ' << e._age << ' ' << e._rand << endl;
	}
	cout << endl;

	sort(arr, arr + sizeof(arr) / sizeof(arr[0]), ageless());//年龄升序
	for (const auto& e : arr)
	{
		cout << e._name << ' ' << e._age << ' ' << e._rand << endl;
	}
	cout << endl;

	sort(arr, arr + sizeof(arr) / sizeof(arr[0]), randgreater());//排名降序
	for (const auto& e : arr)
	{
		cout << e._name << ' ' << e._age << ' ' << e._rand << endl;
	}
	cout << endl;

	return 0;
}

运行结果如下
在这里插入图片描述

但是随着c++的发展,人们逐渐认为,上面的写法太过复杂,每次想要进行自定义类型的比较,都要写一个仿函数的类去控制比较逻辑,而且如果有多个场景的比较,则需要实现多个仿函数的类去控制比较逻辑,并且在命名上还不能冲突,如果程序员在命名上不规范,例如直接使用compare1,compare2进行控制,其它程序员在看代码的时候通过命名看不出来这段代码要进行比较的是什么,所以就必须去看仿函数的类的实现,如果此时仿函数的类不在同一个文件,那么就很难受,所以在c++11中lambda表达式就应运而生

c++11的lambda表达式的简单使用

  1. 通过一个[ ](){}就是一个简单的lambda表达式,[ ]是捕捉列表,()中放参数,{}中放比较逻辑,返回值进行了省略,由编译器自动推导
  2. 可以看出lambda表达式没有对应的名字,所以lambda表达式实际上是一个匿名对象
int main()
{
	Student arr[] = { { "xiaoli", 18, 5 }, { "xiaowang", 20, 1 }, { "zhangsan", 19, 3 } };

	sort(arr, arr + sizeof(arr) / sizeof(arr[0]), [](Student s1, Student s2) {
		return s1._name < s2._name;
		});//姓名升序
	for (const auto& e : arr)
	{
		cout << e._name << ' ' << e._age << ' ' << e._rand << endl;
	}
	cout << endl;

	sort(arr, arr + sizeof(arr) / sizeof(arr[0]), [](Student s1, Student s2) {
		return s1._age < s2._age;
		});//年龄升序
	for (const auto& e : arr)
	{
		cout << e._name << ' ' << e._age << ' ' << e._rand << endl;
	}
	cout << endl;

	sort(arr, arr + sizeof(arr) / sizeof(arr[0]), [](Student s1, Student s2) {
		return s1._rand > s2._rand;
		});//排名降序
	for (const auto& e : arr)
	{
		cout << e._name << ' ' << e._age << ' ' << e._rand << endl;
	}
	cout << endl;

	return 0;
}

运行结果如下
在这里插入图片描述

lambda表达式的语法

lambda表达式的书写:[捕捉列表](参数列表)mutable->返回值类型{函数体,控制比较逻辑的语句}

  1. [ ]:捕捉列表,该列表总是出现在lambda表达式的开头位置,编译器会根据[ ]判断接下来的代码是否是lambda表达式,捕捉列表能够捕捉上下文的变量供lambda表达式使用,[ ]不可省略,捕捉可以使用传值捕捉和引用捕捉,传值捕捉就是只写变量名即可,例如a,引用捕捉则是在变量前加一个引用,例如&a
  2. ():参数列表,与普通函数的参数列表相同,如果参数列表不需要进行传参,则()可以省略
  3. multable(英文意思为可变的):修饰符,默认情况下,lambda表达式中的捕获列表中的变量或对象如果是传值捕捉是具有const属性,即具有常性,即默认是不可以修改的,如果我们想要进行修改那么就要给lambda表达式加上multable,multable的英文意思是可变的,用在这里就可以取消捕获列表中传值捕捉的变量或对象的常性,使其可以进行修改,当使用multable修饰符的时候,参数列表()不能省略,即使参数列表中的参数为空也不能省略参数列表(),如果我们不需要修改参数列表中的参数则可以省略multable
  4. ->:返回值类型,其中->返回值类型,这个语法叫做追踪返回类型语法,可以使用追踪返回类型去声明lambda表达式的返回值类型,如果没有返回值类型,则可以省略->返回值类型。如果我们使用return明确返回对象,即有返回值(返回值类型明确)的情况下,我们也可以省略->返回值类型,由编译器通过返回值对象进行推导返回值类型
  5. {}:函数体,控制比较逻辑的语句,在该函数体内,不光可以使用参数列表中的参数,还可以使用捕获列表中所有的变量或对象

总结:在lambda表达式中,参数列表(),->返回值类型,multable都是可省略的,捕捉列表[ ],函数体{}是不可省略,所以最简单的lambda表达式是[ ]{},并且这也意味着这个最简单的lambda不能执行任何语句,也就是说没有任何作用

lambda表达式的使用

  1. 编写简单的语句,使用auto获取lambda的类型,使用这个类型定义对象进行使用,lambda表达式可以理解为一个无名函数,无法直接进行调用,如果想要进行调用可以利用auto将其赋值给一个变量
  2. 捕捉上下文变量的用法
int main()
{
	int a = 0;
	int b = 1;

	auto add = [](int x, int y) { return x + y; };//lambda表达式是一个匿名对象
	//那么我们可以使用auto来自动推导它的类型,使用这个类型定义一个对象,这样这个对象
	//就可以与lambda表达式的匿名对象有一样的作用
	cout << add(a, b) << endl;

	auto fun = [a, b]() {
		cout << a << endl;
		cout << b << endl;
		};//在捕捉列表中捕捉上下文的变量a和b

	fun();

	return 0;
}

运行结果如下
在这里插入图片描述

  1. 如果是捕获列表中的变量如果是传值捕获,不加mutable则不能进行修改
int main()
{
	int a = 0;
	int b = 1;

	auto fun = [a]() {
		a = 1;
		cout << a << endl;
		};

	fun();
	cout << a << endl;

	return 0;
}

运行结果如下,不加mutable则无法进行修改捕捉列表为传值捕捉的变量
在这里插入图片描述

  1. 加上multable则可以修改捕捉列表中传值捕捉的变量
int main()
{
	int a = 0;
	int b = 1;

	auto fun = [a]() mutable {
		a = 1;
		cout << a << endl;
		};

	fun();
	cout << a << endl;

	return 0;
}

运行结果如下,可以进行修改了,但是由于是传值捕捉,即捕捉列表中的a是上下文的变量a的一份拷贝,捕捉列表中的a进行修改不影响上下文的变量a,
在这里插入图片描述

  1. 如果捕捉列表采用引用捕捉的形式进行捕捉,那么则不用加mutable就可以进行修改,并且捕捉列表中采用引用捕捉的变量的修改会影响上下文中被引用的变量
int main()
{
	int a = 0;
	int b = 1;

	auto fun = [&a]() mutable {
		a = 1;
		cout << a << endl;
		};

	fun();
	cout << a << endl;

	return 0;
}

运行结果如下
在这里插入图片描述

  1. 捕捉列表中可以捕捉局部有auto定义的lambda表达式类型的对象,捕捉之后在lambda表达式中进行调用,对于全局的函数,lambda可以直接进行调用不需要进行捕捉
int sub(int x, int y)
{
	return x - y;
}

int main()
{
	int a = 0;
	int b = 1;

	auto add = [](int x, int y) { return x + y; };

	auto fun = [add](int x, int y) {
		cout << sub(x, y) << endl;
		cout << add(x, y) << endl;
		};
	
	fun(a, b);

	return 0;
}

运行结果如下
在这里插入图片描述

  1. 在使用捕捉列表的时候可以直接使用=,将上下文全部的变量都进行传值捕捉
int main()
{
	int a = 0;
	int b = 1;

	auto add = [](int x, int y) { return x + y; };

	auto fun = [=] {
		cout << a << endl;
		cout << b << endl;
		cout << add(a, b) << endl;
		};

	fun();

	return 0;
}

运行结果如下
在这里插入图片描述

  1. 在使用捕捉列表的时候同样的也可以利用&,将上下文全部的变量都进行传引用捕捉
int main()
{
	int a = 0;
	int b = 1;

	auto add = [](int x, int y) { return x + y; };

	auto fun = [&] {
		a++;
		b++;

		cout << a << endl;
		cout << b << endl;
		cout << add(a, b) << endl;
		};

	fun();
	cout << a << endl;
	cout << b << endl;

	return 0;
}

运行结果如下,可以在lambda内修改上下文变量的值
在这里插入图片描述

  1. 同样可以进行采用传值捕捉和引用捕捉混搭,上下文的变量除了变量a采用传引用捕捉,其它变量都采用传值捕捉

运行结果如下
在这里插入图片描述

  1. 同样可以进行采用传值捕捉和引用捕捉混搭,上下文的变量除了变量a采用传值捕捉,其它变量都采用传引用捕捉
int main()
{
	int a = 0;
	int b = 1;

	auto add = [](int x, int y) { return x + y; };

	auto fun = [&, a] {
		b++;
		cout << b << endl;
		cout << add(a, b) << endl;
		};

	fun();
	cout << b << endl;

	return 0;
}

运行结果如下
在这里插入图片描述

  1. 捕捉列表不可以重复使用同一种方式捕捉,例如[=,a]{}或者[&,&a]{}否则则会编译报错
int main()
{
	int a = 0;
	int b = 1;
	//以下代码是小编为了更好的讲解效果故意编写的错误代码,实际使用中读者友友请勿这样编写
	auto fun1 = [=, a] {
		};


	auto fun2 = [&, &a] {
		};

	return 0;
}

运行结果如下
在这里插入图片描述

lambda表达式的底层

  1. 不同的lambda表达式之间不可以互相赋值,尽管看起来貌似好像类型相同
int main()
{
	auto fun1 = [] { cout << "hello world" << endl; };
	auto fun2 = [] { cout << "hello world" << endl; };
	//以下代码是小编为了更好的讲解效果故意编写的错误代码,实际使用中读者友友请勿这样编写
	fun1 = fun2;

	return 0;
}

运行结果如下,无法进行赋值
在这里插入图片描述

  1. 我们知道如果是类型相同的对象是可以支持赋值的,那么这里不相同,就说明fun1和fun2的类型不相同,我们使用typeid().name()将fun1和fun2的类型打印看一下
int main()
{
	auto fun1 = [] { cout << "hello world" << endl; };
	auto fun2 = [] { cout << "hello world" << endl; };

	cout << typeid(fun1).name() << endl;
	cout << typeid(fun2).name() << endl;

	return 0;
}

运行结果如下,可以看出fun1和fun2的类型不同,所以不能进行赋值
在这里插入图片描述

  1. 那么lambda表达式的底层究竟是什么呢?小编这里可以告诉大家,lambda表达式的底层其实就是仿函数,没啥,就类似于范围for一样都是被编译器无脑替换,范围for是无脑替换成迭代器,这里的lambda表达式则是被编译器无脑替换成仿函数
  2. 函数对象,又称仿函数。即可以想函数一样使用的对象,就是重载了operator()的类对象,下面小编通过下面代码看一下仿函数和lambda表达式的底层汇编
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);

	// lambda表达式
	auto r2 = [rate](double monty, int year)->double { 
		return monty * rate * year; };

	r2(10000, 2);

	return 0;
}

汇编如下
在这里插入图片描述
在这里插入图片描述

  1. 从使用方式上看,lambda表达式与仿函数完全一样
    仿函数将rate作为成员变量,在定义对象的时候给出rate的初始值即可,那么就会调用构造函数进行初始化rete,lambda则是通过捕获列表直接对rate进行捕获
  2. 底层编译器对lanbda表达式的处理方式,完全是按照仿函数的方式进行处理。如果你定义了一个lambda表达式,那么编译器则会自动生成一个类,并且重载operator()

二、可变模板参数

可变参数

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

  1. 其实我们之间已经接触过类似于函数中的可变参数,函数中的可变参数是对象,在我们学习c语言的开始的时候就已经使用过可变参数了,也就是我们的printf和scanf,对于printf底层实际上使用了一个数组将可变参数存起来,编译器知道可变参数的个数,所以对于这个数组开多大也就可以确定,当需要输出的时候就依次将数组的内容依次取出并且对应上即可
int main()
{
	printf("%d\n", 0);
	printf("%d %d\n", 0, 1);
	printf("%d %d %d\n", 0, 1, 2);


	return 0;
}

运行结果如下
在这里插入图片描述

可变参数模板基本概念

相比于c++98/03中函数模板和类模板只能接收并使用固定数量的模板参数,c++11的可变参数模板可以让我们创建可以接收可变参数的函数模板和类模板

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

//Args是模板参数包,args是函数形参参数包
//Args... args表示声明一个参数包,这个参数包中可以包含0到N个模板参数
template<class ...Args>
void ShowList(Args... args)
{}
  1. 上面的args前面有…,所以args就是可变模板参数,我们把带省略号的参数称为参数包,参数包里面包含了0到N(N >= 0)个模板参数,我们无法直接获取参数包中的每个参数,所以我们只能通过展开参数包的方式获取参数包中的每个参数,那么下面就由小编带领大家学习如何展开参数包获取参数包中的每个参数

通过函数递归的方式展开参数包

方式一
  1. 当调用的有零个参数的时候,那么会直接去匹配递归终止函数
  2. 当调用的有一个参数的时候,那么就会直接匹配展开函数,将第一个参数给给value,参数包允许有0个参数,所以此时参数包中有0个参数,输出第一个参数value,递归参数包,此时会直接匹配参数个数为0个的递归终止函数输出换行
  3. 当调用的有两个参数的时候,会去匹配展开函数,其中第一个参数会去给value,第二个参数会给参数包args,那么在展开函数中展开了第一个参数将第一个参数输出,接下来就去递归参数包,根据参数包中的参数个数去匹配对应可变参数的模板函数,如果参数包中的参数个数有零个,那么此时会匹配展开函数,如果参数包中的参数个数有一个至多个,那么会匹配展开函数,此时这种情况下参数包的参数个数有一个会去匹配展开函数,将这个一个参数给value,参数包允许有0个参数,所以参数包中会有0个参数,那么接下来进行输出第一个参数value,接下来递归参数包,由于参数包中只有0个参数,所以会匹配最合适的递归终止函数,输出换行
//递归终止函数
void ShowList()
{
	cout << endl;

	return;
}
//展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << ' ';
	ShowList(args...);//往下递归参数包固定写法args...
}

int main()
{
	ShowList();//0个参数
	ShowList(1);//一个参数
	ShowList(1, 2);//两个参数
	ShowList(1, 2, 3);
	ShowList(1, 2, 3, 4);


	return 0;
}

运行结果如下
在这里插入图片描述

方式二

在这里插入图片描述

  1. 观察vector中的emplace_back的可变参数的模板函数的形参列表中没有第一个参数,只有一个参数包,那么此时我们应该如何进行设计展开参数包呢?
  2. 同样是根据上面方式一,将方式一变成子函数,再套一层即可
void _ShowList()
{
	cout << endl;

	return;
}

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();
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	ShowList(1, 2, 3, 4);

	return 0;
}

运行结果如下
在这里插入图片描述

逗号表达式展开参数包

方式一
  1. 逗号表达式会依次执行,但是执行之后,最后的结果会是最后一个逗号后面的值,所以可以充分利用逗号表达式的性质展开参数包
int main()
{
	cout << (1, 0) << endl;

	return 0;
}

在这里插入图片描述

  1. 那么我们就可以定义一个数组arr,通过(PrintArg(args), 0)… 的形式将参数包进行分解,(PrintArg(args), 0)中其中第一个PrintArg(args)代表的是参数包中的第一个参数,其中的第二个0是使用0初始化数组,…代表的是参数包剩下的参数,这里我们必须写…来代表参数包剩下的参数,因为我们也无法确定参数包中参数的个数,编译器由于要确定arr数组的大小,所以会展开参数包,并且编译器也知道参数包中参数的个数,所以会将…代表的参数包剩下的参数进行展开,例如参数包中一共有三个参数那么编译器会将 { (PrintArg(args), 0)… }中的…替换成{ (PrintArg(args), 0), (PrintArg(args), 0), (PrintArg(args), 0) },由于逗号表达式会依次执行,所以对于单个的(PrintArg(args), 0),就会先调用执行(PrintArg(args)去输出参数,接下来再执行0,那么逗号表达式的结果就是0,即使用0初始化数组
  2. 由于参数包中参数的个数允许是0个,所以要写一个无参的ShowList函数输出换行,用于对应参数包中的参数个数为0个的情况
void ShowList()
{
	cout << ' ' << endl;
}

template<class T>
void PrintArg(T value)
{
	cout << value << ' ';
}

template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... };
	//如果有参数包中有三个参数,那么编译器会替换成下面这种形式	
//int arr[] = { (PrintArg(args), 0), (PrintArg(args), 0), (PrintArg(args), 0) };

	cout << endl;
}

int main()
{
	ShowList();
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	ShowList(1, 2, 3, 4);

	return 0;
}

运行结果如下
在这里插入图片描述

方式二
  1. 对于上面的方式一,小编还可以进行简化一下,将逗号表达式去掉,巧妙修改PrintArg的返回值类型为int,并且让PrintArg的返回值为0即可
void ShowList()
{
	cout << ' ' << endl;
}

template<class T>
int PrintArg(T value)
{
	cout << value << ' ';

	return 0;
}

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

	cout << endl;
}

int main()
{
	ShowList();
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	ShowList(1, 2, 3, 4);

	return 0;
}

运行结果如下
在这里插入图片描述

三、emplace系列接口测试

铺垫

  1. 在学习emplace系列接口前,我们先学习一下可变参数模板的如下应用
  2. 如下是在可变参数的函数模板中利用参数包的特性,如果参数包中的参数是int类型的参数,那么就会去匹配Date的构造函数,如果是三个int类型的参数,那么就会去对应Date的构造函数的三个参数去进行Date的构造,如果参数包中的参数类型是Date类型的,那么就会去匹配Date的拷贝构造
#include <iostream>

using namespace std;


class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "Date构造函数" << endl;
	}


	Date(const Date& d)
		:_year(d._year)
		,_month(d._month)
		,_day(d._day)
	{
		cout << "Date拷贝构造" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

template<class ...Args>
Date* Creat(Args... args)
{
	Date* ret = new Date(args...);//参数包传参的固定写法args...

	return ret;
}

int main()
{
	Creat(2025, 7, 18);
	cout << endl;

	Date d(2025, 7, 18);
	Creat(d);

	return 0;
}

运行结果如下
在这里插入图片描述

emplace_back

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

  1. emplace系列接口使用了可变参数模板,那么也就是说我们可以向上面小编讲的Date的例子去传构造函数的参数直接进行构造,并且本质都是进行插入,那么小编接下来就挑一个典型的尾插对比push_back进行讲解
  2. push_back进行插入的只能是value即节点中存储的实际类型,不能够类似的和emplace_back接口一样直接传节点中存储类型的构造函数的参数进行构造
节点中存储浅拷贝类型
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date构造函数" << endl;
	}

	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date拷贝构造" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	list<Date> lt;
	lt.emplace_back(2025, 7, 18);
	cout << endl;

	lt.push_back(Date(2025, 7, 18));

	return 0;
}

运行结果如下
在这里插入图片描述

  1. 当节点中存储浅拷贝类型的时候emplace_back只调用了一次构造函数,push_back调用了一次构造函数和拷贝构造,所以节点中存储浅拷贝类型的时候emplace_back的消耗较小
节点中存储深拷贝的自定义类型
#include <assert.h>

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

		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		//移动构造
		string(string&& s)
			:_str(nullptr)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		//移动赋值
		string& operator=(string&& s)
		{
			swap(s);
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			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';
		}

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


int main()
{
	list<wzx::string> lt;

	lt.emplace_back("xxxxx");
	cout << endl;

	lt.push_back(wzx::string("yyyyy"));


	return 0;
}

运行结果如下
在这里插入图片描述

  1. 当节点中存储的是需要进行深拷贝的自定义类型的时候,那么emplace_back只调用了拷贝构造,push_back调用了构造函数和移动构造
  2. 小编个人认为当当节点中存储的是需要进行深拷贝的自定义类型的时候emplace_back和push_back的消耗差不多,emplace_back没有比push_back优秀多少,因为移动构造转移资源的消耗足够低

四、新增的默认成员函数

移动构造函数,移动赋值运算符重载

  1. 默认成员函数就是我们不显示写,编译器会默认生成,在原来的c++类中有6个默认成员函数,如下
    在这里插入图片描述
  2. c++11中新增了两个默认成员函数,分别是移动构造函数,移动赋值运算符重载,那么关于新增的这两个默认成员函数注意如下:
  3. 如果我们没有编写移动构造函数,并且对于拷贝构造函数,赋值运算符重载,析构函数都没有进行实现,那么此时编译器会默认生成一个移动构造函数,编译器默认生成的移动构造函数,对于内置类型成员,会按照成员逐字节进行拷贝,对于自定义类型成员,如果自定义类型成员实现了移动构造函数,那么就会优先调用移动构造函数,否则才会去调用拷贝构造函数
  4. 如果我们没有编写移动赋值运算符重载函数,并且对于拷贝构造函数,赋值运算符重载,析构函数都没有进行实现,那么此时编译器会默认生成一个移动赋值运算符重载函数,编译器默认生成的移动赋值运算符重载函数,对于内置类型成员,会按照成员逐字节进行拷贝,对于自定义类型成员,如果自定义类型成员实现了移动赋值运算符重载函数,那么就会优先调用移动赋值运算符重载函数,否则才会去调用赋值运算符重载函数
  5. 如果我们实现了移动构造函数和移动赋值运算符重载函数,那么编译器将不会再去生成对应的移动构造函数和移动赋值运算符重载函数
//当没有编写没有编写移动构造函数,
//但是实现了拷贝构造函数,赋值运算符重载,析构函数
//此时编译器不会生成移动构造函数和移动赋值运算符重载函数
//wzx::string的源代码在上面的节点中存储深拷贝的自定义类型中有
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:
	wzx::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = s1;
	cout << endl;
	Person s3 = std::move(s1);
	cout << endl << endl;

	Person s4;
	s4 = std::move(s2);

	return 0;
}

运行结果如下
在这里插入图片描述

  1. 由于小编string拷贝构造采用的是现代写法,所以在string深拷贝的时候会去调用string的构造函数,所以将拷贝构造函数和下面的构造函数看作执行了一条拷贝构造函数
  2. 由于小编string的赋值运算符重载采用的是现代写法,所以在string调用赋值运算符重载的时候下面会去调用string的拷贝构造函数,而string的拷贝构造函数小编是采用的现代写发,所以将赋值运算符重载和拷贝构造函数和构造函数看作执行了一条赋值运算符重载
  3. 由于此时编译器不会生成移动构造函数和移动赋值运算符重载函数,所以对于自定义类型的成员不会调用移动构造函数和移动赋值运算符重载函数,而是则会调用对应的拷贝构造函数和赋值运算符重载函数
//当没有编写没有编写移动构造函数,
//并且也没有实现拷贝构造函数,赋值运算符重载,析构函数
//所以此时编译器会生成移动构造函数和移动赋值运算符重载函数
//wzx::string的源代码在上面的节点中存储深拷贝的自定义类型中有
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:
	wzx::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = s1;//左值生命周期未结束,匹配string拷贝构造的参数
	cout << endl;  //则会调用string的拷贝构造
	Person s3 = std::move(s1);//move以后的左值会返回右值,右值则会调用移动构造
	cout << endl << endl;

	Person s4;
	s4 = std::move(s2);//move以后的左值会返回右值,右值调用移动赋值

	return 0;
}

运行结果如下
在这里插入图片描述

  1. 那么由于编译器生成了移动构造函数和移动赋值运算符重载函数,那么对于需要深拷贝的自定义类型string,由于小编实现string的时候实现了移动赋值构造函数和移动赋值运算符重载函数,所以就会去调用它的移动赋值构造函数和移动赋值运算符重载函数,当然如果小编没有实现移动赋值构造函数和移动赋值运算符重载函数就会去调用它的拷贝构造函数和赋值运算符重载函数

思考一下为什么对于拷贝构造函数,赋值运算符重载,析构函数都没有进行实现,那么此时编译器会默认生成一个对应的移动构造函数和移动赋值运算符重载函数

  1. 因为通常来讲只有需要进行深拷贝的自定义类型的类才会需要我们显示编写拷贝构造函数,赋值运算符重载,析构函数去完成对应资源的拷贝或清理
  2. 对于仅需要进行浅拷贝的自定义类型的类一般我们不会显示编写拷贝构造函数,赋值运算符重载,析构函数而是由编译器生成默认的拷贝构造函数,赋值运算符重载,析构函数去完成按字节的进行浅拷贝即可
  3. 所以拷贝构造函数,赋值运算符重载,析构函数通常是三位一体的,并且只有需要进行深拷贝的自定义类型的类才需要移动构造函数和移动赋值运算符重载函数进行转移资源,所以编译器默认生成移动构造函数和移动赋值运算符重载函数对于拷贝构造函数,赋值运算符重载,析构函数的要求看似不合理,实则很合理

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

c++11可以让我们更好的控制要使用的默认成员函数,如果我们要使用一些默认成员函数,但是由于一些原因并没有默认生成,例如:我们编写了类的拷贝构造函数,所以此时编译器默认不会帮我们生成移动构造函数,但是此时我们又想要使用移动构造函数,所以可以使用关键字default强制编译器生成对应的默认构造函数

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

	Person(const Person& p)
	:_name(p._name)
	,_age(p._age)
	{}


	Person(Person&& p) = default;


	Person& operator=(const Person& p)
	{
		if(this != &p)
		{
		_name = p._name;
		_age = p._age;
		}

		return *this;
	}

	~Person()
	{}

private:
	wzx::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = s1;//左值生命周期未结束,匹配string拷贝构造的参数
	cout << endl;  //则会调用string的拷贝构造
	Person s3 = std::move(s1);//move以后的左值会返回右值,右值则会调用移动构造
	cout << endl << endl;

	Person s4;
	s4 = std::move(s2);//move以后的左值会返回右值,但是由于我们没有
	                   //使用default让编译器生成移动赋值运算符重载
					   //所以对于深拷贝的自定义类型只会调用赋值运算符重载

	return 0;
}

运行结果如下
在这里插入图片描述

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

如果我们想要限制某些默认成员函数的生成,在c++98中需要将该默认成员设置为私有,即只声明不定义,那么编译器由于我们声明了默认成员函数,所以编译器就不会去生成默认成员函数。在c++11中更为简单,如果我们想要限制某些默认成员函数的生成,只需要在该默认成员函数的声明后面加上=delete即可,该语法就会指示编译器不去生成对应的默认的成员函数,被=delete修饰的函数称为删除函数

当我们使用=delete修饰移动赋值运算符重载函数的时候,编译器不仅会将移动赋值运算符重载函数删除,也会一并的将移动构造也删除了
也就是说如果使用=delete修饰了移动构造函数和移动赋值运算符重载函数中的任意一个,另一个也会被编译器一并删除

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

	Person(const Person& p) = delete;
	//我们使用=delete将拷贝构造删除之后,编译器就不会生成默认的拷贝构造函数了
	
private:
	wzx::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;//由于编译器就不会生成默认的拷贝构造函数,
	//所以这里调用拷贝构造失败,使用delete删除成功

	return 0;
}

运行结果如下,使用=delete删除拷贝构造函数成功
在这里插入图片描述


总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐