1. 程式人生 > >函式傳值 傳地址 傳引用的區別

函式傳值 傳地址 傳引用的區別

傳值,   
  是把實參的值賦值給行參   
  那麼對行參的修改,不會影響實參的值   
   
  傳地址   
  是傳值的一種特殊方式,只是他傳遞的是地址,不是普通的如int   
  那麼傳地址以後,實參和行參都指向同一個物件   
   
  傳引用   
  真正的以地址的方式傳遞引數   
  傳遞以後,行參和實參都是同一個物件,只是他們名字不同而已   
  對行參的修改將影響實參的值

-----------------------------------------------------------------------------------

覺得從函式呼叫的角度理解比較好   
   
  傳值:   
  函式引數壓棧的是引數的副本。   
  任何的修改是在副本上作用,沒有作用在原來的變數上。   
   
  傳指標:   
  壓棧的是指標變數的副本。   
  當你對指標解指標操作時,其值是指向原來的那個變數,所以對原來變數操作。   
   
  傳引用:   
  壓棧的是引用的副本。由於引用是指向某個變數的,對引用的操作其實就是對他指向的變數的操作。(作用和傳指標一樣,只是引用少了解指標的草紙)  

-----------------------------------------------------------------------------------
函式引數傳遞機制的基本理論   
    函式引數傳遞機制問題在本質上是呼叫函式(過程)和被呼叫函式(過程)在呼叫發生時進行通訊的方法問題。基本的引數傳遞機制有兩種:值傳遞和引用傳遞。以下討論稱呼叫其他函式的函式為主調函式,被呼叫的函式為被調函式。   
    值傳遞(passl-by-value)過程中,被調函式的形式引數作為被調函式的區域性變數處理,即在堆疊中開闢了記憶體空間以存放由主調函式放進來的實參的值,從而成為了實參的一個副本。值傳遞的特點是被調函式對形式引數的任何操作都是作為區域性變數進行,不會影響主調函式的實參變數的值。   
    引用傳遞(pass-by-reference)過程中,被調函式的形式引數雖然也作為區域性變數在堆疊中開闢了記憶體空間,但是這時存放的是由主調函式放進來的實參變數的地址。被調函式對形參的任何操作都被處理成間接定址,即通過堆疊中存放的地址訪問主調函式中的實參變數。正因為如此,被調函式對形參做的任何操作都影響了主調函式中的   
  實參變數。   


-----------------------------------------------------------------------------------

僅討論一下值傳遞和引用:   
  所謂值傳遞,就是說僅將物件的值傳遞給目標物件,就相當於copy;系統將為目標物件重新開闢一個完全相同的記憶體空間。   
  所謂引用,就是說將物件在記憶體中的地址傳遞給目標物件,就相當於使目標物件和原始物件對應同一個記憶體儲存空間

。此時,如果對目標物件進行修改,記憶體中的資料也會改變。

一、函式引數傳遞機制的基本理論

  函式引數傳遞機制問題在本質上是呼叫函式(過程)和被呼叫函式(過程)在呼叫發生時進行通訊的方法問題。基本的引數傳遞機制有兩種:值傳遞和引用傳遞。以下討論稱呼叫其他函式的函式為主調函式,被呼叫的函式為被調函式。

  值傳遞(passl-by-value)過程中,被調函式的形式引數作為被調函式的區域性變數處理,即在堆疊中開闢了記憶體空間以存放由主調函式放進來的實參的值,從而成為了實參的一個副本。值傳遞的特點是被調函式對形式引數的任何操作都是作為區域性變數進行,不會影響主調函式的實參變數的值。

  引用傳遞

(pass-by-reference)過程中,被調函式的形式引數雖然也作為區域性變數在堆疊中開闢了記憶體空間,但是這時存放的是由主調函式放進來的實參變數的地址。被調函式對形參的任何操作都被處理成間接定址,即通過堆疊中存放的地址訪問主調函式中的實參變數。正因為如此,被調函式對形參做的任何操作都影響了主調函式中的實參變數。

