左值右值

     左值(lvalue)和右值(rvalue)是编程中两个非常基本的概念,但是也非常容易让人误解,看了很多文章,自我感觉真正将这个问题讲的很透彻的文章还没有看见,所以自告奋勇来尝试一下。如果左值右值的概念不是非常清楚的话,它们迟早会像拦路虎一样跳出来,让你烦心不已,就像玩电脑游戏的时候每隔一段时间总有那么几个地雷考验你的耐性,如果一次把所有地雷扫尽就好了。:)

     左值(lvalue)和右值(rvalue)最先来源于编译理论(感谢南大小百合的programs)。在C语言中表示位于赋值运算符两侧的两个值,左边的就叫左值,右边的就叫右值。比如:

    int ii = 5;//ii是左值,5是右值

    int jj = ii;//jj是左值,ii是右值

    char str[] = "hello, world"; // str is a  lvalue"hello, world" is a rvalue

上面表明,左值肯定可以作为右值使用,但反之则不然。

左值和右值的最早区别就在于能否改变。左值是可以变的,右值不能变。【注1】

注1:这一点在C++中已经猪羊变色,不再成立。拱猪游戏还是挺好玩的,我还真抓过好几次全红心,不过真的好险。:)

 

在很多文章中提到,在C++中,左值更多的指的是可以定位,即有地址的值,而右值没有地址。【注2】

注2:这一点仍然不准确,我在程序中生成一个临时右值std::vector(),你能够说它没有地址吗?难道它是没有肉体的鬼魂或幽灵?它是有地址的,而且它也是绝对的右值。

在现代C++中,现在左值和右值基本上已经失去它们原本所具有的意义,对于左值表达式,通过具体名字和引用(pointer or reference)来指定一个对象。非左值就是右值。我来下一个定义:

左值表示程序中必须有一个特定的名字引用到这个值。

右值表示程序中没有一个特定的名字引用到这个值

跟它们是否可以改变,是否在栈或堆(stack or heap)中有地址毫无关系。

所以也可以这么说:左值是能够取得地址的,右值是不能获得地址而不是没有地址。

1.左值

在下面的代码中:

int ii = 5;

int const jj = ii;

int a[5];

a[0] = 100;

*(a+3) = 200;

int const& max( int const& a, int const& b ) //call by reference

{

     return a > b ? a : b;

}

int& fun(int& a) //call by reference

{

    a += 5;

    return a;

}

ii,jj,a[0],*(a+3),还有函数max的返回值比如max(ii, jj),【注3】函数fun的返回值fun(ii)都是左值。,它们都是有特定的引用名字的值。ii,jj,a[0],*(a+3),max(ii, jj),fun(ii)分别就是它们的名字。

注3:在这里有一个不太容易分清楚的盲点。那就是有人会问max(8, 9)到达是左值还是右值,C++标准规定常量引用(reference to const)可以引用到右值,所以max(8, 9)似乎应该是右值,不过不管它是左值,还是右值,我们都不能试图去改变它。为了与前面的概念一致,我认为它是左值,不可改变的常量左值。

左值有不能改变的,即被const所修饰的左值,比如上面的jj,max(ii, jj)都是被常量(const)魔咒所困住的左值。

没有被const困住的左值当然是可以改变的,比如下面的代码都是成立的:

ii = 600;

a[0] = 700;

fun(ii) = 800; //OK!

我们的眼睛没有问题,fun(ii) = 800;完全正确,因为它是可以改变的左值。所以我们看STL的源码,就会理解std::vector中的重载operator[]运算符的返回值为什么要写成引用,因为operator[]必须返回左值。

 

们的眼睛没有问题,fun(ii) = 800;完全正确,因为它是可以改变的左值。所以我们看STL的源码,就会理解std::vector中的重载operator[]运算符的返回值为什么要写成引用,因为operator[]必须返回左值。

 

2.右值

没有特定名字的值是右值。先看下面的代码:

std::list();

std::string(“It is a rvalue!”);

int fun1() //call by value

{

  …

}

int* fun2() //call by value

{

  …

}

     其中std::list(),std::string(“It is a rvalue!”),函数fun1的返回值fun1(),函数fun2的返回值fun2()都是右值,它们的值都没有特定的名字去引用。也许有人会奇怪,fun2()也是右值?最前面的max(a,b)不是左值吗?

    请看清楚,函数fun2的返回值是pointer,pointer也是call by value,而函数max的返回值是reference,reference是call by reference。所以说C++中引入reference不仅仅是为了方便,它也是一种必须。【注4】

