c/c++ 中的左值和右值 理解

简单的定义

1、左值表示了一个占据内存某个可识别的位置(也就是一个地址)的对象
2、右值则使用排除法来定义。 一个表达式不是左值就是右值。那么,右值是一个不表示内存中某个可识别位置的对象的表达式。

举例

上面的术语定义先的有些模糊,那么现在看这一个例子,我们假设定义并赋值了一个整形变量:

int var;
var = 4;

赋值操作需要左操作数是一个左值。var是一个有内存位置的对象,因此它是左值。下面写法错误:

4 = var
(var + 1) = 4;

常量4和表达式var + 1 都不是左值(也就是说,它们是右值),因为它们都是表达式的临时结果,而没有可识别的内存位置(也就是说,只存在于计算过程中的每个临时寄存器中)。因此,赋值给它们是没有任何语义上的意义的

那么,我们理解下第一个代码片段中的错误信息的含义,foo返回的是一个临时的值。它是一个右值,赋值给它是错误的,因此当编译器看到 foo() =2 时,会报错

给函数返回的结果赋值,不一定总是错误的操作。例如C++ 引用可以这样写

int globalvar = 20;
int& foo() {
	return globalvar;
}

int main() {
	foo() = 10;
	return 0;
}

foo函数返回一个引用,引用一个左值,因此可以赋值给它,实际上 函数可以返回左值的功能对实现一些重载的操作符非常重要。 一个比较常见的例子就是说重载括号操作符 [],来实现一些查找访问的操作,如std::map 中的方括号:

std::map<int,float> mymap;
mymap[10] = 5.6;

之所以能赋值给mymap[10],是因为std::map::operator[]的重载返回的一个可赋值的引用。

可修改的左值

左值一开始在C中定义为“可以出现在赋值操作左边的值”。然而,当ISO C假如 const关键字后,这个定义便不成立:

const int a = 10; // 'a' 是左值
a = 10; //但不可以赋值给它!

于是定义需要继续精华。不是所有的左值都可以被赋值。可以赋值的左值被称为 可修改左值。C99标准定义可以修改左值为:

[...]可修改左值是特殊的左值,不含有数组类型、不完整类型、const修饰的类型。
如果它是struct或者union,它的成员都(递归地)不应含有const修饰的类型。

左值与右值间的转换

计算对象的值的语言成分,都是用右值作为参数。例如,两元加法操作符+ 就需要两个右值参数,并返回一个右值。

int a1 = 1; // a1 是左值
int a2 = 2; // a2 是左值
int a3 = a1 + a2; // + 需要右值,所以 a1 和 a2 被转换成了右值

在例子中,a1和a2 都是左值。因此,在第三行中,它们经历了隐式的左值到右值的转换。除了数组、函数、不完整类型的所有左值都可以转换为右值。

那右值转换为左值呢? 当然不行, 不过,右值可以通过一些更显示的方式产生左值。例如,一元解引用操作符* 需要一个右值参数,但返回一个左值结果。考虑这样的代码:

int arr[] = {1,2};
int* p = &arr[0];
*(p+1) = 10; // 正确: p+1是右值,但*(p+1) 是左值

相反,一元取地址操作符&需要一个左值参数,返回一个右值:

int var = 10;
int* bad_addr = &(var + 1); //错误:一元 '&'操作符需要左值参数
int* addr = &var; // 正确:var是左值
&var = 40;	//错误:复制操作的左操作数需要是左值

在C++ 中& 符号 还有另一个功能---- 定义引用类型。引用类型又叫做 “左值引用”。因此,不能将一个右值赋值给(非常量的)左值引用:

// 错误: 非常量的引用'std::string&' 错误地使用右值'std::string'初始化
std::string& sref = std::string(); 

常量的左值引用可以使用右值赋值。因为你无法通过常量的引用修改变量的值,也就不会出现修改了右值的情况。这也使得C++ 中一个常见的习惯成为可能: 函数的参数使用常量引用接收参数,避免创建不必要的临时对象。

CV限定的右值

在C++ 标注草稿的中说到:

一个非函数、非数组的类型T的左值可以转换为右值。[...]如果不是类类型【标注:类类型即C++中使用类定义,区别与内置类型】,
转换后的右值的类型是T的未限定CV的版本。其它情况下,转换后的右值类型就是T本身。

什么叫做“未限定CV”?CV限定符这个术语指的是const和volatile两个类型限定符。C++ 标准的3.9.3节写到:

每个类型都有三个对应的CV-限定类型的版本:const限定、volatile限定和const-volatile限定版本。
有或无CV限定的不同版本的类型是不同的类型,但写法和赋值需求都是相同的。

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

