右值引用的剖析

news2025/5/11 17:51:16

引入:为什么要有右值引用?

右值引用的存在,就是为了解决左值引用解决不了的问题!

左值引用的问题:

我们知道,左值引用在做参数和做返回值都可以提高效率;但是有时候,我们无法用左值引用返回值,比如:当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回,就导致无法提高效率;这时候就需要右值引用来提高效率了!

一:左值引用和右值引用

1:左值和右值的区别

①:左值

左值概念:

左值是一个表示数据的表达式我们可以获取它的地址+一般可以对它赋值,且左值可以出现赋值符号的左边或右边
重点: 可以取地址  ; 一般可以赋值  ;可以出现在赋值符号左边或右边
左值例子:
int main()
{

// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;

return 0;
}

如何证明这些例子就是左值?规则说左值可以取地址,那就对上述例子取地址验证!

取地址验证:

int main()
{

	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	//对上面左值取地址验证
	cout << &p << endl;
    cout << &(*p) << endl;
	cout << &b << endl;
	cout << &c << endl;
   

	return 0;
}

运行结果:

目前,取地址这个规则是通过了,继续进行规则中验证赋值

在以下中验证赋值的时候,会知道为什么规则中会说左值一般可以赋值 因为有个例无法赋值

赋值验证:

int main()
{

	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	cout << p << endl;
	cout << *p << endl;
	cout << b << endl;
	cout << c << endl;



	//对其赋值验证
	int k = 100;
	p = &k;
	*p = 1000;
	b = 30;
	//c = 10;

	cout << p << endl;
	cout << *p << endl;
	cout << b << endl;
	cout << c << endl;

	return 0;
}

运行结果:

我们的确对这些左值做出了赋值,验证通过! 除了c这个左值无法赋值!

一般可以对左值进行赋值,这句话中的一般就是指的c这种static修饰的左值不可以修改(取消注释即报错),所以c我们无法赋值修改

证明了 例子中的左值的确是左值

注意: 左值也可以出现赋值符号的右边

例子:

int main()
{

int b = 1;
int* p = &b;//赋值符号的左边p是左值,右边也b是左值

return 0;
}

②:右值

右值概念:

右值是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址,不能赋值
右值例子:
int main()
{
double x = 1.1, y = 2.2;

// 以下几个都是常见的右值
10;
x + y;

return 0;
}

注意:x和y是左值,但是x+y这个表达式是右值  

如何证明以上例子就是右值?规则说右值不可以取地址,那就对上述右值进行取地址验证! 

int main()
{
	double x = 1.1, y = 2.2;

	10;
	x + y;
	

	&(10);//报错
	&(x + y);//报错


	return 0;
}

报错:

目前,取地址这个规则的确报错,继续进行验证赋值查看是否报错:

int main()
{
	double x = 1.1, y = 2.2;

	10;
	x + y;

	10 = 20;//报错
	x + y = 20;//报错

	return 0;
}

报错:

证明了 例子中的右值的确是右值

③:从函数返回值来看左值和右值

函数返回值也有左右值之分 之前不好一起举例子 现在单独拿出来举例子~

函数采取的是传值返回 ->则返回的是右值

函数采取的是引用返回->则返回的是左值

例子如下:

// 返回左值的函数 
int& getLeftValue(int& x)
{
	return x; // 返回变量的引用
}

// 返回右值的函数 
int getRightValue(int a)
{
	return a; // 返回临时计算结果
}

int main() {
	int num = 10;

	// 对返回左值函数进行赋值
	getLeftValue(num) = 20; // 返回值为左值,所以可以赋值,修改了返回的值
	cout << getLeftValue(num) << endl; // 输出20

	// 对返回左值函数进行取地址
	&(getLeftValue(num)); // 可以取地址
	cout << &(getLeftValue(num)) << endl; // 输出左值的地址


	// 使用右值返回函数
	//getRightValue(num) = 20; // 不可以赋值 报错!
	//&(getRightValue(num)); // 不可以取地址 报错!



	return 0;
}

 报错如下: 

总结:

