从内存角度聊聊引用和右值引用

        引用变量常见的一般如下:

int a  = 3;
int &b = a;
int *c = &a;

        b 即引用变量,此时它的值也是 3,且地址也是变量 a 的地址,这点可以通过 gdb 调试时看出:

(gdb) p a
$4 = 3
(gdb) p &a
$5 = (int *) 0x7ffeefbffa08
(gdb) p b
$6 = (int &) @0x7ffeefbffa08: 3
(gdb) p &b
$7 = (int *) 0x7ffeefbffa08

        指针 *c 也是可以表示变量 a 的,但是单独申请了一段空间来存储变量 a 的地址:

(gdb) p *c
$8 = 3
(gdb) p c
$9 = (int *) 0x7ffeefbffa08
(gdb) p &c
$10 = (int **) 0x7ffeefbff9f8

        这点和引用有所不同,定义引用变量无需申请内存空间。如果再将另一个变量赋给 b 呢?

int d = 4;
b = d;

        可以再看下 ba 有哪些变化:

(gdb) p &d
$1 = (int *) 0x7ffeefbff9f4
(gdb) p b
$2 = (int &) @0x7ffeefbffa08: 4
(gdb) p a
$4 = 4

        尽管 ab 的值已经更新为 4,但 b 依然是和 a 捆绑着的。即 b 在初始化时引用的对象就已经固定了,不会再改。

        实际引入引用变量的用途是用作函数的参数,在引入之前,函数传参主要是值传递,但值传递会生成实参的副本。如果是一般的内置类型倒还好,但如果是数据较大的结构体或对象时,性能就有所损耗。如定义这个么函数:

void func(int v)
{
    v = v + 5;
}

        将前面定义好的变量 a 传递给该函数,打印如下:

(gdb) p &a
$1 = (int *) 0x7ffeefbffa08
(gdb) s
func (v=3) at test.cpp:20
20          v = v + 5;
(gdb) p &v
$2 = (int *) 0x7ffeefbff9ec

        可以看到实参 a 与形参 v 的地址并不相同,va 的拷贝。那么 a 的值是怎么给到 v 的呢?可通过 disassemble 命令查看对应汇编程序:

(gdb) disassemble /m $pc
Dump of assembler code for function main():
    ...
11          int a = 3;
   0x0000000100003d2f <+15>:    movl   $0x3,-0x8(%rbp)

12          func(a);
   0x0000000100003d36 <+22>:    mov    -0x8(%rbp),%edi
   0x0000000100003d39 <+25>:    call   0x100003d70 <_Z4funci>
    ...

        可以看到在调用 func() 方法时,程序是将实参 a 的值传给了寄存器 edirbp 始终指向当前栈帧的底部。再看看 func() 方法内部怎么操作的:

19      {
   0x0000000100003d70 <+0>:     push   %rbp
   0x0000000100003d71 <+1>:     mov    %rsp,%rbp
   0x0000000100003d74 <+4>:     mov    %edi,-0x4(%rbp)
20          v = v + 5;
=> 0x0000000100003d77 <+7>:     mov    -0x4(%rbp),%eax
   0x0000000100003d7a <+10>:    add    $0x5,%eax
   0x0000000100003d7d <+13>:    mov    %eax,-0x4(%rbp)

        程序再将寄存器 edi 的值传到当前栈帧中,也就是说形参和实参是通过寄存器来传值的。再回到函数传参,如果是引用传参呢?可定义这么个函数:

void refunc(int &r)
{
    r = r + 6;
}

        调试打印如下:

(gdb) p &a
$1 = (int *) 0x7ffeefbffa08
(gdb) s
refunc (r=@0x7ffeefbffa08: 3) at test.cpp:27
27          r = r + 6;
(gdb) p &r
$2 = (int *) 0x7ffeefbffa08

        可以看到实参 a 与形参 r 指向的是同一块空间,操作 r 即操作 a 本身,省去了一次拷贝。这里传的是变量 a,如果传一个常数呢?譬如传 4

refunc(4);

        编译时就要报错了:

note: candidate function not viable: expects an l-value for 1st argument

        翻译过来就是 refunc(int &r) 期待一个左值。左值字面上可以理解成赋值语句左边的变量,右值就是赋值语句右边的变量。深层次的区别就是左值是可以获取内存中确切地址的,如一般变量、指针、数组元素等都是左值,而常数、表达式等则是右值。所以上面介绍的引用实际上只是对左值的引用,那有没有对右值的引用呢?c++11 确实提出了一个新的引用,即右值引用,顾名思义是对右值的引用。如:

int && a  = 3;

        但这样定义个变量本身其实没多大意义,更多还是应用于移动语义,可定义这么个函数:

int rvalfunc(int && r)
{
    r = r + 7;
    return r;
}

        添加返回值是为了能拿到右值引用结果,然后执行下面语句:

int a = rvalfunc(3);

        主函数中汇编程序如下:

12          int a = rvalfunc(3);
=> 0x0000000100003d0f <+15>:    movl   $0x3,-0xc(%rbp)
   0x0000000100003d16 <+22>:    lea    -0xc(%rbp),%rdi
   0x0000000100003d1a <+26>:    call   0x100003d50 <_Z8rvalfuncOi>

        这块和左值引用时稍微有所不同了,执行的是 lea 命令,并不是将 -0xc(%rbp) 处的 3 传给寄存器 rdi,而是直接将变量 3 所在的地址 -0xc(%rbp) 传给 rdi。然后调用 rvalfunc() 方法:

29      {
   0x0000000100003d50 <+0>:     push   %rbp
   0x0000000100003d51 <+1>:     mov    %rsp,%rbp
   0x0000000100003d54 <+4>:     mov    %rdi,-0x8(%rbp)

30          r = r + 7;
=> 0x0000000100003d58 <+8>:     mov    -0x8(%rbp),%rax
   0x0000000100003d5c <+12>:    mov    (%rax),%ecx
   0x0000000100003d5e <+14>:    add    $0x7,%ecx
   0x0000000100003d61 <+17>:    mov    -0x8(%rbp),%rax
   0x0000000100003d65 <+21>:    mov    %ecx,(%rax)

        先将寄存器 rdi 中的值即地址传到栈帧 -0x8(%rbp) 处,接着传给寄存器 rax,再将 rax 中保存的地址所在的值 3 传给寄存器 ecx,最后再执行方法体中的处理程序。由此可以看出,当形参定义为右值引用时,并没有数据的拷贝,而是将数据所在的地址传给寄存器,然后在函数内部从寄存器中取出该地址并放在自己的栈帧空间中,再根据该地址取出对应的值放到寄存器中。

        而移动语义也就是利用这点来实现的。避免了原始数据的拷贝,只是将所有权进行转移。

拓展:

        1. 除了右值引用,其实 const 引用也是可以指向右值的。如:

const int &a = 5;
int b = 3, c = 4;
const int &d = b;
const int &e = b + c;

        以上程序是可以正常编译的。可以看到 const & 既可以对左值,也可以对右值,某种程度上可以认为 const & 对左值引用和右值引用做了个统一。但是也还有个问题,就是在函数传参时如果定义为 const &,那么参数值就没法改变了,某些情况下不符合业务需求。

        2. 汇编语法分 InterAT&T 两种,Inter syntax 主要用于 windowsAT&T syntax 主要用于 unix 和类 unix 系统。二者指令的操作数位置相反:

Inter syntax:mov to from

AT&T syntax: mov from to

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值