首页 > 编程语言 >c++11 --- 左值与右值的使用;

c++11 --- 左值与右值的使用;

时间:2024-11-07 17:43:59浏览次数:3  
标签:11 string 右值 int 左值 引用 &&

C++98的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,C++11之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名(语法层面上)。

左值和右值

  • 左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址
  • 右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象,匿名对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
  • 左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是leftvalue、right value 的缩写。
  • 现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
int main()
{
	// 左值:可以取地址
	// 以下的p、b、c、*p、s、s[0]就是常见的左值
	int* p = new int(0);
	int b = 1;
	const int c = 1;
	*p = 10;
	string s("11111");
	s[0];

	//查看地址
	cout << &b << endl;
	cout << &(*p) << endl;
	cout << p << endl;
	cout << &p << endl;


	//特殊一点的,默认s[0]地址是整个值
	cout << (void*)&s[0] << endl;
	cout << &s << endl;

	// 右值:不能取地址
	double x = 1.1;
	double y = 2.2;
	// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
	10;					// 常量
	x + y;				//临时变量	
	fmin(x, y);			//是传值返回, 临时变量
	string("111111");	//匿名变量


	//cout << &10 << endl;
	//cout << &(x+y) << endl;
	//cout << &(fmin(x, y)) << endl;
	//cout << &string("11111") << endl;

	return 0;
}

左值引用和右值引用

  • Type& r1 = x; Type&& rr1 = y; 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
  • 左值引用不能直接引用右值,但是const左值引用可以引用右值
  • 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
int main()
{
	// 左值:可以取地址
	// 以下的p、b、c、*p、s、s[0]就是常见的左值
	int* p = new int(0);
	int b = 1;
	const int c = 1;
	*p = 10;
	string s("11111");
	s[0];


	// 右值:不能取地址
	double x = 1.1;
	double y = 2.2;
	// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
	10;					// 常量
	x + y;				//临时变量	
	fmin(x, y);			//是传值返回, 临时变量
	string("111111");	//匿名变量

	// 左值引⽤给左值取别名
	int*& r1 = p;
	int& r2 = b;
	const int& r3 = c;
	string& r4 = s;
	char& r5 = s[0];

	// 右值引⽤给右值取别名
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("11111");

	// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
	const int& rx1 = 10;
	const double& rx2 = x + y;
	const double& rx3 = fmin(x, y);
	const string& rx4 = string("11111");

	// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
	int&& rrx1 = move(b);
	int*&& rrx2 = move(p);
	int&& rrx3 = move(*p);
	string&& rrx4 = move(s);
	string&& rrx5 = (string&&)s;
	return 0;
}
  • template <class T> typename remove_reference<T>::type&& move (T&&arg);
    • move是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及一些引用折叠的知识。

    就是类似于强转

  • 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。

接上面代码 

    // b、r1、rr1都是变量表达式,都是左值
    cout << &b << endl;
    cout << &r1 << endl;
    cout << &rr1 << endl;

	// 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
	int& r6 = r1;
	//int&& rrx6 = rr1;
	int&& rrx6 = move(rr1);

为什么能转换呢?

一切皆指针,无论是什么引用在汇编层都是指针,所以可以相互转换; 

注意:

  • 语法层面看,左值引用和右值引用都是取别名,不开空间。
  • 从汇编底层的角度看下面代码中r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。
  • 底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到一起去理解,互相佐证,这样反而是陷入迷途。
  • 知道使用move,首先强转后,本身类型不变;

  •  但要注意使用,不能随便使用move,move调用的是移动构造,会改变原来的值;

左值和右值的参数匹配
 

  • C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
  • C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用)。

说白了和模板一样,如果有对胃的菜,就选最对胃的;没有才选需要转化的。

  • 右值引用变量在用于表达式时属性是左值,这个设计这里会感觉跟怪,下面讲右值引用的使用场景时,就能体会这样设计的价值了。(左值是可以改变,这很重要)

