# 函數中資料的傳遞 - 傳值,傳址,傳參考
函數中資料的傳遞:傳值、傳址、傳參考
以下的說明是每種程式語言都通用的觀念,但不同程式語言有不同的語法,以及不同的程式語言不一定都支援全部的傳遞方式,要看你使用的是哪種程式語言。
把資料輸入到函數中進行運算,在程式語言中有個專業的術語叫做「傳遞」(pass)。例如函數Y(x) = 3x + 1,若我們設定x = 2,則把x = 2丟進去Y(x)函數中做運算,術語就稱為傳遞,而Y(x)函數輸出的結果是7,數學式寫作Y(2) = 7,應該很容易理解。
另外,關於函數的「引數」與「參數」,這兩個重要觀念很多人說不清楚,可能連程式經驗豐富的人都還是對它們的區別一知半解。但這是很重要的觀念,必須要徹底理解,我有信心這篇文章應該是你看過最清楚的解說了。
所以,在解說函數傳遞的三種方式之前,先釐清一下何謂函數的引數和參數。
-----------------------------------------------------------------------------------------
函數的引數(argument) v.s. 函數的參數(parameter)
在使用函數(或稱函式或副程式)時,往往會遇到這兩個名詞,時常造成混淆,每人翻譯的也不盡相同,但由於非常重要,必須做好釐清。
1. 參數(parameter)是用於「函數的宣告」,也就是在定義函數,或者宣告函數的原型時使用。由於必須要在函數的小括號內寫上參數的型態,故又稱為「形式參數」,可記為「參數的型式」。
A. 定義函數時:
void swapvalue(int x, int y) // x , y稱為參數(parameter),小括號內要寫上參數的型態
{ 函數定義域的程式碼,以參數x、y來撰寫敘述 ………
}
2. 引數(argument)是用於「呼叫函式」,也就是「執行函數」時的「輸入值」。
B. 呼叫(執行)函數時:
swapvalue(a,b); // ()中的a , b稱為引數(argument),不需要寫上型態
我們使用者是透過呼叫的方式來執行函數,所以我們是在B.中設定兩個引數a、b來使用這個函數。當電腦看到引數a、b後,便會將a、b傳遞給A.內的參數x、y,以執行函數所定義的運算,所以傳遞的方向是:「由引數(a、b)傳遞給參數(x、y)」。
我們以一個例子來詳述。swap這個英文單字是「交換」的意思,以下我們就自訂一個名為swap()的函數,這個函數可以把我們輸入的數值a、b做交換。例如設定輸入a=5、b=8,則丟進去swap()函數做運算後,我們預期它會輸出a=8、b=5的結果。以下我們就自訂一個swap()函數,來展示「傳遞值」和「傳遞記憶體位址」,以及「傳遞參考」三種方式有什麼不同。
-----------------------------------------------------------------------------------------
範例一,「傳遞值」(pass by value)- 以C語言撰寫:
#include <stdio.h> // 匯入基本輸入 & 輸出的函數庫
void swap(int x,int y) // 定義不需要回傳值(void)的swap()函數
{
int temp; //「交換兩數」的演算法,必須要再宣告一個「第三者」的變數temp(自己
取的名字,但也要有意義),透過它當作一個暫時性儲存數值的容器
temp = x ; // 先把x的值丟到變數temp中
x = y ; // 以y的值取代x原本的值
y = temp ; // 再以變數temp的值取代y原本的值,完成交換
}
int main() // 上面事先定義好了swap()函數,而main()函數之內才是主程式
{
int a=5 , b=8; // 宣告兩個整數型態的變數a、b並分別設定其初始值
swap( a , b ); // 把引數a、b的值(value)傳遞給函數swap()的參數
printf("%d %d\n",a,b); // 在螢幕上顯示變數a、b經過運算後的新值
return 0; // 使main( )函數正常結束
}
pass by value又稱作call by value,但我覺得稱為pass by value比較合乎意思,中文翻譯作「傳遞值」或「傳值呼叫」。
以上程式碼裡面有交換兩個數字的演算法,雖然第一次見到的人需要花點時間理解,但也不難。所以,透過自定義的swap( )函數進行運算,我們預期輸出的結果會是a=8、b=5。但實際執行程式後,發現結果竟然還是a=5、b=8,變數a、b的數值並沒有進行交換,難不成是演算法寫錯,或者是complier錯誤了?其實都不是。
真正的原因是,因為這裡我們使用的是「傳值」的傳遞方法,也就是說當我們呼叫swap()函數時,電腦會把變數a=5、b=8的值(value)「複製」一份到函數的參數,也就是說它實際上是做了x=5、y=8的動作,也就是把a的值複製給x的值、把b的值複製給y的值,然後在函數的定義中互相交換x、y的值 … 看出來了嗎?所以程式實際交換的是參數x和y的值,而不是引數a和b的值,所以最後「printf("%d %d\n",a,b);」,要求電腦在螢幕上顯示a和b的值,當然就沒有變化了。
而且因為變數x、y的「生命週期」只在這個函數的定義域,也就是大括號{ }裡面有效而已,所以只互換x、y的值是沒有用的。
請參考下面兩張圖便可明白,並且注意記憶體位址(memory address)都是用16進位的數字來表示的。
![](https://i.imgur.com/me7uIlo.jpg)
![](https://i.imgur.com/W59TMVQ.jpg)
-----------------------------------------------------------------------------------------
要解決這個問題,要使用「pass by address」的方式來做傳遞,又稱作call by address,但我覺得稱為pass by address比較合乎意思,中文翻譯作「傳遞(記憶體)位址」或「傳址呼叫」。
範例二,「傳遞記憶體位址」(pass by address)- 以C語言撰寫:
#include <stdio.h> // 匯入基本輸入 & 輸出的函數庫
void swap(int *x,int *y) // 定義不需要回傳值的swap()函數,並且宣告兩個指標型態的參數
{
int temp; // 交換兩數的演算法,使用「*」運算子來實作pass by address
temp = *x ;
*x = *y ;
*y = temp ; // 注意演算法使用「*」(取值)運算子
}
int main() // 上面事先定義好了swap()函數,而main()函數之內才是主程式
{
int a=5 , b=8; // 宣告兩個整數型態的變數a、b,並分別設定其初始值
swap( &a , &b ); // 使用取址運算子「&」,把引數a、b的「記憶體位址」傳遞到參數x,y中
printf("%d %d\n",a,b); // 在螢幕上顯示變數a、b經過運算後的新值
return 0; // 使main( )函數正常結束
}
使用pass by address的方式來傳遞,需要在函數的定義中宣告兩個參數,同時也是指標變數的x、y,準備用來存放a、b的記憶體位址。之後,我們把引數a、b的記憶體位址複製到參數x、y中,並在函數的定義中交換x、y所存放的值(value),使得x、y指向的對象互換。所以執行完函數swap()後,用語法「printf("%d %d\n",a,b);」就能正確在螢幕上顯示我們想要的a=8、b=5,成功完成互換。
若覺得以上文字描述有點抽象,請參考下面兩張圖,便可明白。
![](https://i.imgur.com/H0aXqsZ.jpg)
![](https://i.imgur.com/kRRWw5d.jpg)
註:從以上可以看出,事實上pass by address也是在傳值,只不過這個值剛好是記憶體位址(指標)而已,所以其實嚴格的說,根本沒有pass by address這個東西。不過為了解說的方便,還有為了要做出區隔,我們還是習慣說成pass by address。其實,只要知道它的原理即可,不必過度糾結於使用的術語。
經過以上的兩個範例,讀者可以多多去體會函數的「傳值」與「傳址」有什麼不同,並且慢慢去理解變數、值、指標(記憶體位址)和指標變數之間的差異。
以下把經常會混淆的地方,做個小整理:
1. 定義函數時,函數()內填寫的輸入值稱為「參數」(parameters),參數要宣告它的資料型態。
而{ }內填寫的內容,稱為函數的實作程式碼,也就是定義這個函數到底要做什麼事。例如:
int add(int x, int y){ return x + y ; }
就是定義一個可以計算兩個整數相加的函數add(),其中x、y稱為此函數的參數(parameters),並把大括號{ }內稱為函數的定義域。
2. 呼叫(執行)函數時,函數()內填寫的輸入值稱為「引數」(arguments)。例如:
int sum ;
sum = add(3,5);
sum的值就會是8,其中3和5稱為此函數的引數(arguments),引數不需要宣告它的資料型態。
3. 參數和引數是不同的,不能混為一談。
4. 注意,無論是傳值、傳址,或是傳參考,一律都是依據參數和引數的「對應位置」來傳遞的。也就是說,系統並不管你給參數和引數取的名字,只會依照它們所互相對應的前後位置來進行傳遞。
5. 由此可見,程式的執行順序會在函數的呼叫與定義區塊之間產生跳躍與轉折,雖然會降低一點執行效率,但可以得到更好的可讀性和維護性,並且能夠重複利用函數,讓我們人類更容易理解與撰寫程式,並且能用系統化的方式管理程式,好處多多。
-----------------------------------------------------------------------------------------
補充:範例三,「傳遞參考」(pass by reference)- 以C++撰寫:
這裡補充除了傳值和傳址以外的第三種方式,即「傳遞參考」(pass by reference或稱call by reference)。傳遞參考其實和傳遞記憶體位址很像,不過由於傳遞參考並不會分配記憶體空間給參數x、y,所以需要的記憶體空間比較少,請看下圖即可明白。
![](https://i.imgur.com/nDaule9.jpg)
由於傳遞參考的方式,系統並不會給x、y分配記憶體空間,所以其實x、y和a、b是同一個東西,也就是說x、y是a、b的另一個名稱而已,術語叫做「alias」(別名,綽號),就像你給王小明取綽號叫「馬屁精」、「搗蛋鬼」、「遲到大王」… 是同樣的道理,無論你叫他什麼綽號,你都是在說同一個王小明,所以pass by reference的方式就是一種alias的應用。
pass by reference是C++才有的功能,C、Java、甚至C# 都沒有pass by reference。而實際的程式碼寫法,相較於pass by value是不加任何符號、pass by address是用取值運算子(*),而pass by reference則是用取址運算子(&)來撰寫,並且在細節上稍有不同:
#include <stdio.h> // 匯入基本輸入 & 輸出的函數庫
void swap (int &x , int &y){ // 交換兩數的演算法,使用「&」運算子來宣告參數
int temp = x; // 演算法不需使用「*」運算子
x = y;
y = temp;
}
int main() // 上面事先定義好了swap()函數,而main()函數之內才是主程式
{
int a=5 , b=8; // 宣告兩個整數型態的變數a、b,並分別設定其初始值
swap( a , b ); // 呼叫函數swap(a,b)時,引數像平常一樣直接給值就好
printf("%d %d\n",a,b); // 在螢幕上顯示變數a、b經過運算後的新值
return 0; // 使main( )函數正常結束
}
使用pass by reference的寫法,我們一樣可以順利交換a、b的值。
以上pass by reference的程式碼,必須要特別注意它和pass by address不同的地方,主要是在定義函數與呼叫(執行)函數時的語法不同。
-----------------------------------------------------------------------------------------
理解了這三者之間的差異,便可進一步探討它們使用場合的區別:
傳遞值(pass by value):
觀念和使用相對單純,但由於一次只能傳遞一個數值,若是遇到一次需要傳遞整個資料的包裹,則使用傳遞值會非常難以實現。
傳遞記憶體位址(pass by address):
藉由傳遞資料所在的記憶體位址來傳遞資料,很類似實體世界中「不動產」的傳遞模式。雖然操作和觀念較為間接,但對於需要傳遞整個資料包裹的場合,無論其資料包裹有多大、資料有多少,只需要傳遞一個(起始的)記憶體位址即可,非常有效率。
傳遞參考(pass by reference):
和傳遞記憶體位址很類似,不過比較節省一點記憶體空間。不一定每種程式語言都有支援傳遞參考的方式,需要確認使用的程式語言有沒有支援傳遞參考。
pass by value是大家最容易理解,不需說明就會的一種方式,但應用上有所侷限,例如交換兩個變數的值就無法做到,看完本章相信大家應該都理解了,而且也不適用於需要傳遞大量資料的場合。
另外,我們可以發現其實pass by address和pass by reference都能達到同樣的目的,觀念都很像,兩者之間最大的差別在於記憶體的配置方式,pass by reference會比pass by address更省一點記憶體空間。
本章開頭就說過,以上三種方法pass by value、pass by address和pass by reference是不限程式語言的通用觀念,例如C語言只支援pass by value和pass by address兩種方式,C++則是三種全都支援,但在C++之後的JAVA語言(甚至C#)卻和C語言一樣只支援pass by value和pass by address兩種方式,反而不支援pass by reference。
大家或許會覺得很奇怪,怎麼會說JAVA不支援pass by reference呢?我們知道JAVA有兩大資料型態系統,一種是primitive type,使用pass by value的方式來操作記憶體,這個沒問題;但另一種reference type不就是用pass by reference的方式來操作記憶體嗎?
這就是很容易造成誤導的地方了,雖然JAVA把其中一種資料型態系統的名稱命名為reference type,但事實上它卻是使用pass by address(或者嚴格來說 … 仍是pass by value)的方式在操作記憶體。所以是名稱容易造成誤導,也就是說JAVA的reference和C++的reference雖然同名,但實際上指的是不同的東西!
這是有原因的,因為JAVA的開發者們認為把操作記憶體的方式分成3種太過複雜了,而且在JAVA開發的年代記憶體已經愈來愈便宜,硬體效能愈來愈強,所以JAVA的開發者們覺得不支援pass by reference並不會造成什麼問題,而且他們認為pass by address的觀念反而更像真正的pass by reference(觀點問題)。於是JAVA的開發者們就做了這樣的簡化和更名,於是就變成都叫reference,卻在JAVA和C++有不同定義的情況。
註:還有語法不同,但因為不同的程式語言語法本來就會不同,就算在同一個程式語言使用不同的操作,語法也會不同,因此就不特別強調了。
不過,說這是JAVA誤導使用者就不對了,因為每個程式語言都是由不同公司、不同人為了不同的目的所開發出來的,每種程式語言都有它自己的設計理念、定義和命名方式,更何況JAVA本來就是覺得C++太過複雜、很多方面不夠好,而試圖超越它,想要青出於藍勝於藍的。
話說回來,就算我們把JAVA的reference和C++的reference混為一談,我們還是能達成一樣的目的,只是記憶體空間的配置會不同罷了,但電腦會自動處理這些事情,用不著我們操心。
所以,讀者就會看到在很多JAVA的教學或書籍裡面,都把JAVA的reference和C++的reference混為一談了,也就是說都把JAVA的reference type說成是以pass by reference的方式來運作,而不是正確的pass by address,但並不會影響我們理解和實際撰寫程式,而且因為這樣教起來(用pass by reference的方式教)比較容易,所以大家就愈陷愈深,兩者真正的區別就慢慢被人淡忘了。
註:不過把pass by value和其他2個混為一談就不行了。
話雖如此,既然要學就把它的前因後果都追根究柢理解清楚,總是好的。而且讀者會碰到的程式語言也不只C、C++和JAVA,其他語言還是會碰到這些觀念的,只不過這裡把討論範圍侷限在這三種語言而已。
-----------------------------------------------------------------------------------------
補充資料:Java交換兩數數值的方法(method)實作
依照我們剛才講的東西,我們已經示範了如何用C語言定義一個函數swap()來實現兩數的交換。如果今天我們要用物件導向的Java語言,要怎麼做到這件事呢?我們知道Java只提供了pass by value的方式來做方法(method)的引數與參數之間的傳遞,再加上Java並不提供C語言的「*」和「&」運算子,那要怎麼用Java來實作兩個數的交換呢?
這邊要有一些前置觀念,首先要知道Java的資料型態可分成兩種:
1. primitive type
2. reference type
其中primitive type是在記憶體的stack區塊中直接存值(value),不會用到指標,相對來說單純許多。而reference type則是在記憶體的stack區塊中存記憶體位址(指標),來「指向」記憶體的heap區塊去存 / 取具體的物件資料。這些東西在Java的教學都一定會提到(除非那個教學很爛),讀者可以去深入研究這些觀念,非常重要。我們這邊就假設大家都已經懂得Java的這些觀念了,繼續說下去。
我們在C語言時,要定義swap()函數實現兩數的交換,必須要使用到指標。想當然,在Java中要做到一樣的事情,就要使用和指標相關的reference type。
不過,我們想交換的兩個數,基本上都是primitive type呀,例如我們想交換兩個整數a = 5、b = 8,讓它們經過swap()方法的運算之後,輸出成為a = 8、b = 5,而a和b都是primitive type的資料呀。
所以,在Java中要實作出swap()方法,就要想辦法把primitive type都「包裝」成reference type。我們很熟悉的物件、陣列或者Wrapper類別…等等,都是reference type。所以,我們若把a、b的值做成物件、陣列或者Wrapper類別…等,就可以實作出swap()方法了。
而現在依據我們的需求來判斷,把原先primitive type的值包裝成「物件」,實作上可能會比較簡單一點,當然若用其他方式也可以,但這裡只示範包裝成「物件」的作法,其他讀者可以自己舉一反三。
我們就直接以「物件導向」範式來撰寫程式碼,看看Java版本的swap()函數應該怎麼定義:
class intswap { // 宣告intswap類別,裡面有a、b兩個整數屬性,和一個swap()方法
public int a ;
public int b ;
public static void swap(intswap x){ // 定義swap()方法,參數為intswap型態的物件x
int temp = x.a ; // 將物件x的參數a存入整數變數temp中
x.a = x.b ; // 將物件x的參數b存入物件x的參數a中
x.b = temp ; // 將整數變數temp存入物件x的參數b中
}
}
public class swap_example { // 宣告一個swap_example類別
public static void main(String[ ] args){ // 程式的進入點
intswap sp = new intswap(); // 實體化intswap型態的物件,取名為sp
sp.a = 5; // 設定物件sp的屬性a為5(初始化a)
sp.b = 8; // 設定物件sp的屬性b為8(初始化b)
sp.swap(sp); // 執行物件sp的swap()方法,並把intswap型態的物件sp傳入
System.out.println("交換後,a = " + sp.a + " ,b = " + sp.b);
}
}
執行結果為:
交換後,a = 8 ,b = 5
從結果來看,物件sp執行完swap()函數之後,它的a、b兩個屬性的值都被交換了,成功。
如果使用C# 語言,由於C# 保留了指標,因此可以用和C或C++相同的方法實作出swap()方法,反而是Java要實作出swap()方法,較為困難且迂迴。