【c++】c++11新特性(右值引用和移动语义)

发布于:2025-07-16 ⋅ 阅读:(23) ⋅ 点赞:(0)

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



前言

【c++】c++11新特性(列表初始化,initializer_list,auto和decltype,STL中的一些变化)——书接上文 详情请点击<——
本文由小编为大家介绍——【c++】c++11新特性(右值引用和移动语义)

本文小编使用的编译器是vs2019,如果读者友友想要演示现象,建议在vs2019上进行演示,如果是vs2022会存在一系列更严重的优化,对于拷贝构造和移动构造都会优化成为直接使用fun函数返回值str进行去进行对应的操作,会将临时变量的生成也一并优化没了


一、左值引用和右值引用

传统的c++语法中就有引用,在c++11中新增了右值引用的语法特性,所以我们之前学习的引用就叫左值引用,无论是左值引用还是右值引用,它们的本质都是给对象取别名

什么是左值?什么是左值引用

左值是一个表示数据的表达式,如:变量名,指针,解引用的指针等。我们可以获取它的地址,一般可以对它进行赋值。左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。被const修饰的左值不能给它赋值但是可以取它的地址。左值引用就是对左值的引用,给左值取别名,左值引用要使用&

#include <iostream>

using namespace std;

int main()
{
	//以下的a,b,p,*p,str,"yyyyy",字符串"yyyyy"中包含的一个字符'y'都是左值
	int a = 1;//左值a可以出现在赋值符号的左边
	const int b = 0;
	int* p = new int(0);
	const char* str = "yyyyy";

	//以上的数据的表达式都可以取出地址,验证如下
	cout << &a << endl;
	cout << &b << endl;
	cout << &p << endl;//对指针p取地址取出的是栈上存储p指针的那块空间的地址
	cout << &(*p) << endl;//对*p取地址取出的是堆上存放数据0那块空间的地址
	cout << (void*)str << endl;//由于cout遇到字符串会直接打印字符串
	//所以这里我们强转一下打印即可
	cout << (void*)"yyyyy" << endl;//字符串"yyyyy"的本质是表示为常量区
	//存储字符串地址这里我们强转一下即可
	cout << (void*)&((str[1])) << endl;//取出字符串"yyyyy"中的二个字符'y'的地址

	//以下是对左值进行的左值引用
	int& ra = a;//左值a可以出现在赋值符号的右边
	const int& rb = b;
	int*& rp = p;
	const char*& rstr = str;

	return 0;
}

运行结果如下,都可以取出地址
在这里插入图片描述

什么是右值,什么是右值引用

右值也是一个数据的表达式,如:常量,表达式返回值,函数返回值(这个返回值不能是左值引用返回,返回值应该是传值返回)等等。右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取出地址。右值引用就是对右值的引用,给右值取别名,右值引用要使用&&

double Min(double x, double y)//函数是传值返回
{
	return x + y;
}

int main()
{
	double x = 0.1, y = 1.1;
	
	//以下的0,x + y,Min(x, y)都是右值
	// 0
	// x + y
	// Min(x, y)

	//以下是右值引用对右值的引用
	int&& a = 0;//可见右值0可以出现在赋值符号的右边
	double&& b = x + y;
	double&& c = Min(x, y);

	//以下三个赋值操作都小编为了更好的讲解,编写的错误的赋值操作
	//实际中读者友友不要编写这样的代码
	0 = 1;//可以看出右值0不能出现在赋值符号的左边
	x + y = 1;
	Min(x, y) = 1;
	
	//以下三个取地址操作都小编为了更好的讲解,编写的错误的取地址操作
	//实际中读者友友不要编写这样的代码
	cout << &0 << endl;//常量不能取地址
	cout << &(x + y) << endl;//x + y表达式的返回值是一个临时对象
	//临时对象不能直接取出地址
	cout << &Min(x, y) << endl;//Min(x + y)由于是传值返回
	//所以会使用拷贝一个临时对象进行返回,临时对象不能直接取出地址

	return 0;
}