相关代码:

void f(int& x)
{
	std::cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
	std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}

void f(int&& x)
{
	std::cout << "右值引用重载 f(" << x << ")\n";
}

int main()
{
	/*int&& rr1 = 10;
	int a = 20;
	int& r2 = a;*/

	int i = 1;
	const int ci = 2;

	f(i);			 // 调用 f(int&)
	f(ci);			// 调用 f(const int&)
	f(3);			// 调用 f(int&&),如果没有 f(int&&) 重载则会调用 f(const int&)
	f(std::move(i)); // 调用 f(int&&)

	// 右值引用变量在用于表达式时是左值
	// 右值引用本身的属性是左值
	int&& x = 1;
	f(x); // 调用 f(int& x)
	f(std::move(x)); // 调用 f(int&& x)

	return 0;
}

对应输出 

引用延长生命周期

右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。
 

相关代码

//延长生命周期;
int main()
{
	string s1 = "test1";
	//string&& rr1 = s1;   // 错误:右值引用不能绑定左值

	const string& r1 = s1 + s1;  // OK:到 const 的左值引⽤延⻓⽣存期
	//r1 += 'test';              // 错误:不能通过到 const 的引⽤修改

	string&& rr1 = s1 + s1;	     // OK:右值引⽤延⻓⽣存期
	rr1 += "test";				// OK:能通过到⾮ const 的引⽤修改

	cout << rr1 << endl;
	return 0;
}

 注意:

延长生命周期的场景是什么?

对应同一栈里的,可以利用那两种引用延长临时对象的生命周期;把临时对象的生命周期从那一行延长。

但并不是什么情况下右值引用都能延长生命周期;

 比如:在一个函数中,这个栈中;返回一个深拷贝的对象,为什么不能返回引用呢?因为这个函数结束时,栈析构时;这个引用的内容也会被析构,形成一个类似野指针的东西;

具体例子见 (左值引用主要使用场景回顾);

右值引用和移动语义的使用场景
 

左值引用主要使用场景回顾

 左值引用主要使用场景是在函数中左值引用传参左值引用传返回值减少拷贝,同时还可以修改实参和修改返回对象的值

值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如addStrings和generate函数,C++98中的解决方案只能是被迫使用输出型参数解决。
总的来说:

优点

  1. 左值引用和右值引用最终目的就是减少拷贝构造,提高效率;
  2. 左值引用还能修改参数,返回值的值,方便我们的使用

缺点

  1. 部分返回场景,只能用传值返回,不能用引用返回;
  2. 当前函数局部对象,出了当前函数作用域生命周期的到头了,销毁了,不能引用左值返回,之能传值返回。

class Solution {
public:
	// 传值返回需要拷⻉
	string addStrings(string num1, string num2) {
		string str;
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		// 进位
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;
			str += ('0' + ret);
		}
		if (next == 1)
			str += '1';
		reverse(str.begin(), str.end());
		return str;
	}
};

那有什么解决方案呢?能提高效率呢?

 方案一:不返回值,用输出型参数解决。

有人会想到返回指针,但你想一下,返回指针,但原本部分的数据还是随着函数域到头析构了。 

 但这种牺牲了可读性;

// 方案一:不返回值,用输出型参数解决。
	void addStrings(string num1, string num2, string& s) {

		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		// 进位
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;
			s += ('0' + ret);
		}
		if (next == 1)
			s += '1';
		reverse(s.begin(), s.end());
	}
	// 方案一:不返回值,用输出型参数解决。
	 void generate(int numRows, vector<vector<int>>& vv) {
		 vv.reserve(numRows);
		for (int i = 0; i < numRows; ++i)
		{
			vv[i].resize(i + 1, 1);
		}
		for (int i = 2; i < numRows; ++i)
		{
			for (int j = 1; j < i; ++j)
			{
				vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
			}
		}
	}

