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

文章目录

    • 1.C++中的变量名是如何存储及引用
    • 2.C++中的左值与右值
    • 3.右值引用
    • 4.移动语义move函数
    • 参考文献


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


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

int a = 0;

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

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

int main() {
    int a = 0;
    return 0;
}

得到对应汇编代码如下:

(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.

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

int main() {
    int a = 0;
    int &b = a;
    return 0;
}

得到对应汇编代码如下:

(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.

当使用指针时,

int main() {
    int a = 0;
    int *b = &a;
    return 0;
}

得到对应汇编代码如下:

(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.

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

(十)C++中的左值lvalue&右值rvaue_第1张图片

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

// 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;
}

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

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

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

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

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

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

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

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

    #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
    }
    
    
  • 左值可以自动隐式转为右值,但右值无法隐式转为左值

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

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

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

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

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

// 一级指针
int main() {
    int a = 10;
    int *b = &a;
    return 0;
}

// 右值引用
int main() {
    int a = 10;
    int &&b = 10;
    return 0;
}

(十)C++中的左值lvalue&右值rvaue_第2张图片

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

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

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

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

#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;
}

4.移动语义move函数

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

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

从函数声明可以看出,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
}   

通过实验对比,使用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.https://www.zhihu.com/question/34266997#:~:text=%E5%8F%98%E9%87%8F%E5%90%8D%E6%98%AF%E7%BB%99%E7%BC%96%E8%AF%91,%E4%B8%AA%E7%AC%A6%E5%8F%B7%E5%AF%B9%E5%BA%94%E4%B8%80%E4%B8%AA%E5%9C%B0%E5%9D%80%E3%80%82
  • 2.https://www.xianwaizhiyin.net/?p=2763
  • 4.https://learning.oreilly.com/library/view/c-in-a/059600298X/
  • 4.https://www.zhihu.com/question/26203703

你可能感兴趣的:(C++,c++,算法,开发语言,目标检测)