注4:Scott Meyer写的《More Effective C++》的条款1专门讲了pointer和reference的区别,写的很好,辨别的非常清楚。

fun2()是右值,但 *fun2()却是左值,就跟经常看到的*p一样,所以看C++库代码的时候,会发现重载operator*的函数返回值是reference。

    当然我还遗漏了一种右值,那就是字面上的(literal)值,比如5,8.23,’a’等等理所当然的都是右值。

 

    右值最初出现的时候,一个最大的特征就是不可改变。但就跟我们的道德标准一样,时代不同了,标准也变化了,以前的三纲五常早已经被扔到历史的垃圾堆里面了。

    C++中有可以改变的右值,而且这个特性还非常有用。那就是用户自定义的类(class)的构造函数生成的临时对象。比如:

std::vector(9),std::deque(),……都是可以改变的右值。在Herb Sutter的《More Exceptional C++》中的条款7的page51页有这样几行代码:

vector<Customer> c( 10000 );

c.erase( c.begin()+10, c.end() );

vector<Customer>( c ).swap( c );

认真看几遍,你会发现但vector的大小增大到一定程度,你又用不着这么多空间的时候,你会想办法把它收缩到最合适的大小,但利用别的办法比如调用成员函数reserve()都无法办到,这个时候就必须利用右值可以改变这个性质了。

vector<Customer>( c ).swap( c );这行代码就是点睛之处。

首先使用复制构造函数生成临时右值vector<Customer>( c ),这个右值正好是合适大小,然后和c交换【注5】,c就变成合适大小了,最后在整个表达式结束的时候,这个临时右值析构归还内存空间。真是绅士一般的优雅!

注5:这个时候这个临时右值就发生了改变。

 

如果还不理解,可以看看书,或者直接看库的源代码。

    至于为什么会这样?我思考了一下,我想是这样的,我们看类(class)的数据布置结构,会发现它的每一个数据成员都是有名字的,我想编译器在编译的过程中,都会生成一个外部不所知的对这个临时对象右值的名字引用,但需要改变这个临时对象的时候,这个名字就用上了。比如:

class Point

{

public: //纯粹为了方便,我把数据成员公开,现实中尽量不要这样用

  int x, y ,z;

  ……//其他各种成员函数

};

我们现在就可以改变右值,用到了匿名的引用名字。

Point().x = 6;//改变了右值

Point().y = 6;//同意改变了右值,不过注意,这个右值跟上面的不是同一个。

 

总结

左值和右值的真正区别我想就是这些了,左值表示有特定的名字引用,而右值没有特定的名字引用。当然我仍然会有疏忽,希望大家能够提醒我,指正我的不足。

本文来自:http://amyz.itpub.net/post/34151/411832