方案二:编译器优化 

 右值对象构造,只有拷贝构造,没有移动构造的场景

  • 下图展示了vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,中间为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
  • 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为⼀,变为直接构造。(图最右边)

如何彻底没有优化?

linux下可以将下面代码拷贝到test.cpp⽂件,编译时用 g++ test.cpp -fno-elideconstructors 的方式关闭构造优化,运行结果可以看到图最左边没有优化的两次拷贝。
 

右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景        
 

彻底关闭优化:

展示了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。

最厉害的优化 :

在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造要返回的临时对象str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

方案三:新标准语法处理(左值引用,右值引用) 

  那移动构造的意义是什么呢,所有自定义类型都有移动构造?

 深拷贝定义类型如vector,string,map等等实现移动构造的意义很大,因为移动构造的效率比拷贝构造快的多;直接交换对应内存的内容,比开辟一个再拷贝过去,效率高的多。

浅拷贝定义类型如data,pair<int,int>,实现移动构造的意义其实一般

原因: 

类型分类

引用折叠

  •  C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或 typedef中的类型操作可以构成引用的引用。
  • 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了⼀个引用折叠的规则:右值引用的右值引用折叠成右值引用(仅右+右),所有其他组合均折叠成左值引用;

上面两个知识的代码演示1在下面的程序中很好的展示了模板和typedef时构成引用的引用时的引用折叠规则,大家可以一个一个仔细理解一下。

  • 像f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
  • 万能引用,无论传什么都能接收,且数值类型和其本身一样

Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则;就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。

在代码演示2中存在万能引用

代码演示1:

// 由于引用折叠限定,f1实例化以后总是⼀个左值引用
template<class T>
void f1(T & x)
{}

// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)
{}


int main()
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;

	lref& r1 = n;			// r1 的类型是 int&
	lref&& r2 = n;			// r2 的类型是 int&
	rref& r3 = n;			// r3 的类型是 int&
	rref&& r4 = move(n);	// r4 的类型是 int&&

	//这里学习,加了转换函数的调用表达类型(模板参数)
	//一般实际使用引用折叠是不加的;
	
		
	// 没有折叠->实例化为void f1(int& x)	引用左值
	f1<int>(n);
	//f1<int>(0); // 报错

	// 折叠->实例化为void f1(int& x)	引用左值
	f1<int&&>(n);
	//f1<int&&>(0); // 报错

	// 折叠->实例化为void f1(const int& x)	引用左值
	f1<const int&>(n);
	//f1<const int&>(0);
	f1<const int&>(0);

	// 折叠->实例化为void f1(const int& x)	引用右值
	f1<const int&&>(n);
	f1<const int&&>(0);



	// 没有折叠->实例化为void f2(int&& x)    引用右值
	//f2<int>(n); // 报错
	f2<int>(0);

	// 折叠->实例化为void f2(int& x)		 引用左值
	f2<int&>(n);
	//f2<int&>(0); // 报错

	// 折叠->实例化为void f2(int&& x)		 引用右值
	//f2<int&&>(n); // 报错
	f2<int&&>(0);

	return 0;
}

 代码演示2:

万能引用

//万能引用
template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	//x++;
	cout << &a << endl;
	cout << &x << endl << endl;
}

int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(10);	// 右值

	int a;
	// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
	Function(a);	// 左值

	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(std::move(a)); // 右值



	const int b = 8;
	// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)
	// 所以Function内部会编译报错,x不能++
	Function(b); // const 左值

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
	// 所以Function内部会编译报错,x不能++
		Function(std::move(b)); // const 右值

	return 0;
}

总结: 

那有人会说了,这么麻烦有什么用呢?

实践意义 

list的模拟实现

完美转发

引入:

  • Function(T&& t)函数模板程序(万能引用)中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。
  • 但是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性,就需要使用完美转发实现。

完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的方式实现。

  • 下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回。
  • 传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。

forward 

 底层其实就是强转类型转换

