大家好。
上周有個(gè)同學(xué)說面阿里云存儲(chǔ)被問到了引用和指針的區(qū)別,并且給我發(fā)了一些百度上搜到的標(biāo)準(zhǔn)回答。
所以在這分享一下剛學(xué) C++ 時(shí)寫的一篇博客,是關(guān)于指針和引用的區(qū)別的,基本沒有做修改,講解可能會(huì)比較粗略,需要有一點(diǎn)點(diǎn)匯編的知識(shí)。
引用和指針有什么區(qū)別呢?
相信大多數(shù)學(xué)過 C++ 的都能回答上幾點(diǎn):
但是引用是別名這是 C++ 語法規(guī)定的語義。
那么到底引用在匯編層面和指針有什么區(qū)別呢?
沒區(qū)別。
是的,這是我當(dāng)時(shí)自己反匯編觀察后得出的結(jié)論,引用會(huì)被 C++ 編譯器當(dāng)做 const 指針來進(jìn)行操作。
我們來看個(gè)例子:
匯編揭開引用面紗
先分別用指針和引用來寫個(gè)非常熟悉的函數(shù)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) # 傳入的第一個(gè)參數(shù)存放到%rbp-8 (應(yīng)該是采用的寄存器傳參,而不是常見的壓棧)
movq %rsi, -16(%rbp) # 第二個(gè)參數(shù) 存放到 %rbp-16
movq -8(%rbp), %rsi # 第一個(gè)參數(shù)賦給 rsi
movl (%rsi), %eax # 以第一個(gè)參數(shù)為地址取出值賦給eax,取出*a暫存寄存器
movl %eax, -20(%rbp) # temp = a
movq -16(%rbp), %rsi # 將第二個(gè)參數(shù)重復(fù)上面的
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
在來一個(gè)函數(shù)調(diào)用引用版本 swap
void call() {
int a = 10;
int b = 3;
int &ra = a;
int &rb = b;
swap(ra, rb);
}
對(duì)應(yīng)匯編:
__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
從上面我們可以看到給引用賦初值,也就是把所引用對(duì)象的地址賦給引用所在內(nèi)存,和指針是一樣的。
再來看看指針的匯編吧
指針版匯編
__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
匯編我就不注釋了,真的是完全一樣!并不是我直接復(fù)制的引用匯編而是真的在編譯器實(shí)現(xiàn)上都是相同的方式。
指針版調(diào)用
void pointer_call() {
int a = 10;
int b = 3;
int *pa = &a;
int *pb = &b;
swap(pa, pb);
}
這次我特意改了下函數(shù)名,對(duì)應(yīng)匯編:
__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
還是幾乎完全一樣.......也沒再注釋
簡(jiǎn)單總結(jié)
- 引用只是c++語法糖,可以看作編譯器自動(dòng)完成取地址、解引用的常量指針
- 引用區(qū)別于指針的特性都是編譯器約束完成的,一旦編譯成匯編就喝指針一樣
- 由于引用只是指針包裝了下,所以也存在風(fēng)險(xiǎn),比如如下代碼:
int *a = new int;
int &b = *a;
delete a;
b = 12; // 對(duì)已經(jīng)釋放的內(nèi)存解引用
- 引用由編譯器保證初始化,使用起來較為方便(如不用檢查空指針等)
- 引用沒有頂層 const (引用本身不可變) 即
int & const
,因?yàn)橐帽旧砭筒豢勺?,所以在加頂?const 也沒有意義;但是可以有底層 const ()即 const int&
,這表示引用所引用的對(duì)象本身是常量 - 指針既有頂層const(
int * const
--指針本身不可變),也有底層const(const int *
--指針?biāo)赶虻膶?duì)象不可變) - 有指針引用--是引用,綁定到指針, 但是沒有引用指針--這很顯然,因?yàn)楹芏鄷r(shí)候指針存在的意義就是間接改變對(duì)象的值。但是引用本身的值我們上面說過了是所引用對(duì)象的地址,但是引用不能更改所引用的對(duì)象,也就當(dāng)然不能有引用指針了。
- 指針和引用的自增(++)和自減含義不同,指針是指針運(yùn)算, 而引用是代表所指向的對(duì)象對(duì)象執(zhí)行++或--