Q1:getRightValue函数返回的是右值,为什么?
A1:返回值是原始类型(int)的副本,是一个临时对象,取地址操作(&)需要一个持久的、可寻址的内存位置,而临时值不满足这个条件;而getLeftValue返回的是左值的引用,所以该返回值是左值,因为具有一个持久的、可寻址的内存位置

注意:getLeftValue函数的参数一定要加上引用符号,否则会返回了一个即将被销毁的局部变量的引用!这是典型的悬空引用!

//错误写法
int& getLeftValue(int x) 
{ 
    return x;  // 会返回局部变量的引用!
}


//正确写法
int& getLeftValue(int& x) 
{ 
    return x;           
}

Q2:为什么参数不加& ,返回值也不能加&? 

A2:函数参数x若只是传值接收,那其的作用域只在这个函数内,出了函数就会销毁,所以只能传值返回;与引入中的返回值即将销毁,函数只能传值返回是一样的道理 ;

而若是,引用接收,那代表其的作用域乃是整个main函数中(因为x本身就是main函数中的变量num),所以函数中的x不会出函数就销毁,所以可以引用返回!

2:左值引用和右值引用的区别

①:左值引用

左值引用很简单,我们之前学的引用全是左值引用,写法如下:

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

return 0;
}

③:二者的底层区别

先说结论:二者底层操作是几乎一致的

int main()
{
	int x = 10;

	int& a = x;//左值引用
	int&& b = 10;//右值引用


	return 0;
}

查看两种引用的汇编:

//int & a = x;的汇编
007A2006  lea    eax, [x]       ; 将变量 x 的地址加载到 eax 寄存器
007A2009  mov    dword ptr [a], eax  ; 将 eax(x 的地址)存入 a(引用本质是指针)

//int&& b = 10;的汇编
007A200C  mov    dword ptr [ebp-30h], 0Ah  ; 将立即数 10 存入栈上的临时空间 [ebp-30h]
007A2013  lea    eax, [ebp-30h]            ; 获取临时空间的地址
007A2016  mov    dword ptr [b], eax        ; 将地址存入 b(引用本质仍是指针)

Q:为什么说二者底层操作是几乎一致的?

A:因为区别仅在于:右值引用,是把这个数据先存储在栈上,再进行引用;而左值是直接引用本来就在栈上存储好的数据,所以说二者底层是几乎一致的

这里介绍二者的底层一致,是为后面作铺垫.....

3:互相引用

在某些写法下,左值引用能引用右值,右值引用也能引用左值~

①:左值引用去引用一个右值

int main()
{
	//左值引用去引用一个右值
	const int& r1 = 10;

    //int& r1 = 10;报错!

	return 0;
}

解释: 

Q1:为什么加了const的左值引用能去引用右值?

A1:我本人对于这个问题,从一开始的表面的理解,到后面深层次的理解,这里我都说一下吧

A:表面的理解:

10这个右值存放在了一个临时变量,而临时变量都具有常性(不可写),所以const让r1不可写,二者权限一致,所以加了const的左值引用能去引用右值。

B:深层次的理解:

        为什么有深层次的理解,因为我觉得表面的理解,有一些漏洞:右值本身的生命周期是当前这一行代码,而左值是当前作用域,而你用左值引用去引用一个值,那这个值肯定在当前作用域都有可能被使用到,而右值又随时可能被销毁,这不是冲突了吗?所以表面的理解只能说明我理解了const让二者权限一致,但不理解为什么二者的生命周期不一致还能使用这个点!!

右值和左值的差别在于:

a:右值的生命周期只在这一行代码;而左值是当前作域

b:右值无法被修改;而左值一般可以修改(可以修改,也可能不可以修改)

所以要想用左值引用去引用一个右值,我们就要尽可能的把这个右值变成一个左值,那怎么变?

a:应该把右值的生命周期延长至和左值一致,为当前作用域

b:右值法修改不用管,因为左值也有不能修改的 也省事

如图:

而const左值引用就可以达到要求:

a:const 左值引用绑定右值:C++ 会隐式延长右值的生命周期,使其和引用的作用域一致。

