C++的引用类型(reference)

Last Updated: 2023-12-26 11:12:26 Tuesday

-- TOC --

C++引入了一种新的类型,叫做reference类型,它与指针有些类似,比指针更安全。

References are a major improvement to handling pointers. They’re similar to pointers, but with some key differences.

reference是一种新的类型,它与指针有如下区别:

  1. reference类型的变量,不能够被重新赋值(reseated),而指针可以随意修改值;
  2. 使用reference类型的变量,就如同直接使用被引用的对象;
  3. reference类型的变量,不可能是空(nullptr);

从C++11开始,reference分为lvalue和rvalue,本文下面的内容,全都是针对lvalue。(rvalue主要用于move semantics

lvalue的三大功能:

  1. 给复杂表达式所代表的对象一个简单的别名;
  2. 在ranged-for loop时,避免对象copy;
  3. 函数传参和返回值,避免对象copy;

定义reference类型,使用ampersand符号&

#include <cstdio>

int main(void) {
    int abc{123};
    auto &to_abc = abc;
    to_abc = 234;
    printf("%d %d\n", abc, to_abc);
    return 0;
}

to_abc就是abc的一个reference,修改to_abc的值,等同于修改abc的值。这段代码会打印出来两个234:

$ g++ test_ref.cpp
$ ./a.out
234 234

Under the hood, references are equivalent to pointers because they’re also a zero-overhead abstraction. The compiler produces similar code.

上面测试代码,反汇编之后:

0000000000401126 <main>:
  401126:       55                      push   %rbp
  401127:       48 89 e5                mov    %rsp,%rbp
  40112a:       48 83 ec 10             sub    $0x10,%rsp
  40112e:       c7 45 f4 7b 00 00 00    movl   $0x7b,-0xc(%rbp)
  401135:       48 8d 45 f4             lea    -0xc(%rbp),%rax
  401139:       48 89 45 f8             mov    %rax,-0x8(%rbp)
  40113d:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  401141:       c7 00 ea 00 00 00       movl   $0xea,(%rax)
  401147:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  40114b:       8b 10                   mov    (%rax),%edx
  40114d:       8b 45 f4                mov    -0xc(%rbp),%eax
  401150:       89 c6                   mov    %eax,%esi
  401152:       bf 10 20 40 00          mov    $0x402010,%edi
  401157:       b8 00 00 00 00          mov    $0x0,%eax
  40115c:       e8 cf fe ff ff          call   401030 <printf@plt>
  401161:       b8 00 00 00 00          mov    $0x0,%eax
  401166:       c9                      leave
  401167:       c3                      ret

下面是注释版:

push   %rbp                # push调用main接口代码的rbp
mov    %rsp,%rbp           # rsp --> rbp,新的rbp
sub    $0x10,%rsp          # rsp - 16,开辟的栈帧空间
movl   $0x7b,-0xc(%rbp)    # -0xc(%rbp)这个位置是abc变量,存放123
lea    -0xc(%rbp),%rax     # 将-0xc(%rbp)放入rax,即将abc的地址放入rax
mov    %rax,-0x8(%rbp)     # 将rax的值存入-0x8(%rbp),这就是to_abc的地址
mov    -0x8(%rbp),%rax     # 将to_abc指向的地址,存入rax
movl   $0xea,(%rax)        # 将234存入rax指向的地址,这个地址就是abc的地址
mov    -0x8(%rbp),%rax
mov    (%rax),%edx
mov    -0xc(%rbp),%eax
mov    %eax,%esi
mov    $0x402010,%edi
mov    $0x0,%eax
call   401030 <printf@plt>
mov    $0x0,%eax
leave
ret

简单的汇编知识,还是很有必要的,不熟悉的同学,可以学习一点汇编知识

分析了汇编发现,其实就是地址。只是reference类型,在编译的时候,编译器对待引用类型,与对待指针类型,有不一样的编译。

由于reference类型不可以出现空值,因此在编写代码的时候,在某些时候,会比使用指针简单一点点。

void test(myobject *p) {
    if (p == NULL)
        return;
    // 开始使用p指针,p->something...or (*p).something...
}

void test(myobject& p{
    // 直接使用对象p,p.something...
}

使用指针的时候,为了安全,在接口入口处,一般都要判断一下指针是否为空。而使用reference,就不需要判断了,直接上。这的确会减少一些overhead!这是reference带来的一个好处。另一个好处是,使用reference,不需要使用member-of-pointer操作符->,直接用.这个符号。

当一个函数接口的参数是reference的时候,传参也变得简单了,直接传入对象本身。返回reference时,也是直接返回对象(avoiding a copy)。请看下面的测试代码:

$ cat test_ref.cpp
#include <cstdio>

struct abc {
    int a;
    int b;
};

abc& change(abc& g) {
    g.a = 4;
    g.b = 5;
    return g;
}

int main(void) {
    abc a{1,2};
    abc& h = change(a);
    printf("%d %d\n", h.a, h.b);
    return 0;
}
$ g++ test_ref.cpp
$ ./a.out
4 5

跟指针一样,下面的代码是错的,返回了一个局部栈空间的地址:

// error code
HolmesIV& not_dinkum() {
    HolmesIV mike;
    return mike;
}

RVO和NRVO是出现在返回对象的场景下。

有一些编码场景,不能使用reference类型,还是只能使用指针!

由于reference类型的变量不能够重新赋值,即不能让它去引用一个别的对象,因此,这个限制也让reference类型的变量,不适合处理链表这样的数据结构。因为,指向链表的指针,在遍历的时候,要不停地变化。

将对象转换成引用

C++有std::ref,可以用在给线程入口传递引用对象的时候。

一般函数传递引用参数,直接填入对象名称即可,但是线程的情况有些特殊,线程有自己独立的stack。如果线程入口参数是对象,会触发copy+move操作。

这部分细节详情,请参考C++向线程传递对象参数

本文链接:https://cs.pynote.net/sf/c/cpp/202208191/

-- EOF --

-- MORE --