• (十)C++中的左值lvalue&右值rvaue



    欢迎访问个人网络日志🌹🌹知行空间🌹🌹


    1.C++中的变量名是如何存储及引用

    int a = 0;
    
    • 1

    如上,在C++中声明一个变量时,可以知道a是一个int类型的变量占用4个字节,其值为0,这里就有个疑问,如果a表示的变量占用了4个字节是其值的内存空间,那么a本身存储在哪里了呢?

    在知乎上有这个问题,下面的回答内容也很好。简单来说就是,对于C/C++这种需要预处理/编译/汇编/链接的翻译成机器代码的语言,变量名不需要储存,只是为了方便程序员编程,在编译器编译时会确定每个变量的地址,所有的局部变量读写都会变成(栈地址 + 偏移)的形式。

    int main() {
        int a = 0;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4

    得到对应汇编代码如下:

    (gdb) disassemble /m main
    Dump of assembler code for function main:
       0x0000555555555129 <+0>:	    endbr64 
       0x000055555555512d <+4>:	    push   %rbp
       0x000055555555512e <+5>:	    mov    %rsp,%rbp
       0x0000555555555131 <+8>:	    movl   $0x1,-0x4(%rbp)
       0x0000555555555138 <+15>:    mov    $0x0,%eax
       0x000055555555513d <+20>:    pop    %rbp
       0x000055555555513e <+21>:    retq   
    End of assembler dump.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可见只有指令,并没有变量的声明。当使用引用时,

    int main() {
        int a = 0;
        int &b = a;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    得到对应汇编代码如下:

    (gdb) disassemble /m main
    Dump of assembler code for function main:
       0x0000000000001149 <+0>:	    endbr64 
       0x000000000000114d <+4>:	    push   %rbp
       0x000000000000114e <+5>:	    mov    %rsp,%rbp
       0x0000000000001151 <+8>:	    sub    $0x20,%rsp
       0x0000000000001155 <+12>:    mov    %fs:0x28,%rax
       0x000000000000115e <+21>:    mov    %rax,-0x8(%rbp)
       0x0000000000001162 <+25>:    xor    %eax,%eax
       0x0000000000001164 <+27>:    movl   $0x1,-0x14(%rbp)
       0x000000000000116b <+34>:    lea    -0x14(%rbp),%rax
       0x000000000000116f <+38>:    mov    %rax,-0x10(%rbp)
       0x0000000000001173 <+42>:    mov    $0x0,%eax
       0x0000000000001178 <+47>:    ov    -0x8(%rbp),%rdx
       0x000000000000117c <+51>:    xor    %fs:0x28,%rdx
       0x0000000000001185 <+60>:    je     0x118c <main+67>
       0x0000000000001187 <+62>:    callq  0x1050 <__stack_chk_fail@plt>
       0x000000000000118c <+67>:    leaveq 
       0x000000000000118d <+68>:    retq   
    End of assembler dump.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    当使用指针时,

    int main() {
        int a = 0;
        int *b = &a;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    得到对应汇编代码如下:

    (gdb) disassemble /m main
    Dump of assembler code for function main:
       0x0000000000001149 <+0>:	    endbr64 
       0x000000000000114d <+4>:	    push   %rbp
       0x000000000000114e <+5>:	    mov    %rsp,%rbp
       0x0000000000001151 <+8>:	    sub    $0x20,%rsp
       0x0000000000001155 <+12>:	mov    %fs:0x28,%rax
       0x000000000000115e <+21>:	mov    %rax,-0x8(%rbp)
       0x0000000000001162 <+25>:	xor    %eax,%eax
       0x0000000000001164 <+27>:	movl   $0x1,-0x14(%rbp)
       0x000000000000116b <+34>:	lea    -0x14(%rbp),%rax
       0x000000000000116f <+38>:	mov    %rax,-0x10(%rbp)
       0x0000000000001173 <+42>:	mov    $0x0,%eax
       0x0000000000001178 <+47>:	mov    -0x8(%rbp),%rdx
       0x000000000000117c <+51>:	xor    %fs:0x28,%rdx
       0x0000000000001185 <+60>:	je     0x118c <main+67>
       0x0000000000001187 <+62>:	callq  0x1050 <__stack_chk_fail@plt>
       0x000000000000118c <+67>:	leaveq 
       0x000000000000118d <+68>:	retq   
    End of assembler dump.
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    是的,没有看错,使用指针和引用得到了相同的汇编代码,

    在这里插入图片描述

    同样可以比较,函数里面用引用传参和指针传参,生成的汇编代码也是一样的。

    // by pointer
    void func1(int &a){}
    int main() {
        int b = 0;
        int &a = b;
        func1(a);
        return 0;
    }
    
    // by reference
    void func1(int *a){}
    int main() {
        int b = 0;
        int *a = &b;
        func1(a);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    因此,从汇编的角度来看,引用跟指针实际上就是同一个东西。引用和指针,更多的是编程语言语法方面的设计,也就是由编译器搞出来的概念,实际上它们最后生成的汇编代码是一样的。而只所以引入引用,据说是为了加运算符重载,没有引用的话,前自增 operator ++(class T)的语义就难以说明清楚。必须在声明引用的同时就要对它初始化,并且,引用一经声明,就不可以再和其它对象绑定在一起了,这和指针常量int * const p有极大的相似之处。引用的一个优点是它一定不为空,因此相对于指针,它不用检查它所指对象是否为空,这增加了效率

    2.C++中的左值与右值

    左值和右值是C++中的基本概念,简单来说,左值就是一个表达式等号左边的部分,右值就是等号右边的部分。如:

    int a = 10; // a 是左值,10是右值
    int b = a;  // b 是左值,a自动转为右值
    
    • 1
    • 2

    如上,其实左值表示的是对象的引用,而右值正是被左值指向的对象。像变量名/数组下标/返回引用类型函数的返回值等都是左值,左值有对应确定的存储区域,因此可以取其地址。

    而像字符串/数字/运算符或函数的运算结果都是右值,右值不需要在内存中存储,只是程序执行中的中间结果,无法寻址。

    只有左值才能用在赋值表达式的左边,而右值必须和一个表达式的逻辑对应,因此只能存在于赋值表达式右边。像取地址符&/自增运算符++/自减运算符--都需要左值作为其参数。

    • 返回引用的函数的调用产生的是左值,返回值的函数调用产生的是右值

      #include 
      #include 
      
      void printStr(std::string &s) {
          printf("%s\n", s.c_str());
      }
      
      std::string getValueString() {
          std::string s = "getValueString";
          return s;
      }
      
      std::string s;
      
      std::string & getRefString() {
          s = "getRefString";
          return s;
      }
      
      int main() {
          getValueString() += " <==> ";// error, getValueString() is lvalue
          printfStr(getValueString()); // error, getValueString() is lvalue
          
          getRefString() += " <==> ";// correct, getRefString() is rvalue
          printfStr(getValueString()); // correct, getValueString() is rvalue
          // getRefString <==> getRefString
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
    • 左值可以自动隐式转为右值,但右值无法隐式转为左值

      int a = 10;
      int b = a; // a is converted implicitly to an rvalue 
      
      • 1
      • 2

    3.右值引用

    以上介绍的引用即通常所说的引用,是指左值引用lvalue reference,而2011年08月份发布的C++11中引入右值引用rvalue reference。在定义左值引用时都是定义变量的引用,而不能定义一个指向临时数据的左值引用,这可以使用右值引用来实现。

    int a = 1;
    int &b = a; // lvalue reference, correct
    
    int &b = 1; // error, lvalue reference can not refer to a temporary value;
    
    int *a = &10; // error: lvalue required as unary ‘&’ operand
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    此时,可以通过&&的方式定义指向临时值的右值引用:

    int x = 1;
    int &&b = 1;
    int &&c = x; // 错误,x是一个左值
    int &&d = b; // 错误,b是一个左值
    
    • 1
    • 2
    • 3
    • 4

    将以下代码转成汇编对比的结果为:

    // 一级指针
    int main() {
        int a = 10;
        int *b = &a;
        return 0;
    }
    
    // 右值引用
    int main() {
        int a = 10;
        int &&b = 10;
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    对比以上的代码可以看到,使用右值引用时,汇编代码几乎和一级指针相同,除了多出两行:

    0x000000000000116b <+34>:	mov    $0xa,%eax
    0x0000000000001170 <+39>:	mov    %eax,-0x18(%rbp)
    
    • 1
    • 2

    右值引用相当于创建了临时指针-0x18用来存放10,在一级指针中使用的时a的地址-0x1给指针赋值,在右值引用中使用系统自动生成的变量地址-0x18p赋值,因此右值引用实际上就是一级指针,只是在语言层面的语义区分,其底层实现仍然是借用指针。

    了解了右值引用后,再看#2printStr函数,要想打印非返回类型为值的函数调用的结果,可以借用右值引用:

    #include
    #include
    std::string getStr() {
        std::string s = "rvalue";
        return s;
    }
    
    void printStr(std::string &&str) {
        printf("%s", str.c_str());
    }
    
    int main() {
        printStr(getStr()); // correct
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    4.移动语义move函数

    C++11标准库在头文件utility中,引入了std::move函数,其目的是提高程序运行的效率,把以前一些需要“先拷贝,再删除源对象”的操作,转化为直接把源对象移动到目标位置,如STL容器中的push_back操作等。

    template< class T >
    constexpr std::remove_reference_t<T>&& move( T&& t ) noexcept;
    
    • 1
    • 2

    从函数声明可以看出,move返回的是std::remove_reference_t类型的右值引用,std::move通常用来表示一个对象有可能是从对象t移动过去的,实现了对象资源的高效传递,避免了对象的销毁重建,其作用等同于将对象static_cast类型转换为一个右值引用rvalue reference

    如下,在std::string类中,其第9个构造函数是带noexcept修饰的move constructor,可以实现将资源从一个string直接移动到另一个string,减少资源的复制删除。

    #include
    #include
    #include
    
    void printStr(std::string &str) {
        printf("%s\n", str.c_str());
    }
    
    int main() {
        std::string str = "this";
        // move (9)	string (string&& str) noexcept;
        std::string s(std::move(str));
        printStr(str);
        printStr(s); // content str is moved to s, str will be empty
        // std::string &&s = std::move(str);
        
        // 通过move实现资源的移动,减少复制
        std::vector<std::string> v;
        v.push_back(std::move(s));
        printStr(v[0]); 
        printStr(s); // s empty
    }   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    通过实验对比,使用std::movevectorpush_back string 10000次,性能相差近4倍。

    #include 
    #include 
    #include 
    #include
    int main()
    {
        std::vector<std::string> v;
        // take 9.26ms
        long st = cv::getTickCount();
        for(int i = 0; i < 10000; i++)
        {
            std::string s = "this";
            v.push_back(s);
        }
        long et = cv::getTickCount();
        printf("%.5f\n", (float)(et - st) / cv::getTickFrequency());
        v.clear();
        // take 2.35ms
        st = cv::getTickCount();
        for(int i = 0; i < 10000; i++)
        {
            std::string s = "this";
            v.push_back(std::move(s));
        } 
        et = cv::getTickCount(); 
        printf("%.5f\n", (float)(et - st) / cv::getTickFrequency());  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    参考文献

  • 相关阅读:
    深度学习模型在训练集上表现良好,但在测试集上表现较差,可能原因
    ubuntu报Unit firewalld.service could not be found.
    1234234234
    【最优化理论】03-无约束优化
    关于使用es数组的改变方式
    vue 多环境文件配置(开发,测试,生产)
    EXCEL+PYTHON学习2:
    vs code 工具HTML、css、javaScript、Vue、等代码插件安装
    Java入门7-面向对象基础
    chatgpt-web发布之docker打包流程
  • 原文地址:https://blog.csdn.net/lx_ros/article/details/127929972