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

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

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

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

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

const int &a; // 常引用,不能通过引用改变原来变量的值,可以传入右值

int func(const int &a)
{
    a += 10; //error
    return 0;
}

int b = 100;
func(b); // OK
func(100); // OK

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

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

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

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

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

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

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

    #include <cstdio>
    #include <cstring>
    
    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 
    
  • 类中定义只能被左值调用的函数 (opens new window)

class Person
{
public:
    Person(std::string_view name) : name_(name) {}
    std::string_view getName() & { return name_;}
private:
        std::string_view name_;

};

Person getBob()
{
    return Person("Bob");
}

void main(void)
{
    Person p("John");
    std::cout << p.getName() << std::endl;
    // function "Person::getName" (declared at line 31) cannot be called on an rvalue
    std::cout << getBob().getName() << std::endl;
}

上面的函数中&就是一个引用限定符,表明函数只能被左值变量调用,如上代码,getBob()返回的是一个右值调用了getName函数,IDE会立刻提示语法错误如上。

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

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

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

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

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

#include<cstdio>
#include<string>
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<cstdio>
#include<string>
#include<vector>

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 <cstdio>
#include <cstring>
#include <vector>
#include<opencv2/opencv.hpp>
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());  
}
(adsbygoogle = window.adsbygoogle || []).push({});

# 参考文献