运行结果如下,右值不能取出地址
在这里插入图片描述

二、左值引用与右值的比较

左值引用总结

  1. 左值引用只能引用左值,不能引用右值
  2. const左值引用可以引用左值,也可以引用右值
int main()
{
	int a = 10;
	int& b = a;

	//int& c = 10;//编译报错,实际编写中读者友友请不要这样编写
	const int& d = a;

	return 0;
}

运行结果如下,左值引用不能引用右值
在这里插入图片描述
const左值可以引用右值
在这里插入图片描述

右值引用总结

  1. 右值引用可以引用右值,但是不能引用左值
  2. 右值引用可以引用move以后的左值,关于move实际上是将左值强制返回一个右值,但是如果一行中如果单纯的move(左值)并且不做任何处理,那么对原左值不会产生任何影响,原左值还是左值
int main()
{
	int a = 0;

	int&& b = 0;
	//int&& c = a;//编译报错,实际编写中读者友友请不要这样编写

	int&& d = move(a);

	return 0;
}

运行结果如下,右值引用可以引用右值,可以引用move以后的左值
在这里插入图片描述
右值引用不可以引用左值
在这里插入图片描述

左值与右值的区别就是能否取出地址,左值可以去取出地址,右值不能取出地址

三、右值引用的使用场景和意义

前面我们可以看到左值引用可以引用左值,const左值引用也可以引用右值,那么c++11为什么要提出右值引用呢?那么右值引用的意义是什么呢?其实左值引用在一些场景中不能使用,左值引用存在一些短板,而右值引用的出现就是为了解决左值引用的短板

下面的讲解需要使用到模拟实现的string,代码如下

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>

using namespace std;

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

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

左值引用的使用场景和意义

左值引用的场景:做参数(输出型参数),做返回值(左值引用返回)
意义:减少拷贝,提高效率

void fun1(wzx::string str)
{}

void fun2(wzx::string& str)//左值引用做参数,减少拷贝
{}

wzx::string fun3(wzx::string& str)
{
	return str;
}

wzx::string& fun4(wzx::string& str)//左值引用做返回值,减少拷贝
{
	return str;
}

int main()
{
	wzx::string s1("xxxxxxxxxx");

	fun1(s1);
	//fun2(s1);

	//fun3(s1);
	//fun4(s1);

	return 0;
}

运行结果如下
调用fun1,传参过程中采用传值传参进行了拷贝构造
在这里插入图片描述
调用fun2,传参是采用左值引用传参,减少了拷贝
在这里插入图片描述

调用fun3返回值是传值返回,会进行拷贝构造
在这里插入图片描述
调用fun4,返回值是引用传参,减少了拷贝
在这里插入图片描述

  1. 通过上述验证,左值引用作为参数和返回值的时候,可以减少拷贝,但是并不是所有的场景都能够使用左值引用传参和作为返回值,左值引用也有短板
    在这里插入图片描述

  2. 如上,当我们的函数fun需要返回一个局部对象str的时候,要进行返回这个局部对象必须使用传值返回,不能使用左值引用返回

  3. 如果使用左值引用作为返回值进行返回,那么出了fun函数作用域之后,局部对象str会调用析构函数,先去释放它的堆上的那块空间,将操作权限归还给操作系统,之后fun函数所占用的栈空间也会进行销毁,此时在函数外拿到的左值引用的对象就是已经销毁的对象,此时就无法拿到我们想要的堆上的内容

  4. 所以只能使用传值返回这样去拷贝局部对象str,去返回一个临时对象tmp,这样局部对象str的值就被拷贝到了临时对象tmp上,函数返回临时对象tmp后,出了函数作用域局部对象str调用析构函数析构堆上那块空间的内容,函数栈空间销毁,尽管局部对象str析构释放了,但是不影响临时对象的内容,所以这样就可以正常在函数外拿到局部对象str拷贝的值了,但是传值返回进行拷贝是具有消耗的

wzx::string fun()
{
	wzx::string str("xxxxxx");

	return str;
}

