函數在c++中可能出現在三種地方,一是函數的定義,它包括了如上圖的結構;二是函數的聲明,它與函數的定義相比,沒有了函數體部分;三則是函數的調用。當然,不同的函數定義可以還會稍有不同,比如類的成員函數、内聯函數等。這裡我們主要讨論函數的調用時需要注意的一些問題。
我們将函數定義或聲明裡的參數叫形參,而在調用函數時傳入的參數叫實參。那麼根據形參類型的不同,有幾下形式的參數傳遞。
1)普通的内置類型
普通非引用類型的參數通過複制對應的實參實作形參的初始化。當用實參的副本初始化形參時,函數并沒有通路調用所傳遞的實參的本身,是以函數不可能改實參的值。比如下面的交換兩個數的程式:
上面程式中,實參為a與b,但是在調用時,v1與v2接受的是a與b的副本,是以實際上a與b的值沒有變化。
2)指針形參
函數的形參可以是指針,此時将複制實參指針,其實這類跟1)原理類似,函數内并無法改變實參的指針值。隻是函數可以通過複制到的位址改變實參指針所指向的值。
上面程式中定義的swap的形參為指針類型,main中調用swap,實際上swap并不能改變p1與p2的值,隻是改變了它們所指向的值。
3)const 形參
對于普通的非引用類型用const修飾實際上是沒有意義的,因為本來函數就不會改變實參的值。像下面的定義,實際中編譯器會忽略const的定義,而将其視為int型。
1)在上面的程式中我們看到,如果想交換兩個變量的值,通過調用普通的非引用類型形參的函數,并不能實作。用它們的指針可以,同時我們也可以用引用。
在實際調用swap時,v1與v2實際相當于a與b的另一個名字。
2)在有的時候我們需要向函數傳遞大型對象,需要使用引用形參,如果直接使用複制實參的形式可以,但是它的效率太低了,甚至有些對象是無法複制的。但是使用引用形參時,我們不希望函數改變了實參傳入的值,我們就可以使用const來限定形參。下面程式用來判斷哪個字元串更長,明顯我們不希望函數會改變字元串的内容,我們就可以用const引用型的形參。
是以,如果使用引用形參的惟一的目的是避免複制實參時,則應将形參定義為const引用。
3)在使用引用形參函數時,有兩點值得注意:
不要用const限定的實參或字面值來調用非const引用形參函數。因為這樣函數内,可以改變實參的值,這不合法。
非const引用形參隻能與完全同類型的非const對象關聯。
4)傳遞指向指針的引用
如下有下面的程式:
上面的程式依然不能改變a與b的值,但是它改變了p1與p2的值,現在p1指向了b,而p2指向了a。
1)vector和其他類型的形參:一般在這種類型作為形參時,為了避免複制應該考慮形參聲明為引用類型。c++程式員傾向于傳遞容器中需要處理的元素的疊代器來傳遞容器。
2)數組形參:由于數組不能複制,是以不能直接編寫數組類型的形參函數,一般通過傳遞指向數組的元素的指針來處理數組。值得注意的是在通過引用傳遞數組時,在調用函數時形參與實參的類型要比對。
1)沒有傳回值
很多函數并沒有傳回值,尤其是現在c++風格,習慣于把需要的結果作為引用形參。這類型函數一般沒有return語句,有時候有return是使函數中途中斷執行。
2)傳回非引用類型
這種情況在函數調用處,程式會用一個臨時變量複制函數的傳回值。
3)傳回引用
當函數傳回引用類型時,并沒有複制傳回值。相反,傳回的是對象本身。
在傳回引用這種情況下,注意不要傳回局部變量的引用,因為局部變量在函數體内定義,當函數執行完後就銷毀了,所謂的引用也就沒有意義了。同理,不要傳回指向局部變量的指針。
出現在相同作用域中的兩個函數,如果具有相同的名字而形參不同,則稱為重載函數。
1)注意區分函數重載與重複聲明
有些看起來不同的形參,本質是相同的。下面代碼中的都是重複聲明的例子
2)重載與作用域
局部聲明的函數,将屏蔽所有全局作用的同名函數。下面例子顯示,即使全局作用的函數更加比對調用的實參類型,但是仍然調用的是局部的函數。
上面程式中,将調用void print(double)函數,雖然42是int型。
3)重載确定的三個步驟
如果定義了衆多的函數重載,将存在函數調用到底與哪個重載函數相比對的問題。我們通過下面的示例代碼來說明問題:
第一步:确定候選函數
假如我們調用f(4.2),那麼先找到同名函數,并且在作用域内可見,上面例子中5個函數都滿足。
第二步:選擇可行的函數
必須滿足2個條件:一是函數形參與該調用實參個數相同;第二,每個實參的類型必須與對應的類型比對,或者可以被隐式轉換為對應的形參類型。這裡我們再調用f(4.2)時,排除了1、4、5号函數,隻剩下2與3。其中2号函數可以通過類型轉換來滿足。
第三步:尋找最佳比對
在經過第二步确定後,剩下2與3函數,那麼2需要進行類型轉換,顯然3是最佳比對了。
但是如果這樣調用f(42,4.2)。這時候就會出現二義性,編譯器将提示。
還有一種要注意的就是有預設參數的函數,比如我們定義6号函數為void f(double,int =1);那麼在調用f(4.2)時就會有二義性。
可基于函數的引用形參是指向const對象還是指向非const對象實作函數重載。
1,如何定義一個指針為函數類型?我們知道一個函數的類型是由它的傳回類型和形參類型共同決定,而與函數名無關。是以在定義一個指向函數的指針,必須包含形參表與傳回值這些資訊。
下面來看一個比較兩個字元串長度的函數:
那麼這個函數的類型即bool(const string&, const string&) 。如果要想定義一具指向這種類型函數的指針,則可以如下定義:
注意上式中*pf外面的括号不可以省略,不然pf就成了一個函數的定義,這個函數傳回一個指向bool類型的指針。
2,當我們把函數名作為一個值使用時,該函數自動地轉換成指針,是以我們可以這樣對函數指針pf初始化。
上面兩種方法是等價的。
那麼在使用函數指針時,我們可以解引用,也可以不解引用。
3,在給函數指針指派的時候,一定要注意函數類型的完全比對,但是我們可以給指向任意函數類型的指針賦一個nullptr或值為0的整型常量表達式,表示該指針沒有指向任何一個函數。
4,如果定義了指向重載函數的指針,在使用這個指針時并不是根據形參來确定所調用的函數,而是根據指針的具體函數類型。即,編譯器根據指針類型決定選用哪個函數,指針類型必須與重載函數中的某一個精确比對。
5,函數指針作為另一個函數的形參。
有的時候,我們需要将一個函數作為一個參數傳遞給别一個參數,比如定義一個函數用來傳回兩個對象中較大的那個,那麼我們需要将一個比較函數作為參數傳遞。
上面定義的函數原型顯得有點冗長,我們可以定義函數類型,來簡化上面的代碼:
有了上面的定義,我們就可以簡單bigstring的定義了:
6,函數傳回值為一個函數指針。
我們知道,函數并不能傳回一個函數,但是可以傳回一個指向函數的指針。
最簡單的方法,我們用類型别名定義一種函數類型:
下面我們來定義傳回函數指針的函數:
當然我們也可以直接定義f:
由内向外觀察:首先f有一個形參表(int),是以f是一個函數,然後f的的左邊有一個*,說明f傳回的是一個指針。進一步發現,指針的類型本身也包含形參表,是以指針指向函數,該函數的傳回類型是int。
我們還可以用c++11中的尾置傳回類型來聲明一個傳回函數指針的函數:
如果我們需要傳回的函數類型有一個函數執行個體,那麼我們可以用decltype來說明函數的類型:
注意上面代碼中decltype(func)傳回的是一個函數類型,我們需要在後面加上*,說明一個函數指針類型。