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++中的一个强大特性,它提供了一种有效的方式来操作数据,同时保持代码的清晰和简洁。正确理解和使用引用,特别是在函数参数和返回值中使用引用,可以显著提高程序的效率和质量。不过,使用引用时也需要谨慎,尤其是避免返回局部变量的引用,以防止未定义行为的发生。
引用是怎么实现的
测试函数代码
我们写两个文件 swap1.cpp 和swapp2.cpp. 分辨实现连个函数,swap 和 sayHello. 一个指针版本,一个引用版本。
然后反汇编看下 汇编的代码
汇编代码
swap 的函数的汇编代码。代码在指令上基本一致。
-
基本设置和标记:
.file "swap1.cpp":声明源文件。.text:指明接下来的段是代码段。.Ltext0:局部标签,用于代码段的开始。 在很多汇编文件中,.Ltext0表示代码段的开始,而.Letext0则对应该段代码的结束。这有助于编译器或链接器识别代码的边界,特别是在多个文件或多个段被整合在一起的情况下。“L” 通常表示局部(local)标签,.text0指的是代码段(text segment)的一个分区。.globl _Z4swapPiS_:声明_Z4swapPiS_函数是全局的,可以被其他文件中的代码引用。.type _Z4swapPiS_, @function:标明_Z4swapPiS_是一个函数。
-
函数开始:
_Z4swapPiS_::函数的开始标记。 -.LFB0::局部函数开始的标签。.cfi_startproc:标记函数的开始,用于调用栈帧信息的生成。endbr64:一个特定于 x86-64 架构的指令,用于防止恶意软件通过代码复用攻击。pushq %rbp:将基指针寄存器(rbp)的值压入栈中。.cfi_def_cfa_offset 16和.cfi_offset 6, -16:调整调用帧信息。movq %rsp, %rbp:将栈指针(rsp)的值复制到基指针(rbp)中,为新的栈帧设置基指针。
-
函数主体:
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), %rax和movl (%rax), %edx:加载第二个参数指向的整数值到 edx 寄存器。movq -24(%rbp), %rax和movl %edx, (%rax):将 edx 的值(即第二个参数的值)存储到第一个参数指向的位置。movq -32(%rbp), %rax和movl -4(%rbp), %edx:将暂存的第一个参数的原始值从栈加载到 edx,然后存到第二个参数指向的位置。
-
函数结束:
nop:无操作指令,通常用于对齐或填充。popq %rbp:恢复之前存储的基指针寄存器的值。.cfi_def_cfa 7, 8和ret:调整调用帧信息并返回到调用者。.cfi_endproc:标记函数的结束,用于调用栈帧信息的结束。.LFE0:和.size _Z4swapPiS_, .-_Z4swapPiS_:函数结束的标签和函数大小信息。LFE0:是一个标签,用于标记函数Z4swapPiS的结束。LFE表示 "Local Function End",即本地函数结束。数字0与函数的起始标签.LFB0(Local Function Begin 0)相对应,这样就成对标记了函数的开始和结束。
可以看到 在汇编指令上指针版本和引用版本基本一致。不同的地方并不影响汇编指令,只是一些代码注释和函数命名。差异的解释看下面
sayHello 函数的代码。swap 是引用作为函数参数,sayHello 函数是普通的引用变量, 从sayHello 函数上更能看出来汇编指令的一致性。
C++ 的名字改编
从截图上看 汇编代码的差异主要有两点,一个是函数的名字,一个是loc 字段。
loc 字段标记指令在代码中的位置。
在C++ 中,函数名字经过编译器编译后由于修改。用于支持函数重载和模板。这也是为什么 C代码需要 extern C 声明的原因。extern C 声明的C 函数 g++ 编译时 不修改函数名字。
-
函数命名:
-
_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 有类似的做法,通过函数名字实现函数签名
- loc 字段含义
.loc指令用于指定源代码中的位置,格式通常为.loc file line column。这有助于调试过程中的错误定位和源代码的步进。- 例如,
.loc 1 2 9表示当前指令对应于源文件中第一文件(由前面的.file 1 "/path/to/file"指定),第2行,第9列。
指针函数是 Z4swapPiS
引用函数是 Z4swapRiS
可以看出 C++ 引用是指针的一个语法糖,在编译器处理后和指针的实现是一样的。