b:const本身就可以保证 r1 是无法修改的 与 右值10权限一致

所以const左值引用可以取引用一个右值了!

对不用改,右吻合这个点的理解:

之前的省事原则,是帮助理解,实则是理解不够到位的

其实这里的保持引用右值的r1无法被修改有更深层次的原因:(也就是表面理解中的权限问题)

右值10本身就是可读不可写的,根据权限原则,你用r1引用右值10,r1不可能变成可读可写,这属于权限的扩大,是非法的,会报错;所以const加上保持r1的权限和10一致,是必要的!

总结:const在左值引用中除了起到了自身的的作用(让r1不可写),还起到了延长声明周期的效果!

Q2:为什么int& r1 = 10;是非法的?

A2:理解了Q1,Q1就很好理解了!

a:你这直接引用,右值的生命周期依旧是处于当前这一行代码,可能很快被销毁 

b:你没const,你让r1的权限(可读可写)相对于10的权限(可读不可写)扩大了,是错的

思考:为什么要用一个左值引用去引用右值?

        其实const左值引用去引用一个右值的写法为什么会产生,本质就是当时没有右值引用,所以赋予了const左值引用能够引用右值的功能,说白了就是一种语法规则,上面剖析这么多,完全是让大家理解这种强势的语法规则的内部原理,理解语法规则的益处远大于仅仅记得语法规则!

②:右值引用去引用一个左值

例子:

int main()
{
	//x是左值
	int x = 0;
	//右值引用引用左值 对左值move一下即可
	int&& rr1 = move(x);

	return 0;
}

解释:因为对一个左值进行了move操作后,就会变成右值,对于右值,当然可以右值引用了

二:右值引用的意义

        引入中说了,有时候,我们无法用左值引用返回,比如:当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回,而这种传值返回的效率不高,这就是左值返回的不足之处,当然寥寥数语,无法感受到这种不足

1:左值引用的不足

例子:(函数值返回string类的场景)

自己模拟实现一个string类,因为这样构造相关的函数被调用时会打印信息 如下:

namespace bit
{
	class string
	{
	public:

		//构造函数
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;//注释掉 是因为我们传值返回场景只看拷贝构造更好理解
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "拷贝构造 -- 深拷贝" << endl;

			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

		//赋值重载
		//s1 = s3 
		string& operator=(const string& s) {
			if (this != &s) {
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;

				// 把容量和大小赋过去
				_size = s._size;
				_capacity = s._capacity;
			}
			cout << "赋值重载----深拷贝" << endl;
			return *this;   // 结果返回*this
		}

		//析构
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}


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

解释:我实现的构造相关的函数中,都不会牵扯到其他的函数,这样对于后面的讲解不易混淆!

在某些更优的写法中,比如赋值重载函数就会调用拷贝构造函数,这样写虽然简单好用,但是在这里相看运行结果的打印消息时容易混淆,所以要看的去这个博客的现代写法中板块中看: string类的模拟实现_string类模拟实现-CSDN博客

此时在类中进行如下测试:

//bit类中:
string test1()
{
	string s1;
	return s1;

}

//main中:
int main()
{
	bit::string s = bit::test1();

	return 0;
}

前提:我们屏蔽掉了普通构造函数的打印信息的语句 避免混淆

分析:

第一步:return s1,s1会拷贝构造一个临时对象(第一次调用拷贝构造函数)
第二步:bit::string s = bit::test1(); 临时对象拷贝构造给s(第二次调用拷贝构造函数)

如图:

本来应该是两次拷贝构造,但是新一点的编译器(比如我用的vs19)一般都会优化,优化后变成了一次拷贝构造。

如图:

所以运行结果如下图所示:

总结:

        这种场景说明了左值引用的不足,导致只能采取值返回,最终要调用两次拷贝构造,我们知道,类似string这种类的拷贝啊,赋值啊,都是深拷贝的,成本很高,所以两次的拷贝构造带来的两次深拷贝,成本极高;但是在右值引用产生之前,C++官方也做出了相应的努力:直接将连续两次的拷贝构造,优化成了一次拷贝构造,直接用s1拷贝构造出了s,但是不管怎么优化,至少都是一次深拷贝,所以C++官方决心用新的语法来彻底解决这种消耗,那就是右值拷贝

2:右值引用的意义->移动构造