int main()
{
	wzx::string ret = fun();

	return 0;
}

在这里插入图片描述

  1. 场景一:例如上面的代码,fun()会返回局部对象str的拷贝的临时对象tmp,函数外ret会调用拷贝构造使用fun()函数的返回值,即临时对象tmp去拷贝构造ret,在同一语句中进行了连续的拷贝构造,消耗较大,编译器不允许在同一语句中进行连续的拷贝构造,所以会进行优化,在局部对象str的生命周期结束前,会使用拷贝构造让局部对象str构造ret,这样就从两次拷贝构造变成了一次拷贝构造,无法用左值引用解决这类消耗,后续我们将使用右值引用的移动构造去优化将这一次拷贝构造的消耗

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

wzx::string fun()
{
	wzx::string str("xxxxxx");

	return str;
}

int main()
{
	wzx::string ret("yyyyy");

	ret = fun();

	return 0;
}
  1. 场景二:再看上面的代码,我们将ret = fun()放到下一行,这样编译器就没有办法进行优化了,这样fun会返回局部对象str的拷贝的临时对象tmp,fun()的返回值,即临时对象tmp会赋值给ret通过ret调用operator=的方式,其中operator=是采用的现代写法,所以operator=会调用拷贝构造构造出一个临时对象和this进行交换,如果采用传统写法也就是采用传入operator=的临时对象去深拷贝给this,所以本质下面的运行结果的第二行和第三行是进行了一次拷贝构造,所以下面总的运行结果是进行了两次拷贝构造,这两次拷贝构造编译器无法优化,并且左值引用也无法解决,后续小编将使用右值引用的移动赋值和移动构造去极大限度的降低这里的消耗

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

右值引用的移动构造

  1. 小编将使用右值引用的移动构造去优化第一个函数传值返回的场景,在我们模拟实现的string中加入如下的右值语义的移动拷贝构造
  2. 移动构造的本质是将参数右值的资源窃取过来,据为己有,这样就不用进行深拷贝了,所以它叫做移动构造,就是窃取将别人的资源移动过来构造自己
  3. 我们将内置类型的右值叫做纯右值
  4. 将自定义类型的右值叫做将亡值,也就是这里对应str,就算你str不将资源给我,出了函数作用域你str也要即将调用析构函数进行析构了,还不如做一个顺水人情将资源交换给我,这样也会减少拷贝,提高效率

在这里插入图片描述

string(string&& s)
	:_str(nullptr)
{
	cout << "string(string&& s) -- 移动构造" << endl;
	swap(s);
}
  1. 那么此时去运行下面的代码,由于编译器进行了优化成一次拷贝构造+特殊处理识别str为右值,就会匹配右值引用的移动构造,这样消耗就被降低了差不多9成,也就不用去开空间进行深拷贝了,只需要调用swap进行交换资源即可,这样效率就被提高了
wzx::string fun()
{
	wzx::string str("xxxxxx");

	return str;
}

int main()
{
	wzx::string ret = fun();

	return 0;
}

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

右值引用的移动赋值

  1. 小编将使用右值引用的移动赋值去解决场景二拷贝+赋值的场景,在我们的模拟实现的string中加入如下右值语义的移动赋值
string& operator=(string&& s)
{
	swap(s);
	cout << "string& operator=(string&& s) -- 移动赋值" << endl;
	return *this;
}

在这里插入图片描述

  1. 移动赋值也是将资源窃取过来,使用swap进行交换,由于如下代码赋值操作操作的是两个已经存在的对象,ret = fun(),那么ret需要被fun()的返回的对象进行赋值操作,那么ret原来的资源就需要调用析构函数进行析构,同时移动赋值ret需要指向fun()的返回的对象的资源,那么反正fun()返回的对象在进行赋值结束后也要进行析构了,干脆我ret直接将需要清理的资源交换给fun()返回的对象,ret交换后得到fun()返回的对象的资源,fun得到ret需要释放的资源,那么进行赋值结束后,fun()返回的对象的资源就会调用析构函数将原ret的资源进行释放
