說起函式,就不免要談談函式的引數和返回值。一般的,我們習慣把函式看作乙個處理的封裝(比如黑箱),而引數和返回值一般對應著處理過程的輸入和輸出。這種情況下,引數和返回值都是值型別的,也就是說,函式和它的呼叫者的資訊交流方式是用過資料的拷貝來完成,即我們習慣上稱呼的「值傳遞」。但是自從引入了「引用」的概念後,函式的傳統模型就不再那麼「和諧」了。引用的傳遞可以允許函式和呼叫者共享資料物件,它們之間的資訊交流不再使用資訊拷貝的方式,而是使用更有效率的資訊共享的方式,引用導致函式的引數並有輸入和輸出的雙重功能。然而,事物總有兩面性,資訊共享帶來方便的同時也帶來了一定的不安全性。我們這裡並不討論函式的使用和設計,我們關注與函式引數和返回值的傳遞方式。
對於內建資料型別的引數和返回值,函式實際引數的傳遞一般是通過壓棧完成,函式執行時會從棧內取出引數的值進行計算。在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不是自定義型別,而是int型別的話,這段測試**會有怎樣的效果。
mov eax,[a];//取出a的值事實是這樣的嗎?我們看一下vs2010的反彙編。push eax;//a值進棧
call fun;//呼叫fun
add esp,4;//恢復棧指標
mov [a],eax;//返回值寫入a;//而fun內部為非也是把引數x的值寫入eax,然後返回而已。
mov eax,[x]
ret
和我們的預期完全一致!
現在,我們回到物件的問題上來。由於物件是值傳遞方式,因此,物件傳遞之前需要進行一次物件拷貝(從原物件到實參)。函式呼叫結束後還需要將返回值物件進行一次拷貝。我們看看vs2010的處理方式。
物件a定義是需要呼叫它的建構函式a::a(0a1112ch)。
物件a包含三個整形資料成員,因此它的大小是12(0x0c)位元組。sub esp,0ch正是開闢12個位元組儲存從物件a拷貝出來的12位元組資料。
mov eax,esp記錄了被拷貝的引數物件的位址(this指標),push eax壓入的是a的位址,也就是拷貝構造函式呼叫時引數物件的位址(引用)。拷貝建構函式(a::a(0a11131h))會把a位址記錄的物件資料拷貝到ecx記錄的this對應的引數物件內。呼叫結束後,使用ret
4指令將剛才壓入的a的位址彈出棧,這樣棧頂儲存著完整引數物件(剛才開闢的12個位元組)。這樣引數物件被完整的複製出來了。
push ecx壓入了記憶體位址ebp-58h,這個位址既不是a的位址,也不是拷貝出引數物件的位址,而是要儲存返回物件的位址!呼叫fun之前將該位址壓棧,就是為了儲存fun處理結束後的返回值物件。fun呼叫結束後將esp指標恢復了16位元組,正好是引數物件的大小(12位元組)加上返回值物件的位址(4位元組)之和!要獲得fun的返回值,直接訪問eax即可,因為它儲存著返回值物件的位址(ebp-58h)!
最後一步是物件的賦值,這裡需要呼叫物件的賦值運算子過載函式。而引數正是剛才fun呼叫結束後eax的值,因為它儲存了返回值物件的位址。ecx記錄this指標,正是被賦值物件的位址(a的位址)。賦值運算子過載函式呼叫結束後,完成返回值物件的賦值操作。
按照編譯器產生的fun函式的語義,我們使用高階語言可以將它的意思描述如下。
a a;//定義a
a.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的一點。所有的變數都可以理解是記憶體中乙個物件...