C++ 中的引用的使用和原理

150 阅读8分钟

C++引用的使用和原理 -- 从汇编的角度看引用的实现

C++ 引用的概念和使用

C++ 引用概念

引用(Reference)是 C++ 相对于C语言的又一个扩充。

引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。引用类似于 Windows 中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序;引用类似于人的绰号,使用绰号和本名都表示同一个人。

引用的本质 引用在C++中作为一个重要的特性,提供了一种变量的间接引用方式。本质上,引用是某个变量的一个别名,对引用的所有操作都是直接作用于它所引用的变量。

C++ 引用的语法

定义和初始化引用 引用的定义必须同时进行初始化,这一点与指针不同。一旦一个引用被初始化后,它就不能再引用另一个对象,即引用是不可变的。

data_type &variable_name = value;

//data_type表示数据类型,比如int、char等等; //variable_name表示“引用”的名称; //value表示被引用的数据。

/* int a = 10; int &b = a; */

引用作为参数

引用作为函数参数 引用常用于函数参数列表中,使得函数能够修改传入的参数本身,而不仅仅是它们的副本,这样可以提高效率,尤其是在传递大型数据结构时。

cpp void increment(int &num) { num++; }

此外,使用引用传递可以避免拷贝构造的开销,尤其是对于大型对象或容器来说,这一点非常重要。

常引用 常引用(const reference)是引用的一种特殊形式,它不允许通过引用修改数据,但可以引用一个常量。常引用对于保证函数中的参数不被修改非常有用,同时还能享受到引用传递的效率优势。

cpp void printReference(const int &value) { 
    std::cout << value << std::endl; 
 }

引用作为返回值

引用作为函数返回值 引用可以作为函数的返回值,但返回的引用必须指向一个在函数外部仍然存在的对象,通常是函数外的静态对象或全局对象,或者是作为参数传递给函数的对象。返回局部变量的引用是不安全的,会导致未定义行为。

int& func(int &x) { 
    x++; 
    return x; // 返回引用是安全的,因为x是外部传入的 
} 
int& badFunc() { 
   int a = 10; 
   return a; // 错误,返回局部变量的引用 
}

引用与指针的区别

引用与指针的区别 引用和指针都可以用来在不同的上下文中引用或访问变量或数据,但它们之间有一些关键的区别:

  • 初始化和可变性: 引用必须在定义时初始化,并且一旦指向一个变量,就不能改变。指针可以在任何时候初始化,并可以改变所指向的对象。
  • 使用语法: 引用的使用更像是使用普通变量,不需要解引用。指针需要显式解引用来访问目标数据。
  • 安全性: 引用比指针更安全,因为引用保证了引用的对象总是有效的,而指针可能为空。

结论

引用是C++中的一个强大特性,它提供了一种有效的方式来操作数据,同时保持代码的清晰和简洁。正确理解和使用引用,特别是在函数参数和返回值中使用引用,可以显著提高程序的效率和质量。不过,使用引用时也需要谨慎,尤其是避免返回局部变量的引用,以防止未定义行为的发生。

引用是怎么实现的

测试函数代码

image.png 我们写两个文件 swap1.cpp 和swapp2.cpp. 分辨实现连个函数,swap 和 sayHello. 一个指针版本,一个引用版本。

然后反汇编看下 汇编的代码

汇编代码

swap 的函数的汇编代码。代码在指令上基本一致。