        既然你函数中的s1即将被销毁,而我们又想要s1中的资源,所以我们能不能不要在拷贝出一份一样的资源了,能不能直接搬运s1中的资源,反正你马上就要被销毁了

        就好比,这有一份已经写完了的作业,你打算马上要烧毁了,而我又刚好需要这一份作业,你能不能别让我用一个新的本子再抄你的作业了,你直接给我呗,反正你也马上烧毁不要了

这种行为叫移动构造!

        所以右值引用的思想就是,将s1的资源拷贝构造到到临时变量中(这一步没变),再把临时变量中的资源移动构造到s中(这一步变了),所以就是拷贝构造+移动构造,而后面C++官方直接优化成了移动构造!

        所以现在我们要在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

另外还要写一个交换函数,因为移动构造本身就是交换资源

代码如下:

namespace bit
{
	class string
	{
	public:

		//构造函数
		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 << "拷贝构造 -- 深拷贝" << endl;

			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

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


		//赋值重载
		//s1 = s3 
		string& operator=(const string& s) {
			if (this != &s) {
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;

				// 把容量和大小赋过去
				_size = s._size;
				_capacity = s._capacity;
			}
			cout << "赋值重载----深拷贝" << endl;
			return *this;   // 结果返回*this
		}

		//析构
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}


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


	string test1()
	{
		string s1;
		return s1;

	}
}

int main()
{
	bit::string s;
	s = bit::test1();

	return 0;
}

运行结果:

 

解释:符合预期,本来是要拷贝构造+移动构造,这里优化成了单独的移动构造

分析:

Q1:为什么移动构造的消耗小?

Q2:在需要构造的时候,是如何匹配到移动构造的?

移动构造代码如下:

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

A1:从代码可知:移动构造函数中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价远远小的多

A2:首先是将s1的资源拷贝构造到到临时变量中,而这个临时变量就是一个右值,所以当这个临时变量想要拷贝构造出一个新的对象的时候,就会匹配到移动构造这个函数,因为移动构造的参数是右值引用,而临时变量也是一个右值,刚好参数匹配,所以调用到了移动构造

Q3:那C++官方直接优化成了一个移动构造,直接从s1移动构造到了s,但是s1不是左值吗,怎么能直接调用移动构造函数构造出s呢?左值怎么匹配到移动构造函数里面的右值类型的参数?

A3: 编译器会承受这一切!它会先把s1隐藏的move一下变成右值,然后就可以移动构造,所以我们不用手动写move了;这样做的目的是,我们不用修改原本的代码(手动move),确保了向前兼容!

总结:

移动构造就是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

当一个类是浅拷贝的类的时候,就没必要写移动构造了

3:右值引用的意义->移动赋值

右值引用的好处不仅仅是移动构造,在下面这种场景,右值引用也会有相应的措施进行效率提高

这种行为叫作移动赋值

namespace bit
{
	class string
	{
	public:

		//构造函数
		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 << "拷贝构造 -- 深拷贝" << endl;

			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

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


		//赋值重载
		//s1 = s3 
		string& operator=(const string& s) {
			if (this != &s) {
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;

				// 把容量和大小赋过去
				_size = s._size;
				_capacity = s._capacity;
			}
			cout << "赋值重载----深拷贝" << endl;
			return *this;   // 结果返回*this
		}

		//析构
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}


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


	string test1()
	{
		string s1;
		return s1;

	}
}

int main()
{
	bit::string s;
	s = bit::test1();

	return 0;
}

运行结果:(因为注释掉了移动构造)

 解释如图:

而当我们把代码里面的移动构造注释放开的时候,会变成: 

解释如图:(因为有了移动构造,所以第一次变成了移动构造)

