c++学习笔记(13)-左值和右值

本文详细介绍了C++中的左值、右值、左值引用和右值引用的概念,以及它们在内存管理和资源分配中的作用。特别强调了std::move在移动对象和优化效率中的应用,以及转移构造函数和转移赋值的操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、左值与右值

什么是左值和右值?

  • 左值(lvalue):在内存中有确定存储地址、有变量名,表达式结束后依然存在的值。简单来说,左值就是非临时对象。
  • 右值(rvalue):在内存中没有确定存储地址、没有变量名,表达式结束后就会被销毁的值。简单来说,右值就是临时对象。
int a = 0;  // 在这条语句中,a 是左值,0 是临时值(右值)。

左值可以分为两类:非常量左值常量左值

int a = 10;             //a  为非常量左值(有确定存储地址,也有变量名)
const int a1 = 10;      //a1 为常量左值(有确定存储地址,也有变量名)
const int a2 = 20;      //a2 为常量左值(有确定存储地址,也有变量名)

右值也可以分为两类:纯右值(prvalue)将亡值(xvalue)

  • 纯右值:基本类型的字面量、临时对象(如函数返回的临时对象)等
  • 将亡值:通过std::move转换的对象、返回对象的右值引用的函数调用等
int a = 10;              
const int a1 = 10;      
const int a2 = 20;       // 10,20为纯右值(prvalue)
a1 + a2                  // (a1 + a2) 为纯右值(prvalue)
std::move(a)             // 将非常量左值 a 转换为将亡值(xvalue)

二、左值引用于右值引用

什么是左值引用与右值引用?

  • 左值引用:绑定到左值的引用,通过&来声明。
  • 右值引用:绑定到右值的引用,通过&&来声明。
左值引用示例
int a = 10;           
const int a1 = 10;        
const int a2 = 20;       

//非常量左值引用
int &b1 = a;            //正确,a是非常量左值,可以绑定到非常量左值引用
int &b2 = a1;           //错误, a1是常量左值,不能绑定到非常量左值引用
int &b3 = 10;           //错误,10是纯右值,不能绑定到非常量左值引用
int &b4 = a1+a2;        //错误,(a1+a2)是纯右值,不能绑定到非常量左值引用

//常量左值引用
const int &c1 = a;      //正确,a是非常量左值,可以绑定到常量左值引用
const int &c2 = a1;     //正确, a1是常量左值,可以绑定到常量左值引用
const int &c3 = a+a1;   //正确,(a+a1)是纯右值,可以绑定到常量左值引用
const int &c4 = a1+a2;  //正确,(a1+a2)是纯右值,可以绑定到常量左值引用

总结

  • 非常量左值引用只能绑定到非常量左值
  • 常量左值引用可以绑定到所有类型的值(非常量左值、常量左值、右值)
右值引用示例
int a = 10;            
const int a1 = 10;     
const int a2 = 20;      

//非常量右值引用
int &&b1 = a;            //错误,a是非常量左值,不能绑定到非常量右值引用
int &&b2 = a1;           //错误, a1是常量左值,不能绑定到非常量右值引用
int &&b3 = 10;           //正确,10是纯右值,可以绑定到非常量右值引用
int &&b4 = a1+a2;        //正确,(a1+a2)是纯右值,可以绑定到非常量右值引用

//常量右值引用
const int &&c1 = a;      //错误,a是非常量左值,不能绑定到常量右值引用
const int &&c2 = a1;     //错误, a1是常量左值,不能绑定到常量右值引用
const int &&c3 = a+a1;   //正确,(a+a1)是纯右值,可以绑定到常量右值引用
const int &&c4 = a1+a2;  //正确,(a1+a2)是纯右值,可以绑定到常量右值引用

总结

  • 右值引用(无论是否为常量)只能绑定到右值
  • 右值引用主要用于移动语义和完美转发
    image.png
std::move的使用

从上述可以发现,常量左值引用可以绑定到右值上,但右值引用不能绑定任何类型的左值,若想利用右值引用绑定左值该怎么办呢?

  • C++11中提供了一个标准库move函数获得绑定到左值上的右值引用,即直接调用std::move告诉编译器将左值像对待同类型右值一样处理,但是被调用后的左值将不能再被使用。

std::move用于将左值强制转换为右值引用,从而可以调用移动构造函数或移动赋值运算符

int a = 10;                 
const int a1 = 10;        

//非常量右值引用
int &&d1 = std::move(a);     //正确,将非常量左值a转换为将亡值(xvalue),可以绑定到非常量右值引用
int &&d2 = std::move(a1);    //错误,将常量左值a1转换为将亡值(xvalue),不能绑定到非常量右值引用

//常量右值引用
const int &&c1 = std::move(a);    //正确,将非常量左值a转换为将亡值(xvalue),可以绑定到常量右值引用
const int &&c2 = std::move(a1);   //正确,将常量左值a1转换为将亡值(xvalue),可以绑定到常量右值引用