wzx::string fun()
{
	wzx::string str("xxxxxx");

	return str;
}

int main()
{
	wzx::string ret("yyyyy");

	ret = fun();

	return 0;
}

运行代码如下
在这里插入图片描述

  1. 右值引用的移动语义是指右值引用的移动构造和右值引用的移动赋值,有了右值引用的移动语义之后,那么c++11的性能就提升起来了,对于传值传参需要进行深拷贝的自定义类型以及赋值操作对于右值会去调用相应的移动构造和移动赋值,这种情况下不需要进行开空间,只需要转移资源即可,减少了拷贝,极大的提高了效率
  2. 同时如果进行赋值操作的值是左值,那么会去匹配operator=参数为左值的版本,进行深拷贝,因为左值不是右值(自定义类型是的右值称作将亡值),自定义类型的右值可以进行对应的移动赋值是因为自定义类型的右值即将消亡了,在执行完成这个语句之后生命周期就会结束,所以可以转移它的资源,但是左值不行,左值在执行完成这个语句之后生命周期还没有结束,不能转移它的资源,需要老老实实的进行深拷贝
  3. 小编如上实现的需要深拷贝的string类,需要实现移动构造和移动赋值进行减少拷贝,提高效率,那么对于需要浅拷贝的类型例如日期类需要实现移动构造和移动赋值么?
  4. 答案是不需要,深拷贝的类由于指针指向的堆上的资源一般较大,所以需要实现移动构造和移动赋值,但是对于浅拷贝的类中的成员函数都是内置类型,内置类型不大,所以进行浅拷贝即可,况且就算实现浅拷贝的移动构造和移动赋值那么进行交换实际上也是内置类型浅拷贝进行交换,浅拷贝的类没有什么需要转移的资源,所以直接进行拷贝即可,代价不大

容器新增的右值引用的插入接口

在这里插入图片描述

  1. 场景三:容器的插入接口,如果插入对象是右值,可以利用插入对象的移动构造将资源转移给数据结构中的对象,可以减少拷贝,提高效率
  2. 下面小编带大家看以下现象,如果插入的是左值,左值在执行完成这个语句之后生命周期还没有结束们不能进行转移资源,插入左值会老老实实调用拷贝进行深拷贝
  3. 如果插入的是右值,自定义类型的需要进行深拷贝的右值,由右值的生命周期只有一行,这一行后生命周期就会结束,所以可以进行转移资源,那么插入的是右值就会调用移动构造进行转移资源
int main()
{
	list<wzx::string> lt;
	wzx::string str("xxxxxxxxxx");

	cout << "插入左值" << endl;
	lt.push_back(str);//左值会匹配参数为左值引用的push_back老老实实的调用拷贝构造
	cout << endl;
	cout << "插入右值" << endl;
	lt.push_back(wzx::string("yyyyyyyy"));//插入右值则会去匹配
	//参数为右值引用的push_back,此时就会去调用自定义类型右值的移动构造
	cout << endl;
	cout << "插入右值" << endl;
	lt.push_back(move(str));//使用move强制将左值str返回右值进行插入
	//此时也会调用移动构造

	return 0;
}

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

四、完美转发

模板&&万能引用

  1. 模板中的T&&是万能引用,既可以引用左值,又可以引用右值
  2. 当实参是左值的时候,T&&就是左值引用(也叫做引用折叠,相当于折叠了一个&变成T&)
  3. 当实参是右值的时候,T&&就是右值引用
template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}