        但是,尽管有了移动构造,我们的两次深拷贝(拷贝构造+赋值重载) 变成了一次深拷贝(移动构造+赋值重载),但是无论如何,都会有一次深拷贝,所以右值引用还有一个意义,那就是实现一个参数为右值的赋值重载->叫作  移动赋值

代码如下:(在string类中增加了移动赋值)

namespace bit
{
	class string
	{
	public:

		//构造函数
		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 << "拷贝构造 -- 深拷贝" << endl;

			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

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


		//赋值重载
		//s1 = s3 
		string& operator=(const string& s) {
			if (this != &s) {
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;

				// 把容量和大小赋过去
				_size = s._size;
				_capacity = s._capacity;
			}
			cout << "赋值重载----深拷贝" << endl;
			return *this;   // 结果返回*this
		}

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

		//析构
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}


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


	string test1()
	{
		string s1;
		return s1;

	}
}

int main()
{
	bit::string s;
	s = bit::test1();

	return 0;
}

运行结果:

不要疑问为什么没有优化,因为像这种分开写的代码,往往都无法优化

//分开写
int main()
{
	bit::string s;
	s = bit::test1();

	return 0;
}

//而不是
int main()
{
	
	bit::string s = bit::test1();

	return 0;
}

关于这种优化的一些规律,我写过一篇博客,建议看看:编译器对连续构造的优化-CSDN博客 

总结:

移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型的,移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,所以它叫移动赋值,就是窃取别人的资源来赋值给自己的意思。

当一个类是浅拷贝的类的时候,就也没必要写移动赋值了

4:右值引用的意义->容器的插入接口

所以当右值引用出来之后,STL中的容器除开增加了移动构造和移动赋值之外,容器的接口还都增加了右值版本

下面是几个例子:

a:string类增加的移动构造:

b:string类增加的移动赋值: 

c:list容器的push_back接口增加了右值版本: 

三:完美转发 forward

那现在我们想对一个模拟实现的没有右值函数接口的list,进行改造,让其也拥有右值的接口。改造如下:

1:改造list,完善右值函数接口

namespace bit
{
	template<class T>
	struct ListNode
	{
		T _data;
		ListNode* _next = nullptr;
		ListNode* _prev = nullptr;
	};
	template<class T>
	class list
	{
		typedef ListNode<T> node;
	public:
		//构造函数
		list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		//左值引用版本的push_back
		void push_back(const T& x)
		{
			insert(_head, x);
		}
		//右值引用版本的push_back
		void push_back(T&& x)
		{
			insert(_head, x); //完美转发
		}
		//左值引用版本的insert
		void insert(node* pos, const T& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = x;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
		//右值引用版本的insert
		void insert(node* pos, T&& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = x; //完美转发

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
	private:
		node* _head; //指向链表头结点的指针
	};
}

此时在main中:

int main()
{
	bit::list<bit::string> lt;
	bit::string s("1111");


	lt.push_back(s);      //期望在监视窗口下的调用逻辑:
						  //左值版本的push_back ----> 左值版本的insert

	lt.push_back("2222"); //期望在监视窗口下的调用逻辑:
						  //右值版本的push_back ----> 右值版本的insert
							
	return 0;
}

解释:按照我们的期望如注释所言,那真的会这样吗?

对lt.push_back(s);   进行调试监视,调用逻辑如下:

符合我们的预期,因为s是左值,所以去调用左值版本的push_back,然后在push_back的内部调用左值版本的inset,和我们预期一致

对lt.push_back("2222");的调试监视,调用逻辑如下:

不符合预期!!因为"2222"是一个右值,应该去调用右值版本的push_back,然后在push_back的内部调用右值版本的inset,但是调试监视下来发现在进入了右值版本的 push_back后,却进入了左值版本的inset,这是为什么??

结论:右值被右值引用后,会变成左值

2:右值被右值引用后,会变成左值

其实这个疑问的点,在之前的移动构造和移动赋值中也有体现:

        右值不可赋值,这是我们在前面就知道的,但是移动构造中调用swap函数的时候,你给swap函数传参数s过去,这个s是右值,而swap里面却能成功得对这个右值s和this指针指向的对象进行交换赋值?!

为什么说交换就是一种赋值,代码如下:

int main()
{
	int x = 1;
	int y = 2;

    //交换二者的值
    //交换本身就是一种赋值
	x = 2;
	y = 1;

	return 0;
}

那请问,这为什么会被允许呢?右值不是不可以赋值吗?

正如结论所言:右值被右值引用会变成左值

例子如下:

int main()
{
	int&& a = 10;

	//10是一个右值 被右值引用之后变成了左值 所以可以赋值和取地址
	a = 20;
	int* p = &a;
	cout << a << endl;
	cout << p << endl;


	return 0;
}

运行结果:

并且在前文的一中的2中的③:二者的底层区别中,汇编也向我们展示了,右值引用和左值引用并未区别,右值引用的步骤:

a:只把这个右值数据先存储在栈上

因为右值一开始无地址,也就是无对应的空间去存储,所以想要引用,只能先存储在栈上

b:再对该空间进行引用

现在看来,b这一步不就是引用一个左值吗

所以这就是为什么右值被右值引用后就变成了左值!

疑问:为什么要这么设计,为什么右值被右值引用后就变成了左值

不这样设计,请问你的移动构造和移动赋值中的交换资源,如何才能进行?你是右值,永远无法完成赋值,而你变成左值后,就能完成交换资源这一步!

所以我们想要将list改造,之前的改造是不彻底的,我们还需要一个东西--->完美转发 forward

3:forward 的作用

forward 完美转发在传参的过程中保留对象原生类型属性,这样我们之前的lt.push_back("2222");就会在调试监视下达到我们期望的调用逻辑
加入了forward 的list代码:
namespace cl
{
	template<class T>
	struct ListNode
	{
		T _data;
		ListNode* _next = nullptr;
		ListNode* _prev = nullptr;
	};
	template<class T>
	class list
	{
		typedef ListNode<T> node;
	public:
		//构造函数
		list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		//左值引用版本的push_back
		void push_back(const T& x)
		{
			insert(_head, x);
		}
		//右值引用版本的push_back
		void push_back(T&& x)
		{
			insert(_head, std::forward<T>(x)); //完美转发
		}
		//左值引用版本的insert
		void insert(node* pos, const T& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = x;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
		//右值引用版本的insert
		void insert(node* pos, T&& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = std::forward<T>(x); //完美转发

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
	private:
		node* _head; //指向链表头结点的指针
	};
}

int main()
{
	cl::list<cl::string> lt;
	cl::string s("1111"); 
	lt.push_back(s);      //调用左值引用版本的push_back

	lt.push_back("2222"); //调用右值引用版本的push_back
	return 0;
}

加入了forward 后的调试监视的调用逻辑:

成功!

四:默认成员函数的增加

C++11 新增了两个默认成员函数:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
①:
        如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类
型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
如果实现了就调用移动构造,没有实现就调用拷贝构造。
②:
        如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

Q:如何理解写了析构函数 、拷贝构造、拷贝赋值重载中的任何一个,就不会生成移动构造?

A:初看的确觉得这个规则很奇怪,竟然存在没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个的这种奇怪要求,但其实其中是有道理的

        一般显式写了析构或拷贝构造或复制重载的,基本都是需要深拷贝的类,所以析构函数 、拷贝构造、拷贝赋值重载这三者是一体化的,一般你需要写其中一个,那么另外两个都得写 ;所以此时移动构造也是需要手动去写成深拷贝的,这就是为什么你但凡写了那三个中的其中的一个,编译器就不会生成默认的移动构造了,而是需要自己手写移动构造了!