最后可以发现,编译器利用std::move将左值强制转换为相同类型的右值之后,引用情况跟右值是一模一样的。

三、右值引用与左值引用的区别

  1. 绑定对象
    • 左值引用绑定到有持久存储的左值
    • 右值引用绑定到临时对象(右值)
  2. 生命周期
    • 左值引用延长对象的生命周期
    • 右值引用通常用于 “窃取” 临时对象的资源
  3. 修改能力
    • 非常量左值引用允许修改绑定的对象
    • 右值引用绑定的对象通常是临时的,可以被修改(除非是常量右值引用)

四、引入右值引用的原因

1. 实现移动语义,提高效率

传统的拷贝构造函数需要复制资源,而移动构造函数可以直接转移资源的所有权,避免不必要的拷贝。

某些情况下,需要拷贝一个对象然后将其销毁,如:临时类对象的拷贝就要先将旧内存的资源拷贝到新内存,然后释放旧内存,引入右值引用后,就可以让新对象直接使用旧内存并且销毁原对象,这样就减少了内存和运算资源的使用,从而提高了运行效率

2. 移动含有不能共享资源的类对象

对于管理独占资源(如文件句柄、网络连接、智能指针等)的类,移动语义允许资源的安全转移。

像IO、unique_ptr这样的类包含不能被共享的资源(如:IO缓冲、指针),因此,这些类对象不能拷贝但可以移动。这种情况,需要先调用std::move将左值强制转换为右值,再进行右值引用。

例子如下:
class MyString {
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
    _data = new char[_len + 1];
    memcpy(_data, s, _len);
    _data[_len] = '\0';
}
public:
// 构造函数和拷贝构造函数保持不变

// 移动构造函数
MyString(MyString&& str) noexcept { 
    std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
    _len = str._len; 
    _data = str._data; 
    str._len = 0; 
    str._data = NULL;   // 防止在析构函数中将内存释放掉
}

// 移动赋值运算符
MyString& operator=(MyString&& str) noexcept { 
    std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
    if (this != &str) { 
        delete[] _data;  // 释放当前对象的资源
        _len = str._len; 
        _data = str._data; 
        str._len = 0; 
        str._data = NULL;  // 防止在析构函数中将内存释放掉
    } 
    return *this; 
}

// 析构函数保持不变
};
int main() { 
    MyString a; 
    a = MyString("Hello");  // 调用移动赋值运算符
    std::vector<MyString> vec; 
    vec.push_back(MyString("World"));  // 调用移动构造函数
}

运行结果:

Copy Assignment is called! source: Hello
Destructor is called!
Copy Constructor is called! source: World
Destructor is called!
Destructor is called!
Destructor is called!

总共执行了2次拷贝,MyString(“Hello”)和MyString(“World”)都是临时对象,临时对象被使用完之后会被立即析构,在析构函数中free掉申请的内存资源。 如果能够直接使用临时对象已经申请的资源,并在其析构函数中取消对资源的释放,这样既能节省资源,有能节省资源申请和释放的时间。 这正是定义转移语义的目的。

通过加入定义转移构造函数和转移赋值操作符重载来实现右值引用(即复用临时对象):

MyString(MyString&& str) { 
    std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
    _len = str._len; 
    _data = str._data; 
    str._len = 0; 
    str._data = NULL;   // ! 防止在析构函数中将内存释放掉
}

MyString& operator=(MyString&& str) { 
    std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
    if (this != &str) { 
        _len = str._len; 
        _data = str._data; 
        str._len = 0; 
        str._data = NULL;  // ! 防止在析构函数中将内存释放掉
    } 
    return *this; 
}

运行结果:

Move Assignment is called! source: Hello
Move Constructor is called! source: World
Destructor is called!
Destructor is called!

需要注意的是:右值引用并不能阻止编译器在临时对象使用完之后将其释放掉的事实,所以转移构造函数和转移赋值操作符重载函数 中都将_data赋值为了NULL,而且析构函数中保证了_data != NULL才会释放。

关键点总结

  1. 左值与右值:核心区别在于生命周期和是否有名称
  2. 左值引用与右值引用
    • 左值引用绑定到左值
    • 右值引用绑定到右值
  3. 移动语义:通过右值引用实现资源的高效转移
  4. **std::move**:将左值转换为右值引用,触发移动语义

移动语义是 C++11 最重要的特性之一,它显著提高了处理临时对象的效率,特别是在容器操作和资源管理方面。

