C++中的左值与右值问题的由来以及应用和意义

左值和右值的由来和定义

由来

左值和右值的概念最早是由CPL程序语言【20世纪60年代的古董】的一篇论文引入的,在B语言中作为文法元素被明确,在C语言中文法不再出现左值和右值的区别,这一概念被保留在语义规则之中。右值跟值变成了同义词。
C++继承了这一概念,并且加以利用,并在C++11标准中引入了左值引用和右值引用的特性。

定义

  • 最早版本:
    • 左值:拥有内存地址,可以被赋值的表达式或者变量,即放在赋值符号左边的表达式,故称为左值
    • 右值:不可被修改,只能读值的表达式,即:放在赋值符号右边的表达式,故称为右值
  • C语言版本:由于C语言引入了const修饰词,const对象拥有内存地址,可以读值但是不能赋值即放在赋值符号左边。所以把左值右值定义更新了
    • 左值:拥有内存地址的表达式
    • 右值:不拥有内存地址的表达式
const int a = 10; // a 是左值,但是是“不可修改的左值”
int b = 10; // b 是可修改的左值,如果可修改的左值是struct或者Union,那么应该递归地都不含有const修饰的类型
//这其中的10就是右值
  • C++版本:C++对于左值和右值的扩展较大,由于C++中引入了操作符重载,如果给一个自定义类型定义了赋值符号,那么这个右值也可以放在赋值符号左边。
    • 因此,C++03标准把具有标识(identity)的表达式规定为左值,不具有标识的表达式规定为右值。
      • 名字、指针、引用等是左值,是命名对象,具有确定的内存地址;字面量、临时对象等为右值。函数名字是左值(在C语言中规定它既不是左值也不是右值),数组名是常量左值,但是在大多数表达式中函数名字与数组名字自动隐式转换为右值。
    • 在C++11中引入了左值引用和右值引用的概念,移动语义把临时对象的内容移动(move)到左值对象上。因而在C++11,对于值的分类,要考虑标识(identity)与可移动性(movability),二者的组合产生了五种分类:
      • 左值:同上,可以取地址的表达式,但是不能移动
      • 纯右值:同上,不具有标示,可以移动
      • 临终值/将亡值:具有标识,对应的对象接近生存期结束,但其内容尚未被移走。
      • 广义左值glvalue:具有标识。包括左值与临终值。可以多态、动态类型。
      • 右值rvalue:可以移动。包括濒死值与纯右值。不能通过&运算符取地址。
    • &表示左值引用,比较好理解,就是引用整个对象,和这个对象本身拥有相同的权力,&&表示右值引用,右值引用变量绑定到右值上,延长了右值对应的临时对象的生存期。
      • 在C++11之前,是没有右值引用的,只有引用,也就是左值引用。左值引用要求被引用对象必须要能够取指【这是因为其实引用在底层跟指针是一个性质的】如果一个表达式没有地址,但是又想引用他,就只能使用常引用,也就是在前面加一个const修饰符
    //标识,可以理解为地址或者变量名,
    //可移动性,可以理解为是否能神不知鬼不觉偷偷转移走,比如10这个字面量,可以神不知鬼不觉转移走,但是a这个变量就不行
    const int a = 10; //a是左值,10是右值
    int &b = 10; //error ,因为10无法取址
    const int &c = 10; //常引用,为了引用一个值,但是这个值又没法取址,就可以使用常引用,常引用只能读,不能写。
    // 常引用等同于:
    // const temp = 10;
    // const &c = temp;
    int &&var = 10; // 右引用
    

其实以上的都不算是定义,定义是对于一个概念的具体解释和设定,上面的这些不过是对于一个值是左值和右值的判断方法,如果真的要看左值和右值的定义,需要去翻ISO C语言标准手册了
关于移动语义,可以看这篇文章写的很好:详解C++移动语义

左值与右值之间的转换

左值可以隐式转换为右值,但是右值不能转换为左值【但是右值可以赋值给左值,也可以说是显式转换为左值】