#include <iostream>
class A{
	public: 
		void foo() const {std::cout << "aA::foo() const\n";}
		void foo() {std::cout << "a:foo()\n"};
}

A bar() {return A();}

const A cbar() { return A()}

int main() {
	bar().foo(); // calls foo
	cbar().foor() ; // calls foo const
}

main 函数中的第二个函数调用实际上调用的是A 中的foo() const 函数,因为cbar 返回的类型是const A,这和A是两个不同的类型。这就是上面的引用中最后一句话所表达的意思。另外注意到,cbar的返回值是一个右值,所以这是一个实际的CV限定的右值的例子。

C++的右值引用

这边文章的大部分内容都是在解释: 左值和右值的主要区别是,左值可以被修改,而右值不能,不过,C++11改变了这一区别。在一些特殊的情况下,可以使用右值的引用,并对右值进行修改。

例如实现一个“整数的vector”,如:

class Intvec {
public:
	explicit Intvec(size_t num = 0)
		: m_size(num),m_data(new int[m_size])
		{
			log("constructor");
		}
	~Intvec() {
		log("destructor");
		if(m_data) {
			delete[] m_data;
			m_data = 0;
		}
	}
	Intvec(const Intvec& other)
		:m_size(other.m_size),m_data(new int[m_size]){
			log("copy constructor");
	        for (size_t i = 0; i < m_size; ++i)
	            m_data[i] = other.m_data[i];
		}
	Intvec& operator=(const Intvec& other)
	    {
	        log("copy assignment operator");
	        Intvec tmp(other);
	        std::swap(m_size, tmp.m_size);
	        std::swap(m_data, tmp.m_data);
	        return *this;
	    }
private:
    void log(const char* msg)
    {
        cout << "[" << this << "] " << msg << "\n";
    }

    size_t m_size;
    int* m_data;
}

这样,定义了基本的构造器、析构器、拷贝构造器 和拷贝赋值操作符【注4:拷贝赋值操作符的实现是在考虑异常安全角度的规范写法。结合使用拷贝构造器和不会抛出异常的std::swap,可以保证在异常发生时不会出现未初始化的内存】。它们都有一个 logging 函数,让我们能知道是否调用了它们。

运行一个将v1的内容拷贝到v2的代码:

Intvec v1(20);
Intvec v2;

cout << "assigning lvalue...\n";
v2 = v1;
cout << "ended assigning lvalue...\n";

运行输出的结果是:

assigning lvalue...
[0x28fef8] copy assignment operator
[0x28fec8] copy constructor
[0x28fec8] destructor
ended assigning lvalue...

这是正常的结果,准确展示了 operator= 的内部过程。但假设我们要将一个右值赋值给 v2

cout << "assigning rvalue...\n";
v2 = Intvec(33);
cout << "ended assigning rvalue...\n";

虽然这里的例子中是赋值一个新创建的 vector,但它可以代表更一般的情况——创建了一个临时的右值,然后赋值给 v2 (例如当一个函数返回 vector 的情况)。我们会得到这样的输入:

assigning rvalue...
[0x28ff08] constructor
[0x28fef8] copy assignment operator
[0x28fec8] copy constructor
[0x28fec8] destructor
[0x28ff08] destructor
ended assigning rvalue...

这看起来就要很多步骤,特别是这里调用了额外的一对构造器/析构器,用来创建和销毁一个临时的对象。然而,在拷贝赋值操作符中,也创建和销毁了另一个临时的对象。这完全是多余的没有意义的工作。

移动语义 / 移动赋值(数据剪切)

现在你不需要多一个临时对象了。C++11引入了右值引用,让我们可以实现“移动语义”,特别是可以实现“移动赋值操作符” 如:

Intvec& operator=(Intvec&& other)
{
    log("move assignment operator");
    std::swap(m_size, other.m_size);
    std::swap(m_data, other.m_data);
    return *this;
}

符号 && 代表了新的 右值引用 。右值引用可以让我们创建对右值的引用。而且在调用结束后,右值引用就会被销毁。我们可以利用这个特性将右值的内部内容“偷”过来——因为我们不再需要使用这个右值对象了!这样得到的输出是:

assigning rvalue...
[0x28ff08] constructor
[0x28fef8] move assignment operator
[0x28ff08] destructor
ended assigning rvalue...

由于将一个右值赋值给了 v2,移动赋值操作符被调用。虽然 Intvec(33) 仍然会创建一个临时对象,调用其构造器和析构器,但赋值操作符中的另一个临时对象不会再创建了。这个赋值操作符直接将右值的内部内容和自己的相交换,自己获得右值的内容,然后右值的析构器会销毁自己原先的内容,而这一内容已经不需要了。优雅~~~。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值