<think>好的,我现在需要帮助用户理解C++中的左值的概念、区别以及它们的关系。首先,我需要回顾一下自己对这些概念的理解,确保准确无误。根据用户提供的引用内容,左值主要与表达式的类别有关,左值是可以被赋的,而通常用于计算。 首先,我应该从基本定义入手。左值通常指那些有明确内存地址的对象,可以出现在赋语句的侧,比如变量或解引用的指针。而则是临时的,没有持久的内存地址,比如字面量或临时对象。不过,C++11之后引入了引用,允许被移动,这可能会让概念变得更复杂,需要特别注意。 接下来,我需要区分它们的特性。比如,左值有持久性,可以取地址,而通常是短暂的。例如,表达式`int a = 5;`中,`a`是左值,`5`是。另外,函数的返回如果是非引用类型,通常也是,但如果是返回左值引用,则可能是左值。这部分可能需要举例说明,以帮助用户理解。 然后,用户提到的关系部分,我需要说明左值可以转换为,比如在表达式中使用左值时,会隐式转换为进行计算。而引用允许绑定到引用,从而支持移动语义,避免不必要的拷贝,提高性能。这里可能需要提到std::move的作用,以及如何将左值转换为引用。 在区别方面,需要强调可寻址性、生命周期、使用场景等。例如,左值可以出现在赋边,而不能。同时,引用主要用于移动构造移动赋,优化资源管理。 用户提供的引用中提到了C++11及之后的现代C++特性,所以必须涵盖引用移动语义的内容。需要解释移动语义如何工作,比如通过移动构造函数转移资源所有权,而不是深拷贝,从而提升效率。 此外,用户可能需要知道如何在实际代码中应用这些概念,比如何时使用std::move,或者如何避免不必要的拷贝。不过用户的问题主要集中在概念、关系区别上,所以可能不需要深入代码示例,但适当的例子有助于理解。 需要确保回答结构清晰,分点列出,比如用编号的方式分别讲解左值的定义、区别、关系以及现代C++中的发展。这样用户能够逐步理解每个部分。 可能还需要检查是否有遗漏的关键点,比如将亡(xvalue)在C++11中的分类,但根据用户的引用内容,可能不需要深入到类别的细节(如prvalue、xvalue等),但可以简要提及引用的引入对类别的影响。 最后,生成的相关问题需要与主题相关,比如左值引用的用途、引用的应用场景、移动语义的作用等,这些都能帮助用户进一步学习。 总结一下,回答的结构应该是: 1. 左值的定义及示例 2. 核心区别(可寻址性、生命周期、使用场景) 3. 相互关系(隐式转换、引用) 4. 现代C++中的发展(移动语义、std::move) 5. 相关问题 需要确保使用中文,数学公式用正确的LaTeX格式,行内公式用$...$,独立公式用$$...$$。同时,引用用户提供的参考文献,在适当的位置添加引用标记,如[^1][^2]。 现在需要组织语言,确保每个部分逻辑连贯,用词准确,避免技术错误。同时,生成的问题要覆盖用户可能感兴趣的扩展方向,比如应用场景、性能优化等。</think>### C++ 左值 vs :概念、区别与关系 #### **1. 基本定义** - **左值(lvalue)** 左值表示一个 **有明确内存地址的对象**,可以出现在赋操作的侧。例如变量、返回左值引用的函数、解引用指针等。 **示例**: ```cpp int a = 10; // a 是左值 int* p = &a; // &a 合法,左值可获取地址 ``` - **(rvalue)** 表示 **临时对象或字面量**,通常没有持久的内存地址,不能出现在赋侧。例如字面量、算术表达式结果、返回非引用类型的函数等。 **示例**: ```cpp int b = 5; // 5 是 int c = a + b; // (a + b) ``` #### **2. 核心区别** | 特性 | 左值 | | |--------------------|-------------------------------|-------------------------------| | **可寻址性** | 有明确地址(如 `&x` 合法) | 无地址(如 `&(x+1)` 非法) | | **生命周期** | 持久(超出作用域才销毁) | 临时(表达式结束后销毁) | | **赋操作** | 可出现在 `=` 侧 | 不可出现在 `=` 侧 | | **典型示例** | 变量、数组元素 | 字面量、临时对象、表达式结果 | #### **3. 相互关系** 1. **隐式转换** 左值可隐式转换为(例如在表达式中使用变量): ```cpp int x = 10; int y = x + 5; // x 转换为参与计算 ``` 2. **引用(C++11 引入)** 引用(`&&`)允许绑定到临时对象,支持 **移动语义**,避免不必要的拷贝: ```cpp std::string s1 = "Hello"; std::string s2 = std::move(s1); // s1 的资源被移动到 s2 ``` 此处 `std::move` 将左值 `s1` 转换为引用,触发移动构造函数。 #### **4. 现代 C++ 中的发展** - **移动语义**:通过引用直接“窃取”临时对象的资源,提升性能(如 `std::vector` 的移动操作)。 - **完美转发**:结合 `std::forward` 保留参数的类别,实现泛型编程。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值