二、 C語言中的函式引數傳遞機制

  在C語言中,值傳遞是唯一可用的引數傳遞機制。但是據筆者所知,由於受指標變數作為函式引數的影響,有許多朋友還認為這種情況是引用傳遞。這是錯誤的。請看下面的程式碼:

int swap(int *x, int *y)

{

int temp;

temp = *x; *x = *y; *y = temp;

return temp;

}

void main()

{

int a = 1, b = 2;

int *p1 = &a;

int *p2 = &b;

swap(p1, p2)

}

  函式swap以兩個指標變數作為引數,當main()呼叫swap時,是以值傳遞的方式將指標變數p1p2的值(也就是變數ab的地址)放在了swap在堆疊中為形式引數xy開闢的記憶體單元中。

 這裡我們可以得到以下幾點:

1程序的堆疊儲存區是主調函式和被調函式進行通訊的主要區域。

2 C語言中引數是從右向左進棧的。

3被調函式使用的堆疊區域結構為:

    區域性變數(如temp

    返回地址

    函式引數

    低地址

    高地址

4由主調函式在呼叫後清理堆疊。

5函式的返回值一般是放在暫存器中的。

  這裡尚需補充說明幾點:一是引數進棧的方式。對於內部型別,由於編譯器知道各型別變數使用的記憶體大小故直接使用push指令;對於自定義的型別(如structure),採用從源地址向目的(堆疊區)地址進行位元組傳送的方式入棧。二是函式返回值為什麼一般放在暫存器中,這主要是為了支援中斷;如果放在堆疊中有可能因為中斷而被覆蓋。三是函式的返回值如果很大,則從堆疊向存放返回值的地址單元(由主調函式在呼叫前將此地址壓棧提供給被調函式)進行位元組傳送,以達到返回的目的。對於第二和第三點,《Thinking in C++》一書在第10章有比較好的闡述。四是一個顯而易見的結論,如果在被調函式中返回區域性變數的地址是毫無意義的;因為區域性變數存於堆疊中,呼叫結束後堆疊將被清理,這些地址就變得無效了。

三、 C++語言中的函式引數傳遞機制

眾所周知,在c++中呼叫函式時有三種引數傳遞方式:

1)傳值呼叫;

2)傳址呼叫(傳指標);

3)引用傳遞;

實際上,還有一種引數傳遞方式,就是全域性變數傳遞方式。這裡的“全域性”變數並不見得就是真正的全域性的,所有程式碼都可以直接訪問的,只要這個變數的作用域足夠這兩個函式訪問就可以了,比如一個類中的兩個成員函式可以使用一個成員變數實現引數傳遞,或者使用static關鍵字定義,或者使用namespace進行限制等,而這裡的成員變數在這種意義上就可以稱作是“全域性”變數(暫時還沒有其它比“全域性”更好的詞來描述)。當然,可以使用一個類外的真正的全域性變數來實現引數傳遞,但有時並沒有必要,從工程上講,作用域越小越好。這種方式有什麼優點呢?

效率高!

的確,這種效率是所有引數傳遞方式中效率最高的,比前面三種方式都要高,無論在什麼情況下。但這種方式有一個致命的弱點,那就是對多執行緒的支援不好,如果兩個程序同時呼叫同一個函式,而通過全域性變數進行傳遞引數,該函式就不能夠總是得到想要的結果。

下面再分別討論上面三種函式傳遞方式。

1. 從功能上。按值傳遞在傳遞的時候,實參被複制了一份,然後在函式體內使用,函式體內修改引數變數時修改的是實參的一份拷貝,而實參本身是沒有改變的,所以如果想在呼叫的函式中修改實參的值,使用值傳遞是不能達到目的的,這時只能使用引用或指標傳遞。例如,要實現兩個數值交換。

void swap(int aint b)

void main(){

int a=1b=2

swap(a b)

}

這樣,在main()函式中的a b值實際上並沒有交換,如果想要交換隻能使用指標傳遞或引用傳遞,如:

void swap(int paint pb)

void swap(int&raint&rb)

2.從傳遞效率上。這裡所說傳遞效率,是說呼叫被調函式的程式碼將實參傳遞到被調函式體內的過程,正如上面程式碼中,這個過程就是函式main()中的a b傳遞到函式swap()中的過程。這個效率不能一概而論。對於內建的intcharshort long float4位元組或以下的資料型別而言,實際上傳遞時也只需要傳遞14個位元組,而使用指標傳遞時在32cpu中傳遞的是32位的指標,4個位元組,都是一條指令,這種情況下值傳遞和指標傳遞的效率是一樣的,而傳遞doublelong long8位元組的資料時,在32cpu中,其傳值效率比傳遞指標要慢,因為8個位元組需要2次取完。而在64位的cpu上,傳值和傳址的效率是一樣的。再說引用傳遞,這個要看編譯器具體實現,引用傳遞最顯然的實現方式是使用指標,這種情況下與指標的效率是一樣的,而有些情況下編譯器是可以優化的,採用直接定址的方式,這種情況下,效率比傳值呼叫和傳址呼叫都要快,與上面說的採用全域性變數方式傳遞的效率相當。

再說自定義的資料型別,classstruct定義的資料型別。這些資料型別在進行傳值呼叫時生成臨時物件會執行建構函式,而且當臨時物件銷燬時會執行解構函式,如果建構函式和解構函式執行的任務比較多,或者傳遞的物件尺寸比較大,那麼傳值呼叫的消耗就比較大。這種情況下,採用傳址呼叫和採用傳引用呼叫的效率大多數下相當,正如上面所說,某些情況下引用傳遞可能被優化,總體效率稍高於傳址呼叫。

3. 從執行效率上講。這裡所說的執行效率,是指在被呼叫的函式體內執行時的效率。因為傳值呼叫時,當值被傳到函式體內,臨時物件生成以後,所有的執行任務都是通過直接定址的方式執行的,而指標和大多數情況下的引用則是以間接定址的方式執行的,所以實際的執行效率會比傳值呼叫要低。如果函式體內對引數傳過來的變數進行操作比較頻繁,執行總次數又多的情況下,傳址呼叫和大多數情況下的引用引數傳遞會造成比較明顯的執行效率損失。

綜合23兩種情況,具體的執行效率要結合實際情況,通過比較傳遞過程的資源消耗和執行函式體消耗之和來選擇哪種情況比較合適。而就引用傳遞和指標傳遞的效率上比,引用傳遞的效率始終不低於指標傳遞,所以從這種意義上講,在c++中進行引數傳遞時優先使用引用傳遞而不是指標。

4. 從型別安全上講。值傳遞與引用傳遞在引數傳遞過程中都執行強型別檢查,而指標傳遞的型別檢查較弱,特別地,如果引數被宣告為 void ,那麼它基本上沒有型別檢查,只要是指標,編譯器就認為是合法的,所以這給bug的產生製造了機會,使程式的健壯性稍差,如果沒有必要,就使用值傳遞和引用傳遞,最好不用指標傳遞,更好地利用編譯器的型別檢查,使得我們有更少的出錯機會,以增加程式碼的健壯性。

這裡有個特殊情況,就是對於多型的情況,如果形參是父類,而實參是子類,在進行值傳遞的時候,臨時物件構造時只會構造父類的部分,是一個純粹的父類物件,而不會構造子類的任何特有的部分,因為辦有虛的解構函式,而沒有虛的建構函式,這一點是要注意的。如果想在被調函式中通過呼叫虛擬函式獲得一些子類特有的行為,這是不能實現的。