int a = 10; //右值显式转换为左值
Aclass a = Aclass(element); // 右值显式转换为左值
// 如果是单纯的int转换,这种显式转换没什么问题
// 但是如果是下面这种,一个临时对象转换为一个左值,那么就需要先进行构造,然后进行拷贝赋值,然后再销毁右边这个临时对象
// 写代码时我们肯定不会这么使用,但是这个小例子是模仿的我们一个函数返回了一个右值对象,然后使用左值对象接收的情况,为了优化这中情况,C++引入了移动语义的概念,也就是移动赋值和移动构造。

这个显式转换的过程并非我们

CV限定问题

这里的CV指的是const 和 volatile这两个限定符,不是Ctrl c和Ctrl v的cv,也不是computer vision的cv【狗头】
每个类型都有三个对应的 CV-限定类型版本: const 限定 、 volatile 限定 和 const-volatile 限定 版本。有或无 CV 限定的不同版本的类型是不同的类型,但写法和赋值需求都是相同的。

在 C 中,只有左值有 CV 限定的类型,而右值从来没有。而在 C++ 中,类右值可以有 CV 限定的类型,但内置类型 (如 int) 则没有。举例如下:

#include <iostream>

class A {
public:
    void foo() const { std::cout << "A::foo() const\n"; }
    void foo() { std::cout << "A::foo()\n"; }
};

A bar() { return A(); }
//返回一个A对象,这个A对象是一个右值
const A cbar() { return A(); }
// 返回一个A对象,这个A对象是个右值,但是可以使用const来修饰

int main()
{
    bar().foo();
    // 聪明的你可以思考一下,上下这两个foo()函数是一个函数吗?
    cbar().foo(); 
}

C++中左值引用和右值引用的用途

  • 左值引用:可以通过左值引用来返回左值,不过左值引用由来已久,其好处就是普通引用的好处。
  • 右值引用:引入右引用概念的目的是为了充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。

左值引用用途代码:

int globalvar = 20;
	int& foo() //使用左值引用让函数的返回一个左值
	{
    	return globalvar;
	}
	int main()
	{
    	foo() = 10;
    	return 0;
	}

右值引用用途代码:

#include<iostream>
using namespace std;
class MyCalss{
public:
	MyClass(int arg): member_(arg) {};
	viod sayhello(){cout<<"I am MyClass"<<member_<<endl;};
private:
	int member_;
};

MyClass func(int arg){
	return MyClass(arg);
}

void func2(MyClass temp){
	temp.sayhello();
}

int main(){
	func2(func(10));
	return 0;
}

如果我们按照上面这种方法写代码,整个代码的流程就是:
进入func调用构造函数,
退出func调用析构函数,
主函数为了保存返回值调用构造函数
返回值进入func2调用构造函数构造形参
退出func2调用析构函数析构形参
func2结束,func1返回值没用了,主函数调用析构函数析构func1返回值
整个过程反复调用构造和析构函数,主要原因就是因为里面的MyClass全部都是作为右值存在,一旦使用完成就析构,才导致的,如果可以延长右值的生命周期,再配合移动语义,就可以避免很多构造析构,

#include<iostream>
using namespace std;
class MyCalss{
public:
	MyClass(int arg): member_(arg) {};
	viod sayhello(){cout<<"I am MyClass"<<member_<<endl;};
private:
	int member_;
};

MyClass && func(int arg){
	return &&MyClass(arg);
}

void func2(MyClass&& temp){
	temp.sayhello();
}

int main(){
	func2(func(10));
	return 0;
}

这样就减少了构造析构的过程

本人对右值引用也没有非常熟悉,上面这个例子可能不是很恰当,毕竟对于一个项目来说,这么点构造析构的时间开销,相对于算法来说可以忽略不计。
C++会为了这么一点时间开销而专门构造出来一个新的概念吗?
网上还要一种说法是为了应付临终值,当上一个函数结束的时候返回的这个值其实是个临终值,为了接受这个值给下一个函数,如果没有右值引用,就需要再在主函数中定义一个临时变量然后把这个临时变量传递给下一个函数,有了右值引用就可以把一个临终值传递给下一个函数,并且右值引用和左值引用可以分别进行重载,互相不影响。
更多应用等到什么时候用到再补充

  • 7
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值