深入讲解右值引用的属性是左值

  1. 那么我们可以先看下面代码,请读者友友猜测一下下面代码的运行结果
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;
}
  1. 如果小编猜测不错的话,相信不少读者友友猜测都是右值对应右值引用,左值对应左值左值,const右值对应const右值引用,const左值对应const左值引用吧,那么究竟运行结果是不是这样呢?下面我们实际运行一下代码一探究竟

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

  1. 可以看出,左值对应左值引用,const左值对应const左值引用,这一点很好理解
  2. 但是右值也去对应了左值引用,const右值也去对应了const左值引用,这就不好理解了
  1. 下面小编将抛出一个概念,右值引用的属性会被编译器识别成左值,如何理解?
  2. 右值引用的属性是左值,右值引用可以取出地址并且可以进行修改,这符合左值的属性
  3. 右值虽然是不可以取出地址的并且不可以进行修改,但是使用右值引用对右值取别名之后,会导致右值被存储到特定的位置(在有的场景中是存指向对象的一个指针),可以取出右值引用的地址,并且可以对右值引用进行修改,这侧面的可以说明右值引用之后的属性是左值。演示如下
int main()
{
	int&& ra = 0;

	cout << ra << endl;

	cout << &ra << endl;

	ra = 1;
	cout << ra << endl;


	return 0;
}

运行结果如下,可以取出地址并进行修改
在这里插入图片描述

  1. 如果不想要右值引用的值被修改,那么使用const修饰右值引用进行去引用操作
int main()
{
	const int&& rra = 0;

	cout << rra << endl;
	cout << &rra << endl;

	//以下赋值操作是小编为了更好的讲解编写的错误赋值操作
	//实际中读者友友请不要编写以下代码
	rra = 1;

	return 0;
}

经过const修饰后的右值引用不可以被修改
在这里插入图片描述

  1. 经过const右值引用的属性就变成了const左值,因为还是可以取出地址
int main()
{
	const int&& rra = 0;

	cout << rra << endl;
	cout << &rra << endl;


	return 0;
}

运行结果如下,可以取出地址,仅仅是不能进行修改,但是const右值引用的属性此时就变成了const左值,因为还是可以取出地址的
在这里插入图片描述

  1. 同时在前面中的string进行移动构造还有细节小编没有拿出来进行讲解,而是放在了这里进行讲解更为合适,如下

在这里插入图片描述

  1. 此时我们就理解了右值引用后的值的属性会被编译器识别成左值,那么我们再回看下面的场景就可以理解为什么右值和右值引用都是调用对应的左值引用和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);//当T&&为右值引用时,t的属性为左值,所以才会去调用对应的左值引用
}

int main()
{
	PerfectForward(10); // 右值

	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值

	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

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

完美转发

  1. 由于在一些场景中需要右值引用后的值属性就是右值,左值引用后的值属性就是左值,以应对不同场景的需求。即如何保持对象的原有的属性呢?
  2. c++标准委员会也提供了完美转发,在传参过程中保持对象原生类型属性,如下使用forward<T>(对象)即可
  3. 那么同样是刚才的场景,我们给我们的完美引用中加入完美转发即可实现保持对象原有属性
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(forward<T>(t));
}

int main()
{
	PerfectForward(10); // 右值

	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值

	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

运行结果如下,保持了对象的原有属性
在这里插入图片描述

五、实际应用完美转发

在这里插入图片描述

  1. 接下来小编将带领大家模拟实现list的push_back的右值引用版本,这样如果list的插入对象是需要自定义类型需要深拷贝的右值,那么就会去调用右值对象的移动拷贝完成资源的转移
  2. 没有实现右值引用版本的push_back的list的源代码小编放在了详情请点击<——,小编会基于没有实现右值引用版本的push_back的list的源代码的基础上去实现list的push_back的右值引用版本
#include "List.h"

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

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


	return 0;
}

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

  1. 解析一下运行结果,第一个string的构造函数是由于我们的list是双向带头循环链表,list进行初始哈的时候所以会有一个哨兵位的头节点,由于节点中有一个缺省参数T(),类型是T,而节点中存放的是string所以会调用string的构造函数进行构造出string匿名对象调用拷贝构造进行深拷贝给节点中构造的T类型的_val,由于我们的string中的拷贝构造采用了现代写法,调用构造函数构造出一个临时对象tmp与*this进行交换
template<typename T>
struct list_node
{
	list_node<T>* _prev;
	list_node<T>* _next;
	T _val;