image.png

  1. 基本设置和标记:

    • .file "swap1.cpp":声明源文件。
    • .text:指明接下来的段是代码段。
    • .Ltext0:局部标签,用于代码段的开始。 在很多汇编文件中,.Ltext0 表示代码段的开始,而 .Letext0 则对应该段代码的结束。这有助于编译器或链接器识别代码的边界,特别是在多个文件或多个段被整合在一起的情况下。“L” 通常表示局部(local)标签,.text0 指的是代码段(text segment)的一个分区。
    • .globl _Z4swapPiS_:声明 _Z4swapPiS_ 函数是全局的,可以被其他文件中的代码引用。
    • .type _Z4swapPiS_, @function:标明 _Z4swapPiS_ 是一个函数。
  2. 函数开始:

    • _Z4swapPiS_::函数的开始标记。 - .LFB0::局部函数开始的标签。
    • .cfi_startproc:标记函数的开始,用于调用栈帧信息的生成。
    • endbr64:一个特定于 x86-64 架构的指令,用于防止恶意软件通过代码复用攻击。
    • pushq %rbp:将基指针寄存器(rbp)的值压入栈中。
    • .cfi_def_cfa_offset 16.cfi_offset 6, -16:调整调用帧信息。
    • movq %rsp, %rbp:将栈指针(rsp)的值复制到基指针(rbp)中,为新的栈帧设置基指针。
  3. 函数主体:

    • movq %rdi, -24(%rbp)movq %rsi, -32(%rbp):将函数的两个参数(两个指针)从寄存器(rdi 和 rsi)存储到栈帧的局部变量位置。
    • movq -24(%rbp), %rax:将第一个参数的地址加载到 rax 寄存器。
    • movl (%rax), %eax:将第一个参数指向的整数值加载到 eax 寄存器。
    • movl %eax, -4(%rbp):将这个值暂存到栈上。
    • movq -32(%rbp), %raxmovl (%rax), %edx:加载第二个参数指向的整数值到 edx 寄存器。
    • movq -24(%rbp), %raxmovl %edx, (%rax):将 edx 的值(即第二个参数的值)存储到第一个参数指向的位置。
    • movq -32(%rbp), %raxmovl -4(%rbp), %edx:将暂存的第一个参数的原始值从栈加载到 edx,然后存到第二个参数指向的位置。
  4. 函数结束:

    • nop:无操作指令,通常用于对齐或填充。
    • popq %rbp:恢复之前存储的基指针寄存器的值。
    • .cfi_def_cfa 7, 8ret:调整调用帧信息并返回到调用者。
    • .cfi_endproc:标记函数的结束,用于调用栈帧信息的结束。
    • .LFE0:.size _Z4swapPiS_, .-_Z4swapPiS_:函数结束的标签和函数大小信息。LFE0:是一个标签,用于标记函数Z4swapPiS的结束。LFE表示 "Local Function End",即本地函数结束。数字0与函数的起始标签 .LFB0(Local Function Begin 0)相对应,这样就成对标记了函数的开始和结束。

可以看到 在汇编指令上指针版本和引用版本基本一致。不同的地方并不影响汇编指令,只是一些代码注释和函数命名。差异的解释看下面

sayHello 函数的代码。swap 是引用作为函数参数,sayHello 函数是普通的引用变量, 从sayHello 函数上更能看出来汇编指令的一致性。

image.png

C++ 的名字改编

从截图上看 汇编代码的差异主要有两点,一个是函数的名字,一个是loc 字段。

loc 字段标记指令在代码中的位置。

在C++ 中,函数名字经过编译器编译后由于修改。用于支持函数重载和模板。这也是为什么 C代码需要 extern C 声明的原因。extern C 声明的C 函数 g++ 编译时 不修改函数名字。

  1. 函数命名:

    • _Z4swapPiS_ 的解释 - 这种命名方式称为名字改编(Name Mangling),C++ 中用于支持函数重载和模板等特性。

    • _Z: 表示这是一个 mangled 名字的开始。

    • 4swap: 表示接下来的函数名是 swap,前面的数字 4 表示名称的长度。

    • PiS_: 编码的参数类型,P 表示指针,i 表示 int 类型,S_ 是复用前面的 int 类型参数。因此,PiS_ 表示函数接受两个 int* 类型的参数。

    • RiS: 表示函数参数。R 指的是引用,i 表示 int 类型,S_ 复用了前面的 int 类型。因此,RiS 指函数接受两个 int&(即两个整数引用)作为参数。

在函数的命名上和JNI 有类似的做法,通过函数名字实现函数签名

  1. loc 字段含义
  • .loc 指令用于指定源代码中的位置,格式通常为 .loc file line column。这有助于调试过程中的错误定位和源代码的步进。
  • 例如,.loc 1 2 9 表示当前指令对应于源文件中第一文件(由前面的 .file 1 "/path/to/file" 指定),第2行,第9列。

指针函数是 Z4swapPiS

引用函数是 Z4swapRiS

可以看出 C++ 引用是指针的一个语法糖,在编译器处理后和指针的实现是一样的。