物件的傳值與返回
說起函式,就不免要談談函式的引數和返回值。一般的,我們習慣把函式看作乙個處理的封裝(比如黑箱),而引數和返回值一般對應著處理過程的輸入和輸出。這種情況下,引數和返回值都是值型別的,也就是說,函式和它的呼叫者的資訊交流方式是用過資料的拷貝來完成,即我們習慣上稱呼的「值傳遞」。但是自從引入了「引用」的概念後,函式的傳統模型就不再那麼「和諧」了。引用的傳遞可以允許函式和呼叫者共享資料物件,它們之間的資訊交流不再使用資訊拷貝的方式,而是使用更有效率的資訊共享的方式,引用導致函式的引數並有輸入和輸出的雙重功能。然而,事物總有兩面性,資訊共享帶來方便的同時也帶來了一定的不安全性。我們這裡並不討論函式的使用和設計,我們關注與函式引數和返回值的傳遞方式。
對於內建資料型別的引數和返回值,函式實際引數的傳遞一般是通過壓棧完成,函式執行時會從棧內取出引數的值進行計算。在
32處理器上,
push
指令一次只能壓入
4個位元組的資料,那麼對於
long long
就需要兩次壓棧指令了,而
double
型別引數就需要
sub esp,8
結合mov
指令完成引數進棧的操作。函式帶有返回值時,若返回值不大於
4位元組,則會把返回值儲存在
eax暫存器中,而
long long
型別返回值回儲存在
edx:eax
暫存器中,
double
型別的資料會被協處理器棧儲存。
相對於內建型別的引數傳遞和返回值,物件的傳值和返回可能更複雜一點。當然,如果使用物件的引用或者指標作為引數傳遞和返回值的方式,這裡和上述的內建型別並無多大區別,因為指標總是
4個位元組。如果不使用引用和指標,單純傳遞純粹的物件時,編譯器會如何處理呢?
為此,我們定義乙個簡單的類
a,為了防止編譯器對我們的**優化處理(
參考我的前一篇博文
),我們自己定義建構函式、複製建構函式和賦值運算子過載函式。
class a
a(const a&a)
const a&operator=(const a&a)
};定義乙個簡單的具有物件引數和返回值的函式,以及測試**。
a fun(a x)
a a;
a=fun(a);
試想一下,如果a
不是自定義型別,而是
int型別的話,這段測試**會有怎樣的效果。
mov eax,[a];
//取出a的值
push eax;
//a值進棧
call fun;
//呼叫fun
add esp,4
;//恢復棧指標
mov [a],eax;
//返回值寫入a
;//而fun內部無非也是把引數x的值寫入eax,然後返回而已。
mov eax,[a]
ret事實是這樣的嗎?我們看一下vs2010
的反彙編。
和我們的預期完全一致!
現在,我們回到物件的問題上來。由於物件是值傳遞方式,因此,物件傳遞之前需要進行一次物件拷貝(從原物件到實參)。函式呼叫結束後還需要將返回值物件進行一次拷貝。我們看看
vs2010
的處理方式。
物件a定義是需要呼叫它的建構函式
物件a包含三個整形資料成員,因此它的大小是12(
0x0c
)位元組。
sub esp,0ch
正是開闢
12個位元組儲存從物件
a拷貝出來的
12位元組資料。
mov ecx,esp
記錄了被拷貝的引數物件的位址(
this
指標),
push eax
壓入的是
a的位址,也就是拷貝構造函式呼叫時引數物件的位址(引用)。拷貝建構函式(
a::a(0a11131h)
)會把a
位址記錄的物件資料拷貝到
ecx記錄的
this
對應的引數物件內。呼叫結束後,使用
ret 4
指令將剛才壓入的
a的位址彈出棧,這樣棧頂儲存著完整引數物件(剛才開闢的
12個位元組)。這樣引數物件被完整的複製出來了。
壓入了記憶體位址
ebp-58h
,這個位址既不是
a的位址,也不是拷貝出引數物件的位址,而是要儲存返回物件的位址!呼叫
fun之前將該位址壓棧,就是為了儲存
fun處理結束後的返回值物件。
fun呼叫結束後將
esp指標恢復了
16位元組,正好是引數物件的大小(
12位元組)加上返回值物件的位址(
4位元組)之和!要獲得
fun的返回值,直接訪問
eax即可,因為它儲存著返回值物件的位址(
ebp-58h
)!最後一步是物件的賦值,這裡需要呼叫物件的賦值運算子過載函式。而引數正是剛才
fun呼叫結束後
eax的值,因為它儲存了返回值物件的位址。
ecx記錄
this
指標,正是被賦值物件的位址(
a的位址)。賦值運算子過載函式呼叫結束後,完成返回值物件的賦值操作。
按照編譯器產生的
fun函式的語義,我們使用高階語言可以將它的意思描述如下。
a a;//
定義aa.a();//
預設構造
a x;//
開闢x的12位元組空間
x.(a);//
物件複製到實際引數
a*pret=&ret;//
取返回值物件位址(已經開闢過了)
fun(pret,x);//
傳遞返回值指標pret和引數物件x
a=*pret;//
把返回值物件賦值給物件a
//這樣原本fun的函式形式就有所變化了。
void fun(a*pret,a x)
我們看一下fun
的彙編**。
引數物件的位址被
x記錄了下來,
ebp+8
記錄的正是函式第乙個引數的內容,即返回值物件的位址!在拷貝構造函式呼叫之前,
ecx儲存的
this
指標正是返回值物件的,進棧的引數是
x的位址,和我們預期的一樣!
因此,我們可以針對物件的傳值和返回得出如下結論:
1. 物件引數傳遞之前需要進行一次物件拷貝,將原物件的內容完整的拷貝到引數物件內部,函式執行時訪問的是引數物件,而不是原物件。
2. 物件返回時,也需要將函式處理的結果進行一次物件拷貝,不過被拷貝的返回值物件記憶體已經在函式呼叫之前已經開闢出來了,函式只需要記錄它的位址即可,然後呼叫拷貝建構函式初始化它。
3. 函式呼叫結束後,
eax儲存了返回值物件的位址,供呼叫者使用。
通過本文的描述,相信讀者對物件作為函式引數和返回值時,編譯器的內部處理機制有個更清晰的了解。
物件的傳值與返回
說起函式,就不免要談談函式的引數和返回值。一般的,我們習慣把函式看作乙個處理的封裝 比如黑箱 而引數和返回值一般對應著處理過程的輸入和輸出。這種情況下,引數和返回值都是值型別的,也就是說,函式和它的呼叫者的資訊交流方式是用過資料的拷貝來完成,即我們習慣上稱呼的 值傳遞 但是自從引入了 引用 的概念後...
類物件的「傳值」與「傳引用」
傳值 就是通過值來傳遞乙個物件,這個過程需要拷貝建構函式來進行。而 傳引用 實質上就是一種指標傳遞。兩種傳遞方式在使用上存在效率問題和 切割 問題。1 效率 而前所述,傳值 需要呼叫拷貝建構函式。例如 class ctest ctest const ctest ref ctest ctest fun...
傳值與傳引用
python的函式傳值和傳引用,和c c 語言是一樣的。在開始之前,我們有必要分清一下python的一些基礎概念。首先要說的是 變數 與 物件 在python中,型別屬於物件,變數是沒有型別的,這正是python的語言特性,也是吸引著很多pythoner的一點。所有的變數都可以理解是記憶體中乙個物件...