	list_node(const T& x = T())
		:_prev(nullptr)
		, _next(nullptr)
		, _val(x)
	{}
};
  1. 接下来的构造函数是小编传入的匿名对象wzx::string(“xxxxx”)调用的构造函数,接下来就是调用拷贝函数去深拷贝构造_val,由于我们的string中的拷贝构造采用了现代写法,调用构造函数构造出一个临时对象tmp与*this进行交换
  2. 所以会显示调用了四次构造函数和两次拷贝构造
  3. 那么下面我们实现实现右值引用版本的push_back,那么我们给push_back增添右值引用版本,由于右值引用会复用insert,所以我们应该给insert增添一个右值引用的版本,insert中使用了new,new节点,所以对于list的节点list_node我们还应该写一个右值版本的构造函数
  4. 我们这里要使用完美转发,因为只有完美转发,才可以保持右值引用的原有属性,进而层层推进,从push_back调用insert的时候保持右值属性,那么才会去调用右值引用版本的insert,只有右值引用版本insert使用完美转发才会调用右值引用版本的list_node构造函数,只有使用完美转发,list_node的构造函数中的val的属性才会保持原有属性是右值,进而才会去调用匹配string的右值引用的移动构造
  5. 同时这里还需要将list_node的原始的构造函数的缺省参数T()去掉,在empty_init()中的new中加入匿名对象T()即可,这样对于哨兵位的头节点,由于我们加入了缺省参数那么会调用string的构造函数进行构造一个临时对象,这个临时对象是右值,则会去匹配list_node的右值版本的构造函数,调用移动构造进行资源的转移,减少拷贝,提高效率
  6. 这种场景下只有使用完美转发才可以保持对象原有的属性,如果不使用完美转发,其实也可以使用库函数move去将左值强制转化为右值,如果不使用库函数move,那么这里进行的一系列的右值引用版本的函数中右值引用的值就会被编译器识别为左值,就不符合我们的需求了,所以在不同场景下有不同的需求,只有合理利用完美转发或者move才能符合我们的需求
void push_back(T&& val)
{
	insert(end(), forward<T>(val));//采用完美转发
}

iterator insert(iterator pos, T&& val)
{
	Node* cur = pos._node;
	Node* newnode = new Node(forward<T>(val));//采用完美转发
	Node* prev = cur->_prev;

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

	_size++;

	return newnode;
}

list_node(T&& x)
	:_prev(nullptr)
	, _next(nullptr)
	, _val(forward<T>(x))//采用完美转发
{}

list_node(const T& x)
	:_prev(nullptr)
	, _next(nullptr)
	, _val(x)
{}

void empty_init()
{
	_head = new Node(T());//加入匿名对象

	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
}
  1. 接下来我们进行运行测试,看看当节点中进行插入的是自定义类型需要深拷贝的右值的时候,能否可以调用右值对象的移动拷贝构造
#include "List.h"

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

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


	return 0;
}

运行结果如下,当节点中进行插入的是自定义类型需要深拷贝的右值的时候,会调用右值对象的移动拷贝构造,这样就减少了拷贝,提高了效率
在这里插入图片描述

六、源代码

List.h

#pragma once

#include <assert.h>

namespace wzx
{
	template<typename T>
	struct list_node
	{
		list_node<T>* _prev;
		list_node<T>* _next;
		T _val;

		list_node(const T& x)
			:_prev(nullptr)
			, _next(nullptr)
			, _val(x)
		{}

		list_node(T&& x)
			:_prev(nullptr)
			, _next(nullptr)
			, _val(forward<T>(x))
		{}
	};

	template<typename T, typename Ref, typename Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;
		typedef __list_iterator<T, T&, T*> iterator;
		Node* _node;

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

		__list_iterator(const iterator& it)
			:_node(it._node)
		{}

		self& operator++()
		{
			_node = _node->_next;

			return *this;
		}

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