5. 從引數檢查上講。一個健壯的函式,總會對傳遞來的引數進行引數檢查,保證輸入資料的合法性,以防止對資料的破壞並且更好地控制程式按期望的方向執行,在這種情況下使用值傳遞比使用指標傳遞要安全得多,因為你不可能傳一個不存在的值給值引數或引用引數,而使用指標就可能,很可能傳來的是一個非法的地址(沒有初始化,指向已經delete掉的物件的指標等)。所以使用值傳遞和引用傳遞會使你的程式碼更健壯,具體是使用引用還是使用,最簡單的一個原則就是看傳遞的是不是內建的資料型別,對內建的資料型別優先使用值傳遞,而對於自定義的資料型別,特別是傳遞較大的物件,那麼請使用引用傳遞。

6. 從靈活性上。無疑,指標是最靈活的,因為指標除了可以像值傳遞和引用傳遞那樣傳遞一個特定型別的物件外,還可以傳遞空指標,不傳遞任何物件。指標的這種優點使它大有用武之地,比如標準庫裡的time( )函式,你可以傳遞一個指標給它,把時間值填到指定的地址,你也可以傳遞一個空指標而只要返回值。

以上討論了四種引數傳遞方式的優缺點,下面再討論一下在引數傳遞過程中一些共同的有用的技術。

1. const關鍵字。當你的引數是作為輸入引數時,你總不希望你的輸入引數被修改,否則有可能產生邏輯錯誤,這時可以在宣告函式時在引數前加上const關鍵字,防止在實現時意外修改函式輸入,對於使用你的程式碼的程式設計師也可以告訴他們這個引數是輸入,而不加const關鍵字的引數也可能是輸出。例如strlen,你可以這樣宣告

int strlen(char str)

功能上肯定沒有什麼問題,但是你想告訴使用該函式的人,引數str是一個輸入引數,它指向的資料是不能被修改的,這也是他們期望的,總不會有人希望在請人給他數錢的時候,裡面有張100的變成10塊的了,或者真鈔變成假鈔了,他們希望有一個保證,說該函式不會破壞你的任何資料,宣告按如下方式便可讓他們放心:

int strlen(const char str)

可不可以給str本身也加一個限制呢,如果把地址改了數得的結果不就錯了嗎?總得給人點兒自由吧,只要它幫你數錢就行了,何必介意他怎麼數呢?只要不破壞你的錢就ok了,如果給str一個限制,就會出現問題了,按照上面的宣告,可以這樣實現:

int strlen(const char str)

{int cnt

if( !str) return 0

cnt = 0

while( (str++) ){

++cnt

}

return cnt

}

可是,如果你硬要把宣告改成

int strlen(const char const str)

上面的函式肯定就執行不了了,只能改用其它的實現方式,但這個不是太有必要。只要我們保護好我們的錢就行了,如果它數不對,下次我次不讓它數,再換個人就是了。

對於成員函式,如果我們要顯示給客戶程式碼說某個成員函式不會修改該物件的值,只會讀取某些內容,也可以在該函式宣告中加一個const.

classperson

{......

public:

unsigned char age( void ) const// 看到const就放心了,這個函式肯定不會修改m_age

private:

unsigned char m_age// 我認為這個型別已經足夠長了,如果覺得不改可以改為unsigned long

}

2. 預設值。個人認為給引數新增一個預設值是一個很方便的特性,非常好用,這樣你就可以定義一個具有好幾個引數的函式,然後給那些不常用的引數一些預設值,客戶程式碼如果認為那些預設值正是他們想要的,呼叫函式時只需要填一些必要的實參就行了,非常方便,這樣就省去了過載好幾個函式的麻煩。可是我不明白c#為什麼把這個特性給去掉了,可能是為了安全,這樣就要求每次呼叫函式時都要顯示地給函式賦實參。所以要注意,這可是個雙刃劍,如果想用使刀的招跟對手武鬥,很可能傷到自己。

3.引數順序。當同個函式名有不同引數時,如果有相同的引數儘量要把引數放在同一位置上,以方便客戶端程式碼。

c++ 中經常使用的是常量引用,如將swap2改為:

Swap2(const int& x; const int& y)

  這時將不能在函式中修改引用地址所指向的內容,具體來說,xy將不能出現在""的左邊。