1. 程式人生 > >程序設計基石與實踐系列之按值傳遞還是按引用

程序設計基石與實踐系列之按值傳遞還是按引用

有趣 name align pos str 堆棧 技術分享 easy pan

從簡單的樣例開始.如果我們要交換兩個整形變量的值,在C/C++中怎麽做呢?我們來看多種方式,哪種能夠做到.

void call_by_ref(int &p,int &q) { // 能夠交換的樣例
    int t = p;
    p = q;
    q = t;
}
 
void call_by_val_ptr(int * p,int * q) { // 不能交換的樣例
    int * t = p;
    p = q;
    q = t;
}
 
void call_by_val(int p,int q){ // 不能交換的樣例
    int t = p ;
    p = q;
    q = t;
}
由於樣例非常easy。看代碼就可以知道僅僅有call_by_ref這種方法能夠成功交換。這裏,你一定還知道一種能夠交換的方式,別著急。慢慢來,我們先看看為什麽僅僅有call_by_ref能夠交換。

call_by_ref

技術分享

void call_by_ref(int &p,int &q) {
    push   %rbp
    mov    %rsp,%rbp
    mov    %rdi,-0x18(%rbp)
    mov    %rsi,-0x20(%rbp)
     
//int t = p;
    mov    -0x18(%rbp),%rax
    
//關鍵點:rax中存放的是變量的實際地址。將地址處存放的值取出放到eax中
    mov    (%rax),%eax
    mov    %eax,-0x4(%rbp)
     
//p = q;
    mov    -0x20(%rbp),%rax
    
//關鍵點:rax中存放的是變量的實際地址,將地址處存放的值取出放到edx
    mov    (%rax),%edx
    mov    -0x18(%rbp),%rax
    mov    %edx,(%rax)
     
//q = t;
    mov    -0x20(%rbp),%rax
    mov    -0x4(%rbp),%edx
    
//關鍵點:rax存放的也是實際地址,同上.
    mov    %edx,(%rax)
}
上面這段匯編的邏輯非常easy,我們看到裏面的關鍵點都在強調:將值存放在實際地址中.上面這句話盡管簡單,但非常重要。能夠拆為兩點:

1、要有實際地址.
2、要有將值存入實際地址的動作.

從上面的代碼中。我們看到已經有“存值”這個動作,那麽傳入的是否實際地址呢?

// c代碼
call_by_val_ptr(&a,&b);
 
// 相應的匯編代碼
 
lea    -0x18(%rbp),%rdx
lea    -0x14(%rbp),%rax
mov    %rdx,%rsi
mov    %rax,%rdi
callq  4008c0 <_Z11call_by_refRiS_>

註意到。lea操作是取地址

。那麽就能確定這樣的“按引用傳遞“的方式,實際是傳入了實參的實際地址。

那麽。滿足了上文的兩個條件,就能交換成功。

call_by_val

技術分享

call_by_val的反匯編代碼例如以下:

void call_by_val(int p,int q){
    push   %rbp
    mov    %rsp,%rbp
    mov    %edi,-0x14(%rbp)
    mov    %esi,-0x18(%rbp) 
    
//int t = p ;
    mov    -0x14(%rbp),%eax
    mov    %eax,-0x4(%rbp) 
    
//p = q;
    mov    -0x18(%rbp),%eax
    mov    %eax,-0x14(%rbp) 
    
//q = t;
    mov    -0x4(%rbp),%eax
    mov    %eax,-0x18(%rbp)
}
能夠看到,上面的代碼中在賦值時。僅僅是將某種”值“放入了寄存器。再觀察下傳參的代碼:

call_by_val(a,b);
 
// 相應的匯編代碼
mov    -0x18(%rbp),%edx
mov    -0x14(%rbp),%eax
mov    %edx,%esi
mov    %eax,%edi
callq  400912 <_Z11call_by_valii>

能夠看出,僅僅是將變量a、b的值存入了寄存器,而非”地址“或者能找到其”地址“的東西。

那麽,由於不滿足上文的兩個條件。所以不能交換。

這裏另一點有趣的東西,也就是我們常聽說的拷貝(Copy):當一個值,被放入寄存器或者堆棧中,其擁有了新的地址。那麽這個值就和其原來的實際地址沒有關系了,這樣的行為。是不是非常像一種拷貝?

但實際上。在我看來。這是一個非常誤導的術語。由於上面的按引用傳遞的call_by_ref實際上也是拷貝一種值。它是個地址。並且是實際地址。

所以,應該記住的是那兩個條件。在你還不能真正理解拷貝的意義之前最好不要用這個術語。

call_by_val_ptr

技術分享

這樣的方式,本來是能夠完畢交換的,由於我們能夠用指針來指向實際地址,這樣我們就滿足了條件1:

要有實際地址。

別著急,我們先看下上文的實現中,為什麽沒有完畢交換:

void call_by_val_ptr(int * p,int * q) {
    push   %rbp
    mov    %rsp,%rbp
    mov    %rdi,-0x18(%rbp)
    mov    %rsi,-0x20(%rbp)
    
//int * t = p;
    mov    -0x18(%rbp),%rax
    mov    %rax,-0x8(%rbp)
    
//p = q;
    mov    -0x20(%rbp),%rax
    mov    %rax,-0x18(%rbp)
    
//q = t;
    mov    -0x8(%rbp),%rax
    mov    %rax,-0x20(%rbp)
}
能夠看到,上面的邏輯和call_by_val非常相似,也僅僅是做了將值放到寄存器這件事,那麽再看下傳給它的參數:

call_by_val_ptr(&a,&b);
 
// 相應的匯編代碼
lea    -0x18(%rbp),%rdx
lea    -0x14(%rbp),%rax
mov    %rdx,%rsi
mov    %rax,%rdi
callq  4008ec <_Z15call_by_val_ptrPiS_>

註意到,lea是取地址,所以這裏實際也是將地址傳進去了,但為什麽沒有完畢交換?

由於不滿足條件2:將值存入實際地址。

call_by_val_ptr中的交換。從匯編代碼就能看出,僅僅是交換了指針指向的地址,而沒有通過將值存入這個地址而改變地址中的值








程序設計基石與實踐系列之按值傳遞還是按引用