大家好。
上周有個同學說面阿里云存儲被問到了引用和指針的區別,并且給我發了一些百度上搜到的標準回答。
所以在這分享一下剛學 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
還是幾乎完全一樣.......也沒再注釋
簡單總結
- 引用只是c++語法糖,可以看作編譯器自動完成取地址、解引用的常量指針
- 引用區別于指針的特性都是編譯器約束完成的,一旦編譯成匯編就喝指針一樣
- 由于引用只是指針包裝了下,所以也存在風險,比如如下代碼:
int *a = new int;
int &b = *a;
delete a;
b = 12; // 對已經釋放的內存解引用
- 引用由編譯器保證初始化,使用起來較為方便(如不用檢查空指針等)
- 引用沒有頂層 const (引用本身不可變) 即
int & const
,因為引用本身就不可變,所以在加頂層 const 也沒有意義;但是可以有底層 const ()即 const int&
,這表示引用所引用的對象本身是常量 - 指針既有頂層const(
int * const
--指針本身不可變),也有底層const(const int *
--指針所指向的對象不可變) - 有指針引用--是引用,綁定到指針, 但是沒有引用指針--這很顯然,因為很多時候指針存在的意義就是間接改變對象的值。但是引用本身的值我們上面說過了是所引用對象的地址,但是引用不能更改所引用的對象,也就當然不能有引用指針了。
- 指針和引用的自增(++)和自減含義不同,指針是指針運算, 而引用是代表所指向的對象對象執行++或--