			return tmp;
		}

		self& operator--()
		{
			_node = _node->_prev;

			return *this;
		}

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

			return tmp;
		}

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

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

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

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

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

		iterator begin()
		{
			return _head->_next;
		}

		iterator end()
		{
			return _head;
		}

		const_iterator begin() const
		{
			return _head->_next;
		}

		const_iterator end() const
		{
			return _head;
		}

		void empty_init()
		{
			_head = new Node(T());

			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		list()
		{
			empty_init();
		}

		void clear()
		{
			iterator cur = begin();

			while (cur != end())
			{
				cur = erase(cur);
			}
			_size = 0;
		}

		~list()
		{
			clear();

			delete _head;
		}

		list(const list<T>& lt)
		{
			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)
		//{
		//	Node* tail = _head->_prev;

		//	Node* newnode = new Node(val);

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


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

		void push_back(T&& val)
		{
			insert(end(), forward<T>(val));
		}

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

		void push_front(const T& val)
		{
			insert(begin(), val);
		}

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

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

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

			_size++;

			return newnode;
		}

		iterator insert(iterator pos, T&& val)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(forward<T>(val));
			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;

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

			delete cur;
			cur = nullptr;

			_size--;

			return next;
		}

		size_t empty()
		{
			return begin() == end();
		}

		size_t size()
		{
			return _size;
		}

	private:
		Node* _head;
		size_t _size;
	};

	template<typename T>
	void print(const list<T>& lt)
	{
		list<int>::const_iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

	void test_list1()
	{
		list<int> lt;

		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

	void test_list2()
	{
		list<int> lt;

		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		print(lt);
	}

	struct A
	{
		A(int a = 0, int b = 0)
			:_a(a)
			, _b(b)
		{
		}

		int _a;
		int _b;
	};

	void test_list3()
	{
		list<A> lt;

		lt.push_back(A(1, 1));
		lt.push_back(A(2, 2));
		lt.push_back(A(3, 3));
		lt.push_back(A(4, 4));

		list<A>::const_iterator it = lt.begin();
		while (it != lt.end())
		{
			//cout << (*it)._a << ' ' << (*it)._b << endl;
			cout << it->_a << ' ' << it->_b << endl;
			it++;
		}
		cout << endl;
	}

	void test_list4()
	{
		list<int> lt;
		lt.push_front(3);
		lt.push_front(2);
		lt.push_front(1);
		lt.push_front(0);

		lt.pop_front();

		lt.push_back(4);
		lt.push_back(5);
		lt.push_back(6);

		lt.pop_back();

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

	void test_list5()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		list<int> lt1(lt);
		for (auto& e : lt1)
		{
			cout << e << ' ';
		}
		cout << endl;

		list<int> lt2;
		lt2 = lt1;
		for (auto& e : lt2)
		{
			cout << e << ' ';
		}
		cout << endl;
	}

	void test_list6()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		lt.clear();
		cout << lt.empty() << endl;
		cout << lt.size() << endl << endl;

		lt.push_back(4);
		lt.push_back(3);
		lt.push_back(2);
		lt.push_back(1);

		cout << lt.empty() << endl;
		cout << lt.size() << endl << endl;

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

}

Test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <list>

using namespace std;


//int main()
//{
//	//以下的a,b,p,*p,str,"yyyyy",字符串"yyyyy"中包含的一个字符'y'都是左值
//	int a = 1;//左值a可以出现在赋值符号的左边
//	const int b = 0;
//	int* p = new int(0);
//	const char* str = "yyyyy";
//
//	//以上的数据的表达式都可以取出地址,验证如下
//	cout << &a << endl;
//	cout << &b << endl;
//	cout << &p << endl;//对指针p取地址取出的是栈上存储p指针的那块空间的地址
//	cout << &(*p) << endl;//对*p取地址取出的是堆上存放数据0那块空间的地址
//	cout << (void*)str << endl;//由于cout遇到字符串会直接打印字符串,所以这里我们强转一下打印
//	cout << (void*)"yyyyy" << endl;//字符串"yyyyy"的本质是表示为常量区存储字符串地址,这里我们强转一下即可
//	cout << (void*)&((str[1])) << endl;//取出字符串"yyyyy"中的二个字符'y'的地址
//
//	//以下是对左值进行的左值引用
//	int& ra = a;//左值a可以出现在赋值符号的右边
//	const int& rb = b;
//	int*& rp = p;
//	const char*& rstr = str;
//
//	return 0;
//}

//double Min(double x, double y)
//{
//	return x + y;
//}
//
//int main()
//{
//	double x = 0.1, y = 1.1;
//	
//	//以下的0,x + y,Min(x, y)都是右值
//	// 0
//	// x + y
//	// Min(x, y)
//
//	//以下是右值引用对右值的引用
//	int&& a = 0;//可见右值0可以出现在赋值符号的右边
//	double&& b = x + y;
//	double&& c = Min(x, y);
//
//	0 = 1;//可以看出右值0不能出现在赋值符号的左边
//	x + y = 1;
//	Min(x, y) = 1;
//
//	cout << &0 << endl;//常量不能取地址
//	cout << &(x + y) << endl;//x + y表达式的返回值是一个临时对象
//	//临时对象不能直接取出地址
//	cout << &Min(x, y) << endl;//Min(x + y)由于是传值返回
//	//所以会使用拷贝一个临时对象进行返回,临时对象不能直接取出地址
//
//	return 0;
//}

//int main()
//{
//	int&& ra = 0;
//
//	cout << ra << endl;
//
//	cout << &ra << endl;
//
//	ra = 1;
//	cout << ra << endl;
//
//
//	return 0;
//}


//int main()
//{
//	const int&& rra = 0;
//
//	cout << rra << endl;
//	cout << &rra << endl;
//
//	rra = 1;
//
//	return 0;
//}


//int main()
//{
//	int a = 10;
//	int& b = a;
//
//	//int& c = 10;
//	const int& d = a;
//
//	return 0;
//}


//int main()
//{
//	int a = 0;
//
//	int&& b = 0;
//	int&& c = a;
//
//	int&& d = move(a);
//
//	return 0;
//}


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

//void fun1(wzx::string str)
//{}
//
//void fun2(wzx::string& str)
//{}
//
//wzx::string fun3(wzx::string& str)
//{
//	return str;
//}
//
//wzx::string& fun4(wzx::string& str)
//{
//	return str;
//}

//int main()
//{

//	//fun4(s1);
//
//	return 0;
//}

//wzx::string fun()
//{
//	wzx::string str("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
//
//	return str;
//}
//
//int main()
//{
//	wzx::string ret = fun();
//	return 0;
//}

//int main()
//{
//	wzx::string ret("yyyyy");
//
//	ret = fun();
//	
//	return 0;
//}

//int main()
//{
//	wzx::string ret = fun();
//
//	return 0;
//}

//int main()
//{
//	wzx::string ret("yyyyy");
//
//	ret = fun();
//
//	return 0;
//}


//int main()
//{
//	list<wzx::string> lt;
//	wzx::string str("xxxxxxxxxx");
//
//	cout << "插入左值" << endl;
//	lt.push_back(str);//左值会匹配参数为左值引用的push_back老老实实的调用拷贝构造
//	cout << endl;
//	cout << "插入右值" << endl;
//	lt.push_back(wzx::string("yyyyyyyy"));//插入右值则会去匹配
//	//参数为右值引用的push_back,此时就会去调用自定义类型右值的移动构造
//	cout << endl;
//	cout << "插入右值" << endl;
//	lt.push_back(move(str));//使用move强制将左值str返回右值进行插入
//	//此时也会调用移动构造
//
//	return 0;
//}

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

//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(forward<T>(t));
//}
//
//int main()
//{
//	PerfectForward(10); // 右值
//
//	int a;
//	PerfectForward(a); // 左值
//	PerfectForward(std::move(a)); // 右值
//
//	const int b = 8;
//	PerfectForward(b); // const 左值
//	PerfectForward(std::move(b)); // const 右值
//	return 0;
//}


#include "List.h"

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

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


	return 0;
}

总结

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


网站公告

今日签到

点亮在社区的每一天
去签到