在字节,每 7 秒就有 100 个指针消失!

苦逼的码农

共 4400字,需浏览 9分钟

 ·

2021-01-15 14:12

最近时间有点紧,发一篇上大学刚学 C++ 时写的一篇博客,是关于指针和引用的区别的,基本没有做修改,讲解可能会比较粗略,需要有一点点汇编的知识。

引用和指针有什么区别呢?

相信大多数学过 C++ 的都能回答上几点:

  • 指针是所指内存的地址
  • 引用是别名
  • 引用必须初始化,并且初始化后不能重新引用其它变量

但是引用是别名这是 C++ 语法规定的语义。

那么到底引用在汇编层面和指针有什么区别呢?

没区别。

是的,这是我当时自己反汇编观察后得出的结论,引用会被 C++ 编译器当做 const 指针来进行操作。

我们来看个例子:

汇编揭开引用面纱

先分别用指针和引用来写个非常熟悉的函数swap

// 指针版
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
// 引用版
void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

直接 gcc -S 输出汇编:

引用版汇编

__Z4swapRiS_:                           ## @_Z4swapRiS_
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movq %rdi, -8(%rbp) # 传入的第一个参数存放到%rbp-8 (应该是采用的寄存器传参,而不是常见的压栈)
movq %rsi, -16(%rbp) # 第二个参数 存放到 %rbp-16
movq -8(%rbp), %rsi # 第一个参数赋给 rsi
movl (%rsi), %eax # 以第一个参数为地址取出值赋给eax,取出*a暂存寄存器
movl %eax, -20(%rbp) # temp = a
movq -16(%rbp), %rsi # 将第二个参数重复上面的
movl (%rsi), %eax
movq -8(%rbp), %rsi
movl %eax, (%rsi) # a = b
movl -20(%rbp), %eax # eax = temp
movq -16(%rbp), %rsi
movl %eax, (%rsi) # b = temp
popq %rbp
retq
.cfi_endproc
## -- End function

在来一个函数调用引用版本 swap

void call() {
    int a = 10;
    int b = 3;
    int &ra = a;
    int &rb = b;
    swap(ra, rb);
}

对应汇编:

__Z4callv:                              ## @_Z4callv
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
leaq -8(%rbp), %rax # rax中是b的地址
leaq -4(%rbp), %rcx # rcx中是a的地址
movl $10, -4(%rbp)
movl $3, -8(%rbp) # 分别初始化a、b
movq %rcx, -16(%rbp) # 赋给ra引用
movq %rax, -24(%rbp) # 赋给rc引用
movq -16(%rbp), %rdi # 寄存器传参, -16(%rbp)就是rcx中的值也就是a的地址
movq -24(%rbp), %rsi # 略
callq __Z4swapRiS_
addq $32, %rsp
popq %rbp
retq

从上面我们可以看到给引用赋初值,也就是把所引用对象的地址赋给引用所在内存,和指针是一样的。

再来看看指针的汇编吧

指针版汇编

__Z4swapPiS_:                           ## @_Z4swapPiS_
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq -8(%rbp), %rsi
movl (%rsi), %eax
movl %eax, -20(%rbp)
movq -16(%rbp), %rsi
movl (%rsi), %eax
movq -8(%rbp), %rsi
movl %eax, (%rsi)
movl -20(%rbp), %eax
movq -16(%rbp), %rsi
movl %eax, (%rsi)
popq %rbp
retq
.cfi_endproc
## -- End function

汇编我就不注释了,真的是完全一样!并不是我直接复制的引用汇编而是真的在编译器实现上都是相同的方式。

指针版调用

void pointer_call() {
    int a = 10;
    int b = 3;
    int *pa = &a;
    int *pb = &b;
    swap(pa, pb);
}

这次我特意改了下函数名,对应汇编:

__Z12pointer_callv:                     ## @_Z12pointer_callv
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
leaq -8(%rbp), %rax
leaq -4(%rbp), %rcx
movl $10, -4(%rbp)
movl $3, -8(%rbp)
movq %rcx, -16(%rbp)
movq %rax, -24(%rbp)
movq -16(%rbp), %rdi
movq -24(%rbp), %rsi
callq __Z4swapPiS_
addq $32, %rsp
popq %rbp
retq

还是几乎完全一样.......也没再注释

简单总结

  1. 引用只是c++语法糖,可以看作编译器自动完成取地址、解引用的常量指针
  2. 引用区别于指针的特性都是编译器约束完成的,一旦编译成汇编就喝指针一样
  3. 由于引用只是指针包装了下,所以也存在风险,比如如下代码:
    int *a = new int;
    int &b = *a;
    delete a;
    b = 12;    // 对已经释放的内存解引用
  4. 引用由编译器保证初始化,使用起来较为方便(如不用检查空指针等)
  5. 尽量用引用代替指针
  6. 引用没有顶层 const (引用本身不可变) 即int & const,因为引用本身就不可变,所以在加顶层 const 也没有意义;但是可以有底层 const ()即 const int&,这表示引用所引用的对象本身是常量
  7. 指针既有顶层const(int * const--指针本身不可变),也有底层const(int * const--指针所指向的对象不可变)
  8. 有指针引用--是引用,绑定到指针, 但是没有引用指针--这很显然,因为很多时候指针存在的意义就是间接改变对象的值。但是引用本身的值我们上面说过了是所引用对象的地址,但是引用不能更改所引用的对象,也就当然不能有引用指针了。
  9. 指针和引用的自增(++)和自减含义不同,指针是指针运算, 而引用是代表所指向的对象对象执行++或--


浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报