        所以根本不奇怪,因为当一个类需要写析构函数 、拷贝构造、拷贝赋值重载,这时候默认生成的移动构造是没作用的,所以C++干脆不生成了!

理解成:移动构造社恐,不敢见到析构函数 、拷贝构造、拷贝赋值重载

移动赋值类似道理不再赘述~

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

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

相关文章

高效Python开发:uv包管理器全面解析

目录 uv简介亮点与 pip、pip-tools、pipx、poetry、pyenv、virtualenv 对比 安装uv快速开始uv安装pythonuv运行脚本运行无依赖的脚本运行有依赖的脚本创建带元数据的 Python 脚本使用 shebang 创建可执行文件使用其他package indexes锁定依赖提高可复现性指定不同的 Python 版本…

【Linux系统编程】进程属性--进程状态

1.进程的状态 1.1进程的状态在PCB中就是一个变量 一般用宏来定义&#xff0c;例如&#xff1a; #define RUNNING 1 #define BLOCK 2 struct task_struct中的int status 1.2并行和并发 CPU执行代码&#xff0c;不是把进程代码执行完毕&#xff0c;才执行下一个&#xff0…

高精度之加减乘除之多解总结(加与减篇)

开篇总述&#xff1a;精度计算的教学比较杂乱&#xff0c;无系统的学习&#xff0c;且存在同法多线的方式进行同一种运算&#xff0c;所以我写此篇的目的只是为了直指本质&#xff0c;不走教科书方式&#xff0c;步骤冗杂。 一&#xff0c;加法 我在此讲两种方法&#xff1a; …

dify插件接入fastmcp示例

文章目录 1. 使用python完成mcp服务1.1 准备环境&#xff08;python安装fastmcp&#xff09;1.2 mcp服务端示例代码1.3 启动mcp服务端 2. dify接入2.1 安装MCP SSE和 Agent 策略&#xff08;支持 MCP 工具&#xff09; 插件2.2 dify agent插件配置mcp:2.3 mcp服务配置&#xff…

c++——二叉树进阶

1. 内容安排说明 二叉树在前面C数据结构阶段已经讲过&#xff0c;本节取名二叉树进阶是因为&#xff1a; 1. map和set特性需要先铺垫二叉搜索树&#xff0c;而二叉搜索树也是一种树形结构 2. 二叉搜索树的特性了解&#xff0c;有助于更好的理解map和set的特性 3. 二叉树中部…

基于flask+pandas+csv的报表实现

基于大模型根据提示词去写SQL执行SQL返回结果输出报表技术上可行的&#xff0c;但为啥还要基于pandas去实现呢&#xff1f; 原因有以下几点&#xff1a; 1、大模型无法满足实时性输出报表的需求&#xff1b; 2、使用大模型比较适合数据量比较大的场景&#xff0c;大模型主要…

PySide6 GUI 学习笔记——常用类及控件使用方法(常用类字体QFont)

文章目录 一、QFont常用方法二、常用方法总结1. 基础属性设置2. 高级样式控制3. 序列化与反序列化4. 字体信息获取 三、应用实例 字体类QFont用于设置界面控件上显示的字体&#xff0c;它包含字体名称、字体尺寸、粗体字、斜体字、删除线、上划线、下划线、字体间距等属性。 如…

宝塔服务安装使用的保姆级教程

宝塔介绍&#xff1a; 宝塔面板&#xff08;BT Panel&#xff09; 是一款 国产的服务器运维管理面板&#xff0c;主要用于简化 Linux/Windows 服务器的网站、数据库、FTP、防火墙等管理操作。它通过图形化界面&#xff08;Web端&#xff09;和命令行工具&#xff08;bt 命令&a…

Linux平台下SSH 协议克隆Github远程仓库并配置密钥

目录 注意&#xff1a;先提前配置好SSH密钥&#xff0c;然后再git clone 1. 检查现有 SSH 密钥 2. 生成新的 SSH 密钥 3. 将 SSH 密钥添加到 ssh-agent 4. 将公钥添加到 GitHub 5. 测试 SSH 连接 6. 配置 Git 使用 SSH 注意&#xff1a;先提前配置好SSH密钥&#xff0c;然…

Java【网络原理】(5)深入浅出HTTPS:状态码与SSL/TLS加密全解析

目录 1.前言 2.正文 2.1状态码 2.2HTTP与HTTPS的关系 2.3SSL协议 2.3.1对称加密 2.3.2非对称加密 2.3.3中间人攻击 2.3.4校验机制 2.3.4.1证书 2.3.4.2数字签名 1. 数字签名的生成过程 2. 数字签名的验证过程 2.4TLS协议&#xff08;握手过程&#xff09; 3.小结…

【基础IO下】磁盘/软硬链接/动静态库

前言&#xff1a; 文件分为内存文件和磁盘文件。磁盘文件是一个特殊的存在&#xff0c;因为磁盘文件不属于冯诺依曼体系&#xff0c;而是位于专门的存储设备中。因此&#xff0c;磁盘文件存在的意义是将文件更好的存储起来&#xff0c;一边后续对文件进行访问。在高效存储磁盘…

SpringBoot项目容器化进行部署,meven的docker插件远程构建docker镜像

需求&#xff1a;将Spring Boot项目使用容器化进行部署 前提 默认其他环境,如mysql,redis等已经通过docker部署完毕, 这里只讨论,如何制作springboot项目的镜像 要将Spring Boot项目使用docker容器进行部署&#xff0c;就需要将Spring Boot项目构建成一个docker镜像 一、手动…

【小记】excel vlookup一对多匹配

一个学生报四门课&#xff0c;输出每个学生课程 应用概述操作预处理数据计数指令 COUNTIFS进行一对多匹配 vlookup 应用概述 应用场景&#xff1a;学生报名考试&#xff0c;需要整理成指定格式&#xff0c;发给考试院。 一个学生最多报考四门 格式实例&#xff1a;准考证号 …

LeetCode热题100 两数之和

目录 两数之和题目解析方法一暴力求解代码 方法二哈希代码 感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接 &#x1f412;&#x1f412;&#x1f412; 个人主页 &#x1f978;&#x1f978;&#x1f978; C语言 &#x1f43f;️&#x1f43f;️&#x1f43f;…

[春秋云镜] Brute4Road 仿真场景

文章目录 靶标介绍&#xff1a;知识点约束性委派攻击 外网redis主从复制base64提权 内网搭建代理wpcargo插件漏洞mssql弱口令SweetPotato提权远程桌面连接mimikatz抓取hash约束性委派攻击 参考文章 靶标介绍&#xff1a; Brute4Road是一套难度为中等的靶场环境&#xff0c;完成…

鸿蒙系统使用ArkTS开发语言支持身份证阅读器、社保卡读卡器等调用二次开发SDK

har库导入&#xff1a; { "license": "", "devDependencies": {}, "author": "", "name": "entry", "description": "Please describe the basic information.", &qu…

《Python星球日记》 第54天:卷积神经网络进阶

名人说&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。—— 屈原《离骚》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、深度CNN架构解析1. LeNet-5&#xff08;1998&#xff09;2. AlexNet&#x…

《AI大模型应知应会100篇》第53篇:Hugging Face生态系统入门

第53篇&#xff1a;Hugging Face生态系统入门 ——从模型获取到部署的全流程实战指南 &#x1f4cc; 摘要 在人工智能快速发展的今天&#xff0c;Hugging Face已成为自然语言处理&#xff08;NLP&#xff09;领域最具影响力的开源平台之一。它不仅提供丰富的预训练模型、强大…

【基于 LangChain 的异步天气查询2】GeoNames实现地区实时气温查询

目录 功能简介 一、创建GeoNames账号 1、进入官网 2、创建账号 二、运行代码 weather_runnable.py main.py 运行结果 功能简介 本文主要通过Langchain&#xff0c;结合GeoNames实现了地区温度的实时查询&#xff0c;并通过GPT-4o对温度进行一段简短的描述。 一、创建Ge…

服务器数据恢复—硬盘坏道导致EqualLogic存储不可用的数据恢复

服务器存储数据恢复环境&故障&#xff1a; 一台EqualLogic某型号存储中有一组由16块SAS硬盘组建的RAID5阵列。上层采用VMFS文件系统&#xff0c;存放虚拟机文件&#xff0c;上层一共分了4个卷。 磁盘故障导致存储不可用&#xff0c;且设备已经过保。 服务器存储数据恢复过程…