使用的使用要注意的是:

不要在模板参数上加& 传要参数的本身的类型即可;

标签:11,string,右值,int,左值,引用,&&
From: https://blog.csdn.net/2302_80253411/article/details/143370783

相关文章

  • 11.7 html
    html一、基本介绍1、定义:html是一种超文本标记语言,也是一种标识性语言(不是编程语言)标记:记号(绰号)超文本:就是页面内容可以包含图片、链接,音乐,视频等素材。2、为什么学习html?(1)测试页面元素,了解页面页面元素(页面是html语言编写的)(2)进行ui自动化需用到元素定位3、html有哪些特点......
  • 【vjudge训练记录】11月个人训练赛2
    训练情况赛后反思A题看错题导致我红温了,C题数组开小又导致我红温了,D题循环太早结束了,导致小数据没答案,我又红温了,F题刚好越界RE了,我又红温了,G题用string会RE,换成char数组就过了。今天全场都在失误红温。。。A题这题是找\(N\timesN\)的字符矩阵中是否包含\(M\timesM\)......
  • SS241106C. 此时此刻的光辉
    SS241106C.此时此刻的光辉题意给你\(n\)个点,每个点需要被染成\(0/1/2\)三种颜色之一,有\(m\)种限制,每个限制形如【\(u\)不是颜色\(x\)or\(v\)不是颜色\(y\)】,问你满足限制的涂色方案数。思路拜谢dxw。题解提到了FWT是什么东西,好吓人啊。这里是题解的做法一......
  • SS241107B. 子序列们(sub)
    SS241107B.子序列们(sub)题意给你一个仅含有\(0,1\)的序列\(A\),定义序列\(B\)为每个元素是序列\(A\)的一个子序列的序列,满足第\(i\)个元素的长度是\(n-i+1\),且\(\forallj>i\),第\(j\)个元素是第\(i\)个元素的子序列。问有多少种本质不同的序列\(B\),对\(99824......
  • 高薪AI职位的痛,1186万应届生最懂!!
    前言开设AI专业容易,培养人才不易2024年秋招,已临近收尾。近年来,高校毕业生的数量呈现逐年增长的势态,预计2025年应届生高达1186万人,再刷历史新高。这意味着,求职竞争愈发激烈。需要注意的是,虽然多地放宽应届生身份认定标准,近2至3年内毕业的高校生都算,但多数公司的橄榄枝依......
  • Win11计算器 科学模式
    不再盲目按!带你了解Win11计算器各个按键的功用_缩写_三角函数_显示(22条消息)计算器上DEGRADGRAD是什么意思有什么区别?-知乎【MC】memoryclear的缩写,作用是删除记忆按键刚才保存的内容【MR】memoryrecall的缩写,作用是读取记忆按键存储的内容,用来显示按下记忆按键时的......
  • laravel:optimize和clear(laravel11)
    一,optimize创建的文件在哪里?执行optimize:$phpartisanoptimizeINFOCachingframeworkbootstrap,configuration,andmetadata.config................................................................57.67msDONEevents.................................
  • 题解:P11248 [GESP202409 七级] 矩阵移动
    题目传送门题目大意给出一个nnn行mmm列的只包含0、1、?的矩......
  • 对象优化及右值引用优化(一)
    对象优化及右值引用优化对象的函数调用时机classTest{public:Test(intval=0):val_(val){cout<<"Test::Test"<<endl;}~Test(){cout<<"Test::~Test"<<endl;}Test(constTest&t......
  • win11中使用docker-nacos连接容器中的mysql实例记录
     二.方式11.拉取nacosdockerpullnacos/nacos-server2.在dockerdesktop中进行配置如下图相比较’方式2‘这种方式更简单,mysqlip地址需要使用ipv4地址,具体的自己查看ipconfig的ipv4地址(注意:localhsot/127.0.0.1/容器名称都是不行的)下面这几个参数在application.proper......