<think>好的,我现在需要准备关于的讲解。首先,我得回想一下自己对这些概念的理解,确保准确无误。是C++中的概念,对吗?我记得指的是可以取地址的、有持久性的表达式,而则是临时的、即将被销毁的。不过,具体的区别和应用场景可能需要进一步理清。 首先,我应该从基本定义入手。(lvalue)通常指的是可以出现在赋语句侧的表达式,比如变量、对象等,它们有明确的内存地址。而(rvalue)则通常出现在赋语句的侧,是临时生成的、无法取地址的,比如字面量、临时对象或者表达式计算结果。 接下来,我需要考虑C++11引入的引用和移动语义,这可能和的现代用法有关。引用(用&&表示)允许我们绑定到,从而实现资源的转移而非复制,提升效率。例如,移动构造函数和移动赋运算符就是利用引用来避免不必要的深拷贝。 用户可能需要了解的区别,以及如何在实际编程中应用这些概念。比如,函数参数传递、返回优化、完美转发等场景。这时候应该举一些例子,说明哪些表达式是,哪些是,以及如何通过类型转换(如std::move)将转换为引用。 需要注意的是,有时候也可以被当作使用,特别是在使用std::move时。这时候原本的会被标记为,允许其资源被移动而非复制。不过,之后访问该可能会有未定义行为,需要谨慎处理。 可能还需要提到引用和引用的区别。引用用&声明,只能绑定到引用用&&声明,只能绑定到。但如果有const引用,也可以绑定到,这是例外情况。例如,const int& a = 5;是合法的,因为常量引用可以绑定到。 另外,模板推导中的引用折叠规则和完美转发(std::forward)也是基于的概念。当使用通用引用(T&&)时,根据传入的实参是还是,T会被推导为不同的类型,从而实现完美转发,保持参数的类别。 不过,在讲解的时候需要避免过于深入复杂的模板元编程,保持基础而清晰的解释。应该用简单的代码示例来说明,比如: int a = 5; // a是 int b = a; // a是,作为使用 int c = std::move(a); // std::move将a转换为引用,允许移动 另外,函数返回的例子:当函数返回一个临时对象时,该返回。如果返回的是局部变量,在C++11之后可能会被当作,从而触发移动语义而非拷贝。 可能存在的误区是认为引用就是,实际上引用本身是,因为它有名字,可以取地址。例如: void f(int&& x) { // x在这里是,因为它有名字 int y = x; // 这里是拷贝,不是移动 int z = std::move(x); // 需要再次使用std::move转换为 } 这里需要强调,即使参数是引用,但在函数内部使用时,它被视为,因此需要显式使用std::move来启用移动语义。 总结一下,讲解的结构应该是: 1. 的定义与区别 2. 引用和引用的语法与用法 3. 移动语义和std::move的作用 4. 实际应用示例和注意事项 5. 相关的高级主题(如完美转发)的简要提及 在写作过程中,要确保例子正确,术语准确,并且语言通俗易懂,适合不同层次的读者理解。需要检查是否有错误,比如混淆了引用和本身,或者对std::move的作用描述不准确。另外,要注意中文表达流畅,避免直译英文术语导致的生硬。</think>### (Lvalue)与(Rvalue)讲解 #### 1. **基本定义** - **(Lvalue)** 指有明确内存地址、可被标识的表达式,通常可出现在赋语句的侧。 **特点**:持久性,可多次使用。 **示例**:变量、对象、返回引用的函数调用。 ```cpp int a = 10; // a 是 int* p = &a; // 可对 a 取地址 a = 20; // 可赋 ``` - **(Rvalue)** 指临时的、无持久性的表达式,通常只能出现在赋语句的侧。 **特点**:短暂性,用完即销毁。 **示例**:字面量、临时对象、表达式计算结果。 ```cpp int b = 5; // 5 是(字面量) int c = a + b; // a + b 的结果是 ``` #### 2. **引用与引用** - **引用(`&`)** 只能绑定到,用于修改或延长的生命周期。 ```cpp int x = 10; int& ref = x; // 正确:引用绑定到 // int& ref = 5; // 错误:引用不能绑定到 ``` - **引用(`&&`)** 只能绑定到,用于实现移动语义和完美转发。 ```cpp int&& rref = 10; // 正确:引用绑定到 // int&& rref = x; // 错误:引用不能绑定到 ``` **例外**:`const` 引用可绑定到。 ```cpp const int& cref = 5; // 合法 ``` #### 3. **移动语义与 `std::move`** - **移动语义** 通过引用“窃取”临时对象的资源,避免深拷贝,提升效率。 ```cpp class String { public: // 移动构造函数 String(String&& other) { data = other.data; // 直接转移资源 other.data = nullptr; // 置空原对象 } }; ``` - **`std::move` 的作用** 将强制转换为引用,触发移动语义。 ```cpp std::vector<int> v1 = {1, 2, 3}; std::vector<int> v2 = std::move(v1); // 移动构造,v1 变为空 ``` **注意**:使用 `std::move` 后,原对象状态不确定,需避免再次访问。 #### 4. **实际应用示例** - **场景1:函数参数传递** ```cpp void process_copy(const std::string& s); // 接受(拷贝) void process_move(std::string&& s); // 仅接受(移动) std::string str = "Hello"; process_copy(str); // 传递,触发拷贝 process_move(std::move(str)); // 传递,触发移动 ``` - **场景2:返回优化(RVO)** ```cpp std::string create_string() { return "Temp"; // 返回,可能触发移动或直接构造 } std::string s = create_string(); // 无需拷贝 ``` #### 5. **注意事项** - **引用本身是** ```cpp void f(int&& x) { int y = x; // x 是(有名字),触发拷贝 int z = std::move(x); // 需显式转换为 } ``` - **避免滥用 `std::move`** 仅在需要转移资源时使用,否则可能导致意外行为。 #### 6. **高级主题(简要提及)** - **完美转发(`std::forward`)** 保留参数的类别(/),常用于泛型编程。 ```cpp template<typename T> void wrapper(T&& arg) { // 保持 arg 的原始类别(func(std::forward<T>(arg)); } ``` --- ### 总结 - ****:有地址、可重复使用的表达式。 - ****:临时、一次性使用的表达式。 - **引用**:实现移动语义,提升性能。 - **关键操作**:`std::move` 转换,`std::forward` 保持类别。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值