《C++ Primer 5th》笔记(6 / 19):函数
文章目錄
- 函數(shù)基礎(chǔ)
- 局部對象
- 函數(shù)聲明
- 分離式編譯
- 參數(shù)傳遞
- 傳遞參數(shù)
- 傳引用參數(shù)
- 使用引用避免拷貝
- 使用引用形參返回額外信息
- const形參和實參
- 指針或引用形參與const
- 盡量使用常量引用
- 數(shù)組形參
- 使用標(biāo)記指定數(shù)組長度
- 使用標(biāo)準(zhǔn)庫規(guī)范
- 顯示傳遞一個表示數(shù)組大小的形參
- 數(shù)組形參和const
- 數(shù)組引用形參
- 傳遞多維數(shù)組
- main:處理命令行選項
- 含有可變形參的函數(shù)
- initializer_list形參
- 省略符形參
- 返回類型和return語句
- 無返回值函數(shù)
- 有返回值函數(shù)
- 值是如何被返回的
- 不要返回局部對象的引用或指針
- 返回類類型的函數(shù)和調(diào)用運算符
- 引用返回左值
- 列表初始化返回值
- 主函數(shù)main的返回值
- 遞歸
- 返回數(shù)組指針
- 聲明一個返回數(shù)組指針的函數(shù)
- 使用尾置返回類型
- 使用decltype
- 函數(shù)重載
- 定義重載函數(shù)
- 判斷兩個形參的類型是否相異
- 重載和const形參
- 建議:何時不應(yīng)該重載函數(shù)
- const_cast和重載
- 調(diào)用重載的函數(shù)
- 重載與作用域
- 特殊用途語言特性
- 默認(rèn)實參
- 使用默認(rèn)實參調(diào)用函數(shù)
- 默認(rèn)實參聲明
- 默認(rèn)實參初始值
- 內(nèi)聯(lián)函數(shù)和constexpr函數(shù)
- 內(nèi)聯(lián)函數(shù)可避免函數(shù)調(diào)用的開銷
- constexpr函數(shù)
- 把內(nèi)聯(lián)函數(shù)和constexpr函數(shù)放在頭文件內(nèi)
- 調(diào)試幫助
- assert預(yù)處理宏
- NDEBUG預(yù)處理變量
- 函數(shù)匹配
- 例子:調(diào)用應(yīng)該選用哪個重載函數(shù)
- 確定候選函數(shù)和可行函數(shù)
- 尋找最佳匹配(如果有的話)
- 含有多個形參的函數(shù)匹配
- 實參類型轉(zhuǎn)換
- 需要類型提升和算術(shù)類型轉(zhuǎn)換的匹配
- 函數(shù)匹配和const實參
- 函數(shù)指針
- 使用函數(shù)指針
- 重載函數(shù)的指針
- 函數(shù)指針形參
- 返回指向函數(shù)的指針
- 將auto和decltype用于函數(shù)指針類型
函數(shù)是一個命名了的代碼塊,我們通過調(diào)用函數(shù)執(zhí)行相應(yīng)的代碼。函數(shù)可以有0個或多個參數(shù),而且(通常)會產(chǎn)生一個結(jié)果。可以重載函數(shù),也就是說,同一個名字可以對應(yīng)幾個不同的函數(shù)。
函數(shù)基礎(chǔ)
一個典型的函數(shù)(function)定義包括以下部分:
- 返回類型(return type)
- 函數(shù)名字
- 由0個或多個形參(parameter)組成的列表,形參以逗號隔開,形參的列表位于一對圓括號之內(nèi)
- 函數(shù)體。函數(shù)執(zhí)行的操作在語句塊中說明,該語句塊稱為函數(shù)體( function body)
我們通過調(diào)用運算符(call operator)來執(zhí)行函數(shù)。調(diào)用運算符的形式是一對圓括號,它作用于一個表達式,該表達式是函數(shù)或者指向函數(shù)的指針;
圓括號之內(nèi)是一個用逗號隔開的實參(argument)列表,我們用實參初始化函數(shù)的形參。
調(diào)用表達式的類型就是函數(shù)的返回類型。
編寫函數(shù)
舉個例子,我們準(zhǔn)備編寫一個求數(shù)的階乘的程序。n 的階乘是從1到n所有數(shù)字的乘積,例如5的階乘是120。
1 * 2 * 3 * 4 * 5 = 120程序如下所示:
// factorial of val is val * (val - 1) * (val - 2) . . . * ((val - (val - 1)) * 1) int fact(int val) {int ret = 1;// local variable to hold the result as we calculate itwhile (val > 1)ret *= val--; // assign ret * val to ret and decrement valreturn ret; // return the result }調(diào)用函數(shù)
要調(diào)用fact函數(shù),必須提供一個整數(shù)值,調(diào)用得到的結(jié)果也是一個整數(shù):
int main() {int j = fact(5); // j equals 120, i.e., the result of fact(5)cout << "5! is " << j << endl;return 0; }函數(shù)的調(diào)用完成兩項工作:
執(zhí)行函數(shù)的第一步是(隱式地)定義并初始化它的形參。因此,當(dāng)調(diào)用fact函數(shù)時,首先創(chuàng)建一個名為val的int變量,然后將它初始化為調(diào)用時所用的實參5。
當(dāng)遇到一條return語句時函數(shù)結(jié)束執(zhí)行過程。和函數(shù)調(diào)用一樣,return語句也完成兩項工作:
函數(shù)的返回值用于初始化調(diào)用表達式的結(jié)果,之后繼續(xù)完成調(diào)用所在的表達式的剩余部分。因此,我們對fact函數(shù)的調(diào)用等價于如下形式:
int val = 5; // initialize val from the literal 5 int ret = 1; // code from the body of fact while (val > 1)ret *= val--; int j = ret; // initialize j as a copy of ret形參和實參
實參是形參的初始值。第一個實參初始化第一個形參,第二個實參初始化第二個形參,以此類推。盡管實參與形參存在對應(yīng)關(guān)系,但是并沒有規(guī)定實參的求值順序(參見4.1.3節(jié),第123頁)。編譯器能以任意可行的順序?qū)崊⑶笾怠?/p>
實參的類型必須與對應(yīng)的形參類型匹配,這一點與之前的規(guī)則是一致的,我們知道在初始化過程中初始值的類型也必須與初始化對象的類型匹配。函數(shù)有幾個形參,我們就必須提供相同數(shù)量的實參。因為函數(shù)的調(diào)用規(guī)定實參數(shù)量應(yīng)與形參數(shù)量一致,所以形參一定會被初始化。
在上面的例子中,fact函數(shù)只有一個int類型的形參,所以每次我們調(diào)用它的時候,都必須提供一個能轉(zhuǎn)換成int的實參:
fact( "hello" ); //錯誤:實參類型不正確 fact(); //錯誤:實參數(shù)量不足 fact(42, 10, 0); //錯誤:實參數(shù)量過多 fact(3.14); //正確:該實參能轉(zhuǎn)換成int類型- 因為不能將const char*轉(zhuǎn)換成int,所以第一個調(diào)用失敗。
- 第二個和第三個調(diào)用也會失敗,不過錯誤的原因與第一個不同,它們是因為傳入的實參數(shù)量不對。要想調(diào)用fact函數(shù)只能使用一個實參,只要實參數(shù)量不是一個,調(diào)用都將失敗。
- 最后一個調(diào)用是合法的,因為 double可以轉(zhuǎn)換成int。執(zhí)行調(diào)用時,實參隱式地轉(zhuǎn)換成int類型(截去小數(shù)部分),調(diào)用等價于
函數(shù)的形參列表
函數(shù)的形參列表可以為空,但是不能省略。要想定義一個不帶形參的函數(shù),最常用的辦法是書寫一個空的形參列表。不過為了與C語言兼容,也可以使用關(guān)鍵字void表示函數(shù)沒有形參:
void f1(){/* ...*/} //隱式地定義空形參列表 void f2(void){/* ...*/} //顯式地定義空形參列表*形參列表中的形參通常用逗號隔開,其中每個形參都是含有一個聲明符的聲明。即使兩個形參的類型一樣,也必須把兩個類型都寫出來:
int f3(int v1, v2){/* ...*/ }//錯誤 int f4(int v1, int v2){/* ...*/}//正確任意兩個形參都不能同名,而且函數(shù)最外層作用域中的局部變量也不能使用與函數(shù)形參一樣的名字。
形參名是可選的,但是由于我們無法使用未命名的形參,所以形參一般都應(yīng)該有個名字。
偶爾,函數(shù)確實有個別形參不會被用到,則此類形參通常不命名以表示在函數(shù)體內(nèi)不會使用它。不管怎樣,是否設(shè)置未命名的形參并不影響調(diào)用時提供的實參數(shù)量。即使某個形參不被函數(shù)使用,也必須為它提供一個實參。
函數(shù)返回類型
大多數(shù)類型都能用作函數(shù)的返回類型。一種特殊的返回類型是void,它表示函數(shù)不返回任何值。函數(shù)的返回類型不能是數(shù)組類型或函數(shù)類型,但可以是指向數(shù)組或函數(shù)的指針,在本章,會陸續(xù)介紹。
局部對象
在C++語言中,名字有作用域,對象有生命周期(lifetime)。理解這兩個概念非常重要。
-
名字的作用域是程序文本的一部分,名字在其中可見。
-
對象的生命周期是程序執(zhí)行過程中該對象存在的一段時間。
如我們所知,函數(shù)體是一個語句塊。塊構(gòu)成一個新的作用域,我們可以在其中定義變量。形參和函數(shù)體內(nèi)部定義的變量統(tǒng)稱為局部變量(local variable)。它們對函數(shù)而言是“局部”的,僅在函數(shù)的作用域內(nèi)可見,同時局部變量還會隱藏(hide)在外層作用域中同名的其他所有聲明中。
在所有函數(shù)體之外定義的對象存在于程序的整個執(zhí)行過程中。此類對象在程序啟動時被創(chuàng)建,直到程序結(jié)束才會銷毀。局部變量的生命周期依賴于定義的方式。
自動對象
對于普通局部變量對應(yīng)的對象來說,當(dāng)函數(shù)的控制路徑經(jīng)過變量定義語句時創(chuàng)建該對象,當(dāng)?shù)竭_定義所在的塊末尾時銷毀它。我們把只存在于塊執(zhí)行期間的對象稱為自動對象(automatic object)。當(dāng)塊的執(zhí)行結(jié)束后,塊中創(chuàng)建的自動對象的值就變成未定義的了。(呼之即來,揮之即去)
形參是一種自動對象。函數(shù)開始時為形參申請存儲空間,因為形參定義在函數(shù)體作用域之內(nèi),所以一旦函數(shù)終止,形參也就被銷毀。
我們用傳遞給函數(shù)的實參初始化形參對應(yīng)的自動對象。對于局部變量對應(yīng)的自動對象來說,則分為兩種情況:
- 如果變量定義本身含有初始值,就用這個初始值進行初始化;
- 否則,如果變量定義本身不含初始值,執(zhí)行默認(rèn)初始化。這意味著內(nèi)置類型的未初始化局部變量將產(chǎn)生未定義的值。
局部靜態(tài)對象
某些時候,有必要令局部變量的生命周期貫穿函數(shù)調(diào)用及之后的時間。可以將局部變量定義成static類型從而獲得這樣的對象。局部靜態(tài)對象(local static object)在程序的執(zhí)行路徑第一次經(jīng)過對象定義語句時初始化,并且直到程序終止才被銷毀,在此期間即使對象所在的函數(shù)結(jié)束執(zhí)行也不會對它有影響。
(MyNote:局部靜態(tài)對象具有全局變量長命期與局部變量的私有性。)
舉個例子,下面的函數(shù)統(tǒng)計它自己被調(diào)用了多少次,這樣的函數(shù)也許沒什么實際意義,但是足夠說明問題:
size_t count_calls() {static size_t ctr = 0; // value will persist across callsreturn ++ctr; }int main() {for (size_t i = 0; i != 10; ++i)cout << count_calls() << endl;return 0; }這段程序?qū)⑤敵鰪?到10(包括10在內(nèi))的數(shù)字。
如果局部靜態(tài)變量沒有顯式的初始值,它將執(zhí)行值初始化,內(nèi)置類型的局部靜態(tài)變量初始化為0。
函數(shù)聲明
和其他名字一樣,函數(shù)的名字也必須在使用之前聲明。類似于變量,函數(shù)只能定義一次,但可以聲明多次。唯一的例外是如第15章將要介紹的,如果一個函數(shù)永遠也不會被我們用到,那么它可以只有聲明沒有定義。
函數(shù)的聲明和函數(shù)的定義非常類似,唯一的區(qū)別是函數(shù)聲明無須函數(shù)體,用一個分號替代即可。
因為函數(shù)的聲明不包含函數(shù)體,所以也就無須形參的名字。事實上,在函數(shù)的聲明中經(jīng)常省略形參的名字。盡管如此,寫上形參的名字還是有用處的,它可以幫助使用者更好地理解函數(shù)的功能:
//我們選擇beg和end作為形參的名字以表示這兩個迭代器劃定了輸出值的范圍 void print (vector<int>::const_iterator beg, vector<int>:: const_iterator end) ;函數(shù)的三要素(返回類型、函數(shù)名、形參類型)描述了函數(shù)的接口,說明了調(diào)用該函數(shù)所需的全部信息。函數(shù)聲明也稱作函數(shù)原型(function prototype)。
在頭文件中進行函數(shù)聲明
回憶之前所學(xué)的知識,我們建議變量在頭文件中聲明,在源文件中定義。與之類似,函數(shù)也應(yīng)該在頭文件中聲明而在源文件中定義。
看起來把函數(shù)的聲明直接放在使用該函數(shù)的源文件中是合法的,也比較容易被人接受;但是這么做可能會很煩瑣而且容易出錯。相反,如果把函數(shù)聲明放在頭文件中,就能確保同一函數(shù)的所有聲明保持一致。而且一旦我們想改變函數(shù)的接口,只需改變一條聲明即可。
定義函數(shù)的源文件應(yīng)該把含有函數(shù)聲明的頭文件包含進來,編譯器負責(zé)驗證函數(shù)的定義和聲明是否匹配。
Best Practices:含有函數(shù)聲明的頭文件應(yīng)該被包含到定義函數(shù)的源文件中。
(MyNote:函數(shù)聲明在頭文件,定義在源文件。)
分離式編譯
隨著程序越來越復(fù)雜,我們希望把程序的各個部分分別存儲在不同文件中。例如,函數(shù)存在一個文件里,把使用這些函數(shù)的代碼存在其他源文件中。為了允許編寫程序時按照邏輯關(guān)系將其劃分開來,C++語言支持所謂的分離式編譯(separate compilation)。分離式編譯允許我們把程序分割到幾個文件中去,每個文件獨立編譯。
編譯和鏈接多個源文件
舉個例子,假設(shè)fact函數(shù)的定義位于一個名為fact.cc的文件中,它的聲明位于名為Chapter6.h的頭文件中。顯然與其他所有用到fact函數(shù)的文件一樣,fact.cc應(yīng)該包含chapter6.h頭文件(#include “Chapter6.h”)。
另外,我們在名為factMain.cc 的文件中創(chuàng)建main函數(shù),main函數(shù)將調(diào)用fact函數(shù)。要生成可執(zhí)行文件(executable file),必須告訴編譯器我們用到的代碼在哪里。
對于上述幾個文件來說,編譯的過程如下所示:
$ cc factMain.cc fact.cc # generates factMain.exe or a.out $ cc factMain.cc fact.cc -o main # generates main or main.exe其中,cc是編譯器的名字,$是系統(tǒng)提示符,#后面是命令行下的注釋語句。接下來如果運行可執(zhí)行文件,就會執(zhí)行我們定義的main函數(shù)。
如果我們修改了其中一個源文件,那么只需重新編譯那個改動了的文件。大多數(shù)編譯器提供了分離式編譯每個文件的機制,這一過程通常會產(chǎn)生一個后綴名是.obj(Windows)或.o(UNIX)的文件,后綴名的含義是該文件包含對象代碼(object code)。
接下來編譯器負責(zé)把對象文件鏈接在一起形成可執(zhí)行文件。在我們的系統(tǒng)中,編譯的過程如下所示:
$ cc -c factMain.cc # generates factMain.o $ cc -c fact.cc # generates fact.o $ cc factMain.o fact.o # generates factMain.exe or a.out $ cc factMain.o fact.o -o main # generates main or main.exe你可以仔細閱讀編譯器的用戶手冊,弄清楚由多個文件組成的程序是如何編譯并執(zhí)行的。
(MyNote:這就是頭文件與源文件如何聯(lián)系在一起。)
參數(shù)傳遞
如前所述,每次調(diào)用函數(shù)時都會重新創(chuàng)建它的形參,并用傳入的實參對形參進行初始化。
Note:形參初始化的機理與變量初始化一樣。
和其他變量一樣,形參的類型決定了形參和實參交互的方式。如果形參是引用類型,它將綁定到對應(yīng)的實參上;否則,將實參的值拷貝后賦給形參。
-
當(dāng)形參是引用類型時,我們說它對應(yīng)的實參被引用傳遞(passed by reference)或者函數(shù)被傳引用調(diào)用(called by reference)。和其他引用一樣,引用形參也是它綁定的對象的別名;也就是說,引用形參是它對應(yīng)的實參的別名。
-
當(dāng)實參的值被拷貝給形參時,形參和實參是兩個相互獨立的對象。我們說這樣的實參被值傳遞(passed by value)或者函數(shù)被傳值調(diào)用(called by value)。
傳遞參數(shù)
當(dāng)初始化一個非引用類型的變量時,初始值被拷貝給變量。此時,對變量的改動不會影響初始值:
int n = 0; //int類型的初始變量 int i = n; // i是n的值的副本 i = 42; // i的值改變;n的值不變傳值參數(shù)的機理完全一樣,函數(shù)對形參做的所有操作都不會影響實參。
例如,在fact函數(shù)
int fact(int val) {int ret = 1;// local variable to hold the result as we calculate itwhile (val > 1)ret *= val--; // assign ret * val to ret and decrement valreturn ret; // return the result }內(nèi)對變量val執(zhí)行遞減操作:
ret *= val--;//將val的值減1盡管fact函數(shù)改變了val的值,但是這個改動不會影響傳入fact的實參。調(diào)用fact(i)不會改變i的值。
指針形參
指針的行為和其他非引用類型一樣。當(dāng)執(zhí)行指針拷貝操作時,拷貝的是指針的值(MyNote:指針,即地址值)。拷貝之后,兩個指針是不同的指針。因為指針使我們可以間接地訪問它所指的對象,所以通過指針可以修改它所指對象的值:
int n = 0, i = 42; int *p = &n, *q = &i; // p points to n; q points to i *p = 42; // value in n is changed; p is unchanged p = q; // p now points to i; values in i and n are unchanged指針形參的行為與之類似:
// function that takes a pointer and sets the pointed-to value to zero void reset(int *ip) {*ip = 0; // changes the value of the object to which ip pointsip = 0; // changes only the local copy of ip; the argument is unchanged }調(diào)用reset函數(shù)之后,實參所指的對象被置為o,但是實參本身并沒有改變:
int i = 42; reset(&i); // changes i but not the address of i cout << "i = " << i << endl; // prints i = 0Best Practices:熟悉C的程序員常常使用指針類型的形參訪問函數(shù)外部的對象。在C++語言中,建議使用引用類型的形參替代指針。
(MyNote:引用,我個人理解為特殊的指針。)
傳引用參數(shù)
回憶過去所學(xué)的知識,我們知道對于引用的操作實際上是作用在引用所引的對象上。
int n = 0, i = 42; int &r = n; // r is bound to n (i.e., r is another name for n) r = 42; // n is now 42 r = i; // n now has the same value as i i = r; // i has the same value as n引用形參的行為與之類似。通過使用引用形參,允許函數(shù)改變一個或多個實參的值。
舉個例子,我們可以改寫上一小節(jié)的reset程序,使其接受的參數(shù)是引用類型而非指針:
// function that takes a reference to an int and sets the given object to zero void reset(int &i) // i is just another name for the object passed to reset {i = 0; // changes the value of the object to which i refers }和其他引用一樣,引用形參綁定初始化它的對象。當(dāng)調(diào)用這一版本的 reset 函數(shù)時,i綁定我們傳給函數(shù)的int對象,此時改變i也就是改變i所引對象的值。此例中,被改變的對象是傳入reset的實參。
調(diào)用這一版本的reset函數(shù)時,我們直接傳入對象而無須傳遞對象的地址:
在上述調(diào)用過程中,形參i僅僅是j的又一個名字。在reset內(nèi)部對i的使用即是對j的使用。
MyNote:傳引用與傳指針相比,傳引用使用時,無需像傳指針那樣要用一個解引用,這樣簡潔些。
// function that takes a pointer and sets the pointed-to value to zero void reset(int *ip) {*ip = 0; // changes the value of the object to which ip pointsip = 0; // changes only the local copy of ip; the argument is unchanged }使用引用避免拷貝
拷貝大的類類型對象或者容器對象比較低效,甚至有的類類型(包括IO類型在內(nèi))根本就不支持拷貝操作。當(dāng)某種類型不支持拷貝操作時,函數(shù)只能通過引用形參訪問該類型的對象。
舉個例子,我們準(zhǔn)備編寫一個函數(shù)比較兩個string 對象的長度。因為string對象可能會非常長,所以應(yīng)該盡量避免直接拷貝它們,這時使用引用形參是比較明智的選擇。又因為比較長度無須改變string 對象的內(nèi)容,所以把形參定義成對常量的引用:
// compare the length of two strings bool isShorter(const string &s1, const string &s2) {return s1.size() < s2.size(); }如將要介紹的,當(dāng)函數(shù)無須修改引用形參的值時最好使用常量引用。(只讀屬性)
Best Practices:如果函數(shù)無須改變引用形參的值,最好將其聲明為常量引用。
使用引用形參返回額外信息
一個函數(shù)只能返回一個值,然而有時函數(shù)需要同時返回多個值,引用形參為我們一次返回多個結(jié)果提供了有效的途徑。
舉個例子,我們定義一個名為find_char的函數(shù),它返回在string對象中某個指定字符第一次出現(xiàn)的位置。同時,我們也希望函數(shù)能返回該字符出現(xiàn)的總次數(shù)。
該如何定義函數(shù)使得它能夠既返回位置也返回出現(xiàn)次數(shù)呢?
當(dāng)我們調(diào)用find_char函數(shù)時,必須傳入三個實參:作為查找范圍的一個string對象、要找的字符以及一個用于保存字符出現(xiàn)次數(shù)的size_type對象。假設(shè)s是一個string對象,ctr是一個size_type對象,則我們通過如下形式調(diào)用find_char函數(shù):
auto index = find_char(s, 'o', ctr);調(diào)用完成后,如果string對象中確實存在o,那么ctr的值就是。出現(xiàn)的次數(shù),index指向o第一次出現(xiàn)的位置;否則如果string對象中沒有o, index等于 s.size()而ctr等于0。
const形參和實參
當(dāng)形參是const時,必須要注意第2章關(guān)于頂層const的內(nèi)容。如前所述,頂層const作用于對象本身:
const int ci = 42; // we cannot change ci; const is top-level int i = ci; // ok: when we copy ci, its top-level const is ignored int * const p = &i; // const is top-level; we can't assign to p,注意,初始化與賦值在C++中是兩碼事 *p = 0; // ok: changes through p are allowed; i is now 0和其他初始化過程一樣,當(dāng)用實參初始化形參時會忽略掉頂層const。換句話說,形參的頂層const被忽略掉了。當(dāng)形參有頂層const時,傳給它常量對象或者非常量對象都是可以的:
void fcn(const int i) { /* fcn can read but not write to i */ }調(diào)用fcn函數(shù)時,既可以傳入const int也可以傳入int。忽略掉形參的頂層const可能產(chǎn)生意想不到的結(jié)果:
void fcn(const int i) { /* fcn can read but not write to i */ } void fcn(int i) { /* . . . */ } // error: redefines fcn(int)在C++語言中,允許我們定義若干具有相同名字的函數(shù),不過前提是不同函數(shù)的形參列表應(yīng)該有明顯的區(qū)別。因為頂層const被忽略掉了,所以在上面的代碼中傳入兩個fcn函數(shù)的參數(shù)可以完全一樣。因此第二 fcn是錯誤的,盡管形式上有差異,但實際上它的形參和第一個fcn的形參沒什么不同。
指針或引用形參與const
形參的初始化方式和變量的初始化方式是一樣的,所以回顧通用的初始化規(guī)則有助于理解本節(jié)知識。
我們可以使用非常量初始化一個底層const對象,但是反過來不行;同時一個普通的引用必須用同類型的對象初始化。
int i = 42; const int *cp = &i; // ok: but cp can't change i const int &r = i; // ok: but r can't change i const int &r2 = 42; // ok://我們可以使用非常量初始化一個底層const對象,但是反過來不行; int *p = cp; // error: types of p and cp don't match int &r3 = r; // error: types of r3 and r don't match int &r4 = 42; // error: can't initialize a plain reference from a literal//普通引用變量綁定一個變量將同樣的初始化規(guī)則應(yīng)用到參數(shù)傳遞上可得如下形式:
// function that takes a reference to an int and sets the given object to zero void reset(int &i) // i is just another name for the object passed to reset {i = 0; // changes the value of the object to which i refers } int i = 0; const int ci = i; string::size_type ctr = 0; reset(&i); // calls the version of reset that has an int* parameter reset(&ci); // error: can't initialize an int* from a pointer to a const int object reset(i); // calls the version of reset that has an int& parameterreset(ci); // error: can't bind a plain reference to the const object ci reset(42); // error: can't bind a plain reference to a literal reset(ctr); // error: types don't match; ctr has an unsigned type// ok: find_char's first parameter is a reference to const find_char("Hello World!", 'o', ctr);要想調(diào)用引用版本的reset,只能使用int類型的對象,而不能使用字面值、求值結(jié)果為int的表達式、需要轉(zhuǎn)換的對象或者const int類型的對象。類似的,要想調(diào)用指針版本的reset只能使用int*。
另一方面,我們能傳遞一個字符串字面值作為find_char的第一個實參,這是因為該函數(shù)的引用形參是常量引用,而C++允許我們用字面值初始化常量引用。
盡量使用常量引用
把函數(shù)不會改變的形參定義成(普通的)引用是一種比較常見的錯誤,這么做帶給函數(shù)的調(diào)用者一種誤導(dǎo),即函數(shù)可以修改它的實參的值。此外,使用引用而非常量引用也會極大地限制函數(shù)所能接受的實參類型。就像剛剛看到的,我們不能把const對象、字面值或者需要類型轉(zhuǎn)換的對象傳遞給普通的引用形參。
這種錯誤絕不像看起來那么簡單,它可能造成出人意料的后果。以上文的find_char函數(shù)為例,那個函數(shù)(正確地)將它的string類型的形參定義成常量引用。假如我們把它定義成普通的string&:
// bad design: the first parameter should be a const string& string::size_type find_char(string &s, char c,string::size_type &occurs);則只能將find_char函數(shù)作用于string對象。類似下面這樣的調(diào)用
find_char("Hello World", 'o', ctr);//string &s = "Hello World";不行,const string &s = "Hello World";行將在編譯時發(fā)生錯誤。
還有一個更難察覺的問題,假如其他函數(shù)(正確地)將它們的形參定義成常量引用,那么第二個版本的find_char無法在此類函數(shù)中正常使用。舉個例子,我們希望在一個判斷string對象是否是句子的函數(shù)中使用find_char:
bool is_sentence(const string &s) {// if there's a single period at the end of s, then s is a sentencestring::size_type ctr = 0;return find_char(s, '.', ctr) == s.size() - 1 && ctr == 1; }如果find_char的第一個形參類型是string&,那么上面這條調(diào)用find_char的語句將在編譯時發(fā)生錯誤。原因在于s是常量引用,但find_char被(不正確地)定義成只能接受普通引用。
解決該問題的一種思路是修改is_sentence的形參類型,但是這么做只不過轉(zhuǎn)移了錯誤而已,結(jié)果是is_sentence函數(shù)的調(diào)用者只能接受非常量string對象了。
正確的修改思路是改正find_char函數(shù)的形參。如果實在不能修改find_char,就在is _sentence內(nèi)部定義一個string類型的變量,令其為s的副本,然后把這個string對象傳遞給find_char。
數(shù)組形參
數(shù)組(第3章內(nèi)容)的兩個特殊性質(zhì)對我們定義和使用作用在數(shù)組上的函數(shù)有影響,這兩個性質(zhì)分別是:
因為不能拷貝數(shù)組,所以我們無法以值傳遞的方式使用數(shù)組參數(shù)。因為數(shù)組會被轉(zhuǎn)換成指針,所以當(dāng)我們?yōu)楹瘮?shù)傳遞一個數(shù)組時,實際上傳遞的是指向數(shù)組首元素的指針。
盡管不能以值傳遞的方式傳遞數(shù)組,但是我們可以把形參寫成類似數(shù)組的形式:
// despite appearances, these three declarations of print are equivalent // each function has a single parameter of type const int* void print(const int*); void print(const int[]); // shows the intent that the function takes an array void print(const int[10]); // dimension for documentation purposes (at best)盡管表現(xiàn)形式不同,但上面的三個函數(shù)是等價的:每個函數(shù)的唯一形參都是const int *類型的。當(dāng)編譯器處理對print函數(shù)的調(diào)用時,只檢查傳入的參數(shù)是否是const int *類型:
int i = 0, j[2] = {0, 1}; print(&i); // ok: &i is int* print(j); // ok: j is converted to an int* that points to j[0]如果我們傳給 print 函數(shù)的是一個數(shù)組,則實參自動地轉(zhuǎn)換成指向數(shù)組首元素的指針,數(shù)組的大小對函數(shù)的調(diào)用沒有影響。
WARNING:和其他使用數(shù)組的代碼一樣,以數(shù)組作為形參的函數(shù)也必須確保使用數(shù)組時不會越界。
因為數(shù)組是以指針的形式傳遞給函數(shù)的,所以一開始函數(shù)并不知道數(shù)組的確切尺寸,調(diào)用者應(yīng)該為此提供一些額外的信息。管理指針形參有三種常用的技術(shù)。
使用標(biāo)記指定數(shù)組長度
管理數(shù)組實參的第一種方法是要求數(shù)組本身包含一個結(jié)束標(biāo)記,使用這種方法的典型示例是C風(fēng)格字符串。C風(fēng)格字符串存儲在字符數(shù)組中,并且在最后一個字符后面跟著一個空字符。函數(shù)在處理C風(fēng)格字符串時遇到空字符停止:
void print(const char *cp) {if (cp) // if cp is not a null pointerwhile (*cp) // so long as the character it points to is not a null charactercout << *cp++; // print the character and advance the pointer }這種方法適用于那些有明顯結(jié)束標(biāo)記且該標(biāo)記不會與普通數(shù)據(jù)混淆的情況,但是對于像int這樣所有取值都是合法值的數(shù)據(jù)就不太有效了。
使用標(biāo)準(zhǔn)庫規(guī)范
管理數(shù)組實參的第二種技術(shù)是傳遞指向數(shù)組首元素和尾后元素的指針,這種方法受到了標(biāo)準(zhǔn)庫技術(shù)的啟發(fā),使用該方法,我們可以按照如下形式輸出元素內(nèi)容:
void print(const int *beg, const int *end) {// print every element starting at beg up to but not including endwhile (beg != end)cout << *beg++ << endl; // print the current element// and advance the pointer }while循環(huán)使用解引用運算符和后置遞減運算符輸出當(dāng)前元素并在數(shù)組內(nèi)將beg向前移動一個元素,當(dāng)beg和end相等時結(jié)束循環(huán)。
為了調(diào)用這個函數(shù),我們需要傳入兩個指針:一個指向要輸出的首元素,另一個指向尾元素的下一位置:
int j[2] = {0, 1}; // j is converted to a pointer to the first element in j // the second argument is a pointer to one past the end of j print(begin(j), end(j)); // begin and end functions只要調(diào)用者能正確地計算指針?biāo)傅奈恢?#xff0c;那么上述代碼就是安全的。在這里,我們使用標(biāo)準(zhǔn)庫begin和end函數(shù)提供所需的指針。
顯示傳遞一個表示數(shù)組大小的形參
第三種管理數(shù)組實參的方法是專門定義一個表示數(shù)組大小的形參,在C程序和過去的C++程序中常常使用這種方法。使用該方法,可以將print函數(shù)重寫成如下形式:
// const int ia[] is equivalent to const int* ia // size is passed explicitly and used to control access to elements of ia void print(const int ia[], size_t size) {for (size_t i = 0; i != size; ++i) {cout << ia[i] << endl;} }這個版本的程序通過形參size的值確定要輸出多少個元素,調(diào)用print函數(shù)時必須傳入這個表示數(shù)組大小的值:
int j[] = { 0, 1 }; // int array of size 2 print(j, end(j) - begin(j));只要傳遞給函數(shù)的size值不超過數(shù)組實際的大小,函數(shù)就是安全的。
數(shù)組形參和const
我們的三個print函數(shù)都把數(shù)組形參定義成了指向const的指針,本章關(guān)于引用的討論同樣適用于指針。
- 當(dāng)函數(shù)不需要對數(shù)組元素執(zhí)行寫操作的時候,數(shù)組形參應(yīng)該是指向const的指針。
- 只有當(dāng)函數(shù)確實要改變元素值的時候,才把形參定義成指向非常量的指針。
(MyNote:只讀時用const。)
數(shù)組引用形參
C++語言允許將變量定義成數(shù)組的引用,基于同樣的道理,形參也可以是數(shù)組的引用。此時,引用形參綁定到對應(yīng)的實參上,也就是綁定到數(shù)組上:
// ok: parameter is a reference to an array; the dimension is part of the type void print(int (&arr)[10]) {for (auto elem : arr)cout << elem << endl; }Note:&arr兩端的括號必不可少:
f(int &arr[10]) // error: declares arr as an array of references f(int (&arr)[10]) // ok: arr is a reference to an array of ten ints因為數(shù)組的大小是構(gòu)成數(shù)組類型的一部分,所以只要不超過維度,在函數(shù)體內(nèi)就可以放心地使用數(shù)組。但是,這一用法也無形中限制了print函數(shù)的可用性,我們只能將函數(shù)作用于大小為10的數(shù)組:
int i = 0, j[2] = {0, 1}; int k[10] = {0,1,2,3,4,5,6,7,8,9}; print(&i); // error: argument is not an array of ten ints print(j); // error: argument is not an array of ten ints print(k); // ok: argument is an array of ten ints第16章將要介紹我們應(yīng)該如何編寫這個函數(shù),使其可以給引用類型的形參傳遞任意大小的數(shù)組。
傳遞多維數(shù)組
我們曾經(jīng)介紹過,在C++語言中實際上沒有真正的多維數(shù)組,所謂多維數(shù)組其實是數(shù)組的數(shù)組。
和所有數(shù)組一樣,當(dāng)將多維數(shù)組傳遞給函數(shù)時,真正傳遞的是指向數(shù)組首元素的指針。因為我們處理的是數(shù)組的數(shù)組,所以首元素本身就是一個數(shù)組,指針就是一個指向數(shù)組的指針。數(shù)組第二維(以及后面所有維度〉的大小都是數(shù)組類型的一部分,不能省略:
// matrix points to the first element in an array whose elements are arrays of ten ints void print(int (*matrix)[10], int rowSize) { /* . . . */ }上述語句將matrix聲明成指向含有10個整數(shù)的數(shù)組的指針。
Note:再一次強調(diào),*matrix兩端的括號必不可少:
int *matrix[10]; // array of ten pointers int (*matrix)[10]; // pointer to an array of ten ints我們也可以使用數(shù)組的語法定義函數(shù),此時編譯器會一如既往地忽略掉第一個維度,所以最好不要把它包括在形參列表內(nèi):
// equivalent definition void print(int matrix[][10], int rowSize) { /* . . . */ }matrix的聲明看起來是一個二維數(shù)組,實際上形參是指向含有10個整數(shù)的數(shù)組的指針。
main:處理命令行選項
main函數(shù)是演示C++程序如何向函數(shù)傳遞數(shù)組的好例子。到目前為止,我們定義的main函數(shù)都只有空形參列表:
int main () { ... }然而,有時我們確實需要給main傳遞實參,一種常見的情況是用戶通過設(shè)置一組選項來確定函數(shù)所要執(zhí)行的操作。例如,假定main函數(shù)位于可執(zhí)行文件prog之內(nèi),我們可以向程序傳遞下面的選項:
prog -d -o ofile data0這些命令行選項通過兩個(可選的)形參傳遞給main函數(shù):
int main (int argc, char *argv[]){ ... }第二個形參argv是一個數(shù)組,它的元素是指向C風(fēng)格字符串的指針;第一個形參argc表示數(shù)組中字符串的數(shù)量。
因為第二個形參是數(shù)組,所以main函數(shù)也可以定義成:
int main (int argc, char **argv){ ... }其中argv指向char*。
當(dāng)實參傳給main函數(shù)之后,argv的第一個元素指向程序的名字或者一個空字符串,接下來的元素依次傳遞命令行提供的實參。最后一個指針之后的元素值保證為0。
以上面提供的命令行為例,argc應(yīng)該等于5,argv應(yīng)該包含如下的C風(fēng)格字符串:
argv[0] = "prog" ; //或者argv[0]也可以指向一個空字符串 argv[1] = "-d"; argv[2] = "-o"; argv[3] = "ofile"; argv[4] = "datao"; argv[5] = 0;WARNING:當(dāng)使用argv中的實參時,一定要記得可選的實參從argv[1]開始,argv[0]保存程序的名字,而非用戶輸入。
含有可變形參的函數(shù)
有時我們無法提前預(yù)知應(yīng)該向函數(shù)傳遞幾個實參。例如,我們想要編寫代碼輸出程序產(chǎn)生的錯誤信息,此時最好用同一個函數(shù)實現(xiàn)該項功能,以便對所有錯誤的處理能夠整齊劃一。然而,錯誤信息的種類不同,所以調(diào)用錯誤輸出函數(shù)時傳遞的實參也各不相同。
為了編寫能處理不同數(shù)量實參的函數(shù),C++11新標(biāo)準(zhǔn)提供了兩種主要的方法:
C++還有一種特殊的形參類型(即省略符),可以用它傳遞可變數(shù)量的實參。本節(jié)將簡要介紹省略符形參,不過需要注意的是,這種功能一般只用于與C函數(shù)交互的接口程序。
initializer_list形參
如果函數(shù)的實參數(shù)量未知但是全部實參的類型都相同,我們可以使用initializer_list類型的形參。initializer_list是一種標(biāo)準(zhǔn)庫類型,用于表示某種特定類型的值的數(shù)組。initializer_list類型定義在同名的頭文件中,它提供的操作如下表所示。
| initializer_list lst; | 默認(rèn)初始化;T類型元素的空列表 |
| initializer_list lst{a, b, c…}; | lst的元素數(shù)量和初始值一樣多;lst的元素是對應(yīng)初始值的副本;列表中的元素是const |
| lst2(lst) | 拷貝或賦值一個initializer_list對象不會拷貝列表中的元素;拷貝后,原始列表和副本共享元素 |
| lst2 = lst | 同上條 |
| lst.size() | 列表中的元素數(shù)量 |
| lst.begin() | 返回指向lst中首元素的指針 |
| lst.end() | 返回指向lst中尾元素下一位置的指針 |
和vector一樣,initializer_list也是一種模板類型。定義initializer_list對象時,必須說明列表中所含元素的類型:
initializer_list<string> ls; // initializer_list of strings initializer_list<int> li; // initializer_list of ints和 vector不一樣的是,initializer_list對象中的元素永遠是常量值,我們無法改變initializer_list對象中元素的值。
我們使用如下的形式編寫輸出錯誤信息的函數(shù),使其可以作用于可變數(shù)量的實參:
void error_msg(initializer_list<string> il) {for (auto beg = il.begin(); beg != il.end(); ++beg)cout << *beg << " " ;cout << endl; }作用于initializer_list對象的begin和end操作類似于vector對應(yīng)的成員。
begin()成員提供一個指向列表首元素的指針,end()成員提供一個指向列表尾后元素的指針。我們的函數(shù)首先初始化 beg令其表示首元素,然后依次遍歷列表中的每個元素。在循環(huán)體中,解引用beg 以訪問當(dāng)前元素并輸出它的值。
如果想向initializer_list形參中傳遞一個值的序列,則必須把序列放在一對花括號內(nèi):
// expected, actual are strings if (expected != actual)error_msg({"functionX", expected, actual}); elseerror_msg({"functionX", "okay"});在上面的代碼中我們調(diào)用了同一個函數(shù)error_msg,但是兩次調(diào)用傳遞的參數(shù)數(shù)量不同:第一次調(diào)用傳入了三個值,第二次調(diào)用只傳入了兩個。
含有initializer_list形參的函數(shù)也可以同時擁有其他形參。例如,調(diào)試系統(tǒng)可能有個名為ErrCode的類用來表示不同類型的錯誤,因此我們可以改寫之前的程序,使其包含一個initializer_list形參和一個ErrCode形參:
void error_msg(ErrCode e, initializer_list<string> il) {cout << e.msg() << ": ";for (const auto &elem : il)cout << elem << " " ;cout << endl; }因為initializer_list包含begin和end成員,所以我們可以使用范圍for循環(huán)處理其中的元素。和之前的版本類似,這段程序遍歷傳給il形參的列表值,每次迭代時訪問一個元素。
為了調(diào)用這個版本的error_msg函數(shù),需要額外傳遞一個ErrCode實參:
if (expected != actual)error_msg(ErrCode(42), {"functionX", expected, actual}); elseerror_msg(ErrCode(0), {"functionX", "okay"});省略符形參
省略符形參是為了便于C++程序訪問某些特殊的C代碼而設(shè)置的,這些代碼使用了名為varargs的C標(biāo)準(zhǔn)庫功能。通常,省略符形參不應(yīng)用于其他目的。你的C編譯器文檔會描述如何使用varargs。
WARNING:省略符形參應(yīng)該僅僅用于C和C++通用的類型。特別應(yīng)該注意的是,大多數(shù)類類型的對象在傳遞給省略符形參時都無法正確拷貝。
省略符形參只能出現(xiàn)在形參列表的最后一個位置,它的形式無外乎以下兩種:
void foo(parm_list, ...); void foo(...);第一種形式指定了foo 函數(shù)的部分形參的類型,對應(yīng)于這些形參的實參將會執(zhí)行正常的類型檢查。省略符形參所對應(yīng)的實參無須類型檢查。在第一種形式中,形參聲明后面的逗號是可選的。
返回類型和return語句
return語句終止當(dāng)前正在執(zhí)行的函數(shù)并將控制權(quán)返回到調(diào)用該函數(shù)的地方。return語句有兩種形式:
return; return expression;無返回值函數(shù)
沒有返回值的return 語句只能用在返回類型是void 的函數(shù)中。返回void的函數(shù)不要求非得有return語句,因為在這類函數(shù)的最后一句后面會隱式地執(zhí)行return。
通常情況下,void函數(shù)如果想在它的中間位置提前退出,可以使用return語句。return的這種用法有點類似于我們用break語句退出循環(huán)。例如,可以編寫一個swap函數(shù),使其在參與交換的值相等時什么也不做直接退出:
void swap(int &v1, int &v2) {// if the values are already the same, no need to swap, just returnif (v1 == v2)return;// if we're here, there's work to doint tmp = v2;v2 = v1;v1 = tmp;// no explicit return necessary }這個函數(shù)首先檢查值是否相等,如果相等直接退出函數(shù),如果不相等才交換它們的值。在最后一條賦值語句后面隱式地執(zhí)行return。
一個返回類型是void的函數(shù)也能使用return語句的第二種形式,不過此時return語句的expression必須是另一個返回void的函數(shù)。強行令void函數(shù)返回其他類型的表達式將產(chǎn)生編譯錯誤。
有返回值函數(shù)
return語句的第二種形式提供了函數(shù)的結(jié)果。只要函數(shù)的返回類型不是 void,則該函數(shù)內(nèi)的每條return語句必須返回一個值。return語句返回值的類型必須與函數(shù)的返回類型相同,或者能隱式地轉(zhuǎn)換成函數(shù)的返回類型。
盡管C++無法確保結(jié)果的正確性,但是可以保證每個return語句的結(jié)果類型正確。也許無法顧及所有情況,但是編譯器仍然盡量確保具有返回值的函數(shù)只能通過一條有效的return語句退出。例如:
// incorrect return values, this code will not compile bool str_subrange(const string &str1, const string &str2) {// same sizes: return normal equality testif (str1.size() == str2.size())return str1 == str2;// ok: == returns bool// find the size of the smaller string; conditional operatorauto size = (str1.size() < str2.size()) ? str1.size() : str2.size();// look at each element up to the size of the smaller stringfor (decltype(size) i = 0; i != size; ++i) {if (str1[i] != str2[i])return; // error #1: no return value; compiler should detect this error}// error #2: control might flow off the end of the function without a return// the compiler might not detect this error }在上面的程序中,如果一個string對象是另一個的子集,則函數(shù)在執(zhí)行完for循環(huán)后還將繼續(xù)其執(zhí)行過程,顯然應(yīng)該有一條return 語句專門處理這種情況。編譯器也許能檢測到這個錯誤,也許不能。如果編譯器沒有發(fā)現(xiàn)這個錯誤,則運行時的行為將是未定義的。
WARNING:在含有return語句的循環(huán)后面應(yīng)該也有一條return語句,如果沒有的話該程序就是錯誤的。很多編譯器都無法發(fā)現(xiàn)此類錯誤。
值是如何被返回的
返回一個值的方式和初始化一個變量或形參的方式完全一樣:返回的值用于初始化調(diào)用點的一個臨時量,該臨時量就是函數(shù)調(diào)用的結(jié)果。
必須注意當(dāng)函數(shù)返回局部變量時的初始化規(guī)則。例如我們書寫一個函數(shù),給定計數(shù)值、單詞和結(jié)束符之后,判斷計數(shù)值是否大于1。如果是,返回單詞的復(fù)數(shù)形式。如果不是,返回單詞原形:
//如果ctr的值大于1,返回word的復(fù)數(shù)形式 string make_plural(size_t ctr, const string &word, const string &ending) {return (ctr > 1) ? word + ending : word; }該函數(shù)的返回類型是string,意味著返回值將被拷貝到調(diào)用點。因此,該函數(shù)將返回word的副本或者一個未命名的臨時string對象,該對象的內(nèi)容是word和ending的和。
同其他引用類型一樣,如果函數(shù)返回引用,則該引用僅是它所引對象的一個別名。舉個例子來說明,假定某函數(shù)挑出兩個string形參中較短的那個并返回其引用:
//挑出兩個string對象中較短的那個,返回其引用 const string &shorterstring(const string &sl,const string &s2){return s1.size() <= s2.size() ? s1 : s2 ; }其中形參和返回類型都是const string 的引用,不管是調(diào)用函數(shù)還是返回結(jié)果都不會真正拷貝string對象。
不要返回局部對象的引用或指針
函數(shù)完成后,它所占用的存儲空間也隨之被釋放掉。因此,函數(shù)終止意味著局部變量的引用將指向不再有效的內(nèi)存區(qū)域:
// disaster: this function returns a reference to a local object const string &manip() {string ret; // transform ret in some wayif (!ret.empty())return ret; // WRONG: returning a reference to a local object! elsereturn "Empty"; // WRONG: "Empty" is a local temporary string }上面的兩條 return語句都將返回未定義的值,也就是說,試圖使用manip函數(shù)的返回值將引發(fā)未定義的行為。
- 對于第一條return語句來說,顯然它返回的是局部對象的引用。
- 在第二條return語句中,字符串字面值轉(zhuǎn)換成一個局部臨時string對象,對于manip來說,該對象和 ret一樣都是局部的。
當(dāng)函數(shù)結(jié)束時臨時對象占用的空間也就隨之釋放掉了,所以兩條return語句都指向了不再可用的內(nèi)存空間。
Tip:要想確保返回值安全,我們不妨提問:引用所引的是在函數(shù)之前已經(jīng)存在的哪個對象?
如前所述,返回局部對象的引用是錯誤的;同樣,返回局部對象的指針也是錯誤的。一旦函數(shù)完成,局部對象被釋放,指針將指向一個不存在的對象。
返回類類型的函數(shù)和調(diào)用運算符
和其他運算符一樣,調(diào)用運算符也有優(yōu)先級和結(jié)合律。調(diào)用運算符的優(yōu)先級與點運算符和箭頭運算符相同,并且也符合左結(jié)合律。因此,如果函數(shù)返回指針、引用或類的對象,我們就能使用函數(shù)調(diào)用的結(jié)果訪問結(jié)果對象的成員。
例如,我們可以通過如下形式得到較短string對象的長度:
//調(diào)用string對象的size成員,該string對象是由shorterstring函數(shù)返回的 auto sz = shorterString(s1,s2).size();因為上面提到的運算符都滿足左結(jié)合律,所以 shorterString 的結(jié)果是點運算符的左側(cè)運算對象,點運算符可以得到該string對象的size成員,size又是第二個調(diào)用運算符的左側(cè)運算對象。
引用返回左值
函數(shù)的返回類型決定函數(shù)調(diào)用是否是左值。調(diào)用一個返回引用的函數(shù)得到左值,其他返回類型得到右值。可以像使用其他左值那樣來使用返回引用的函數(shù)的調(diào)用,特別是,我們能為返回類型是非常量引用的函數(shù)的結(jié)果賦值:
char &get_val(string &str, string::size_type ix) {return str[ix]; // get_val assumes the given index is valid } int main() {string s("a value");cout << s << endl; // prints a valueget_val(s, 0) = 'A'; // changes s[0] to A這里函數(shù)調(diào)用是左值,雖然有點怪,但是語法正確的cout << s << endl;// prints A valuereturn 0; }把函數(shù)調(diào)用放在賦值語句的左側(cè)可能看起來有點奇怪,但其實這沒什么特別的。返回值是引用,因此調(diào)用是個左值,和其他左值一樣它也能出現(xiàn)在賦值運算符的左側(cè)。
如果返回類型是常量引用,我們不能給調(diào)用的結(jié)果賦值,這一點和我們熟悉的情況是一樣的:
shorterString ( "hi" , "bye" ) = "X";//錯誤:返回值是個常量列表初始化返回值
C++11新標(biāo)準(zhǔn)規(guī)定,函數(shù)可以返回花括號包圍的值的列表。類似于其他返回結(jié)果,此處的列表也用來對表示函數(shù)返回的臨時量進行初始化。如果列表為空,臨時量執(zhí)行值初始化,否則,返回的值由函數(shù)的返回類型決定。
舉個例子,回憶前文的error_msg函數(shù),該函數(shù)的輸入是一組可變數(shù)量的string 實參,輸出由這些string對象組成的錯誤信息。在下面的函數(shù)中,我們返回一個vector對象,用它存放表示錯誤信息的string對象:
vector<string> process() {// . . .// expected and actual are stringsif (expected.empty())return {}; // return an empty vector else if (expected == actual)return {"functionX", "okay"}; // return list-initialized vector else return {"functionX", expected, actual}; }第一條return語句返回一個空列表,此時,process 函數(shù)返回的vector對象是空的。如果expected不為空,根據(jù)expected和actual是否相等,函數(shù)返回的vector對象分別用兩個或三個元素初始化。
如果函數(shù)返回的是內(nèi)置類型,則花括號包圍的列表最多包含一個值,而且該值所占空間不應(yīng)該大于目標(biāo)類型的空間。如果函數(shù)返回的是類類型,由類本身定義初始值如何使用。
主函數(shù)main的返回值
之前介紹過,如果函數(shù)的返回類型不是void,那么它必須返回一個值。
但是這條規(guī)則有個例外:我們允許main函數(shù)沒有return語句直接結(jié)束。如果控制到達了main函數(shù)的結(jié)尾處而且沒有return語句,編譯器將隱式地插入一條返回0的return語句。
第1章介紹的,main函數(shù)的返回值可以看做是狀態(tài)指示器。返回0表示執(zhí)行成功,返回其他值表示執(zhí)行失敗,其中非0值的具體含義依機器而定。為了使返回值與機器無關(guān),cstdlib頭文件定義了兩個預(yù)處理變量,我們可以使用這兩個變量分別表示成功與失敗:
int main() {if (some_failure)return EXIT_FAILURE; // defined in cstdlib elsereturn EXIT_SUCCESS; // defined in cstdlib }因為它們是預(yù)處理變量,所以既不能在前面加上std::,也不能在using聲明中出現(xiàn)。
遞歸
如果一個函數(shù)調(diào)用了它自身,不管這種調(diào)用是直接的還是間接的,都稱該函數(shù)為遞歸函數(shù)(recursive function)。舉個例子,我們可以使用遞歸函數(shù)重新實現(xiàn)求階乘的功能:
//計算val的階乘,即1 * 2* 3 ...* val int factorial (int val) {if (val > 1)return factorial (val-1)* val;return 1; }在上面的代碼中,我們遞歸地調(diào)用factorial 函數(shù)以求得從val中減去1后新數(shù)字的階乘。當(dāng)val遞減到1時,遞歸終止,返回1。
在遞歸函數(shù)中,一定有某條路徑是不包含遞歸調(diào)用的,否則,函數(shù)將“永遠”遞歸下去,換句話說,函數(shù)將不斷地調(diào)用它自身直到程序棧空間耗盡為止。我們有時候會說這種函數(shù)含有遞歸循環(huán)(recursion loop)。在factorial函數(shù)中,遞歸終止的條件是val等于1。
下面的表格顯示了當(dāng)給factorial函數(shù)傳入?yún)?shù)5時,函數(shù)的執(zhí)行軌跡。
| factorial(5) | factorial(4) * 5 | 120 |
| factorial(4) | factorial(3) * 4 | 24 |
| factorial(3) | factorial(2) * 3 | 6 |
| factorial(2) | factorial(1) * 2 | 2 |
| factorial(1) | 1 | 1 |
Note:main函數(shù)不能調(diào)用它自己。
返回數(shù)組指針
因為數(shù)組不能被拷貝,所以函數(shù)不能返回數(shù)組。不過,函數(shù)可以返回數(shù)組的指針或引用。雖然從語法上來說,要想定義一個返回數(shù)組的指針或引用的函數(shù)比較煩瑣,但是有一些方法可以簡化這一任務(wù),其中最直接的方法是使用類型別名:
typedef int arrT[10];// arrT是一個類型別名,它表示的類型是含有10個整數(shù)的數(shù)組 using arrT = int [10];// arrT的等價聲明 arrT* func(int i) ;// func返回一個指向含有10個整數(shù)的數(shù)組的指針其中 arrT是含有10個整數(shù)的數(shù)組的別名。因為我們無法返回數(shù)組,所以將返回類型定義成數(shù)組的指針。因此,func函數(shù)接受一個int實參,返回一個指向包含10個整數(shù)的數(shù)組的指針。
聲明一個返回數(shù)組指針的函數(shù)
要想在聲明func時不使用類型別名,我們必須牢記被定義的名字后面數(shù)組的維度:
int arr[10] ;// arr是一個含有10個整數(shù)的數(shù)組 int *p1[10] ;//p1是一個含有10個指針的數(shù)組 int (*p2)[10] = &arr;// p2是一個指針,它指向含有10個整數(shù)的數(shù)組和這些聲明一樣,如果我們想定義一個返回數(shù)組指針的函數(shù),則數(shù)組的維度必須跟在函數(shù)名字之后。然而,函數(shù)的形參列表也跟在函數(shù)名字后面且形參列表應(yīng)該先于數(shù)組的維度。因此,返回數(shù)組指針的函數(shù)形式如下所示:
Type (*function(parameter_list))[dimension]類似于其他數(shù)組的聲明,Type表示元素的類型,dimension表示數(shù)組的大小。(*function(parameter_list))兩端的括號必須存在,就像我們定義p2時兩端必須有括號一樣。如果沒有這對括號,函數(shù)的返回類型將是指針的數(shù)組。
舉個具體點的例子,下面這個func函數(shù)的聲明沒有使用類型別名:
int (*func(int i))[10];可以按照以下的順序來逐層理解該聲明的含義:
-
func(int i)表示調(diào)用func函數(shù)時需要一個int類型的實參。
-
(*func(int i))意味著我們可以對函數(shù)調(diào)用的結(jié)果執(zhí)行解引用操作。(*不是指針聲明符)
-
(*func(int i))[10]表示解引用func的調(diào)用將得到一個大小是10的數(shù)組。
-
int (*func (int i))[10]表示數(shù)組中的元素是int類型。
使用尾置返回類型
在C++11新標(biāo)準(zhǔn)中還有一種可以簡化上述func聲明的方法,就是使用尾置返回類型( trailing return type)。任何函數(shù)的定義都能使用尾置返回,但是這種形式對于返回類型比較復(fù)雜的函數(shù)最有效,比如返回類型是數(shù)組的指針或者數(shù)組的引用。尾置返回類型跟在形參列表后面并以一個->符號開頭。為了表示函數(shù)真正的返回類型跟在形參列表之后,我們在本應(yīng)該出現(xiàn)返回類型的地方放置一個auto:
//func接受一個int類型的實參,返回一個指針,該指針指向含有10個整數(shù)的數(shù)組 auto func(int i) -> int (*)[10];因為我們把函數(shù)的返回類型放在了形參列表之后,所以可以清楚地看到func函數(shù)返回的是一個指針,并且該指針指向了含有10個整數(shù)的數(shù)組。
使用decltype
還有一種情況,如果我們知道函數(shù)返回的指針將指向哪個數(shù)組,就可以使用decltype關(guān)鍵字聲明返回類型。例如,下面的函數(shù)返回一個指針,該指針根據(jù)參數(shù)i的不同指向兩個已知數(shù)組中的某一個:
int odd[] = {1,3,5,7,9}; int even[] = {0,2,4,6,8}; //返回一個指針,該指針指向含有5個整數(shù)的數(shù)組 decltype(odd) *arrPtr(int i){return (i % 2) ? &odd : &even;//返回一個指向數(shù)組的指針 }arrPtr使用關(guān)鍵字 decltype表示它的返回類型是個指針,并且該指針?biāo)傅膶ο笈codd 的類型一致。因為 odd是數(shù)組,所以arrPtr返回一個指向含有5個整數(shù)的數(shù)組的指針。
有一個地方需要注意:decltype并不負責(zé)把數(shù)組類型轉(zhuǎn)換成對應(yīng)的指針,所以decltype的結(jié)果是個數(shù)組,要想表示arrPtr返回指針還必須在函數(shù)聲明時加一個*符號。
函數(shù)重載
如果同一作用域內(nèi)的幾個函數(shù)名字相同但形參列表不同,我們稱之為重載(overloaded)函數(shù)。例如,上文中我們定義了幾個名為print的函數(shù):
void print(const char *cp); void print(const int *beg, const int *end); void print(const int ia[], size_t size);這些函數(shù)接受的形參類型不一樣,但是執(zhí)行的操作非常類似。當(dāng)調(diào)用這些函數(shù)時,編譯器會根據(jù)傳遞的實參類型推斷想要的是哪個函數(shù):
int j[2] = {0,1}; print("Hello World"); // calls print(const char*) print(j, end(j) - begin(j)); // calls print(const int*, size_t) print(begin(j), end(j)); // calls print(const int*, const int*)函數(shù)的名字僅僅是讓編譯器知道它調(diào)用的是哪個函數(shù),而函數(shù)重載可以在一定程度上減輕程序員起名字、記名字的負擔(dān)。
Note:main函數(shù)不能重載。
定義重載函數(shù)
有一種典型的數(shù)據(jù)庫應(yīng)用,需要創(chuàng)建幾個不同的函數(shù)分別根據(jù)名字、電話、賬戶號碼等信息查找記錄。函數(shù)重載使得我們可以定義一組函數(shù),它們的名字都是lookup,但是查找的依據(jù)不同。我們能通過以下形式中的任意一種調(diào)用lookup函數(shù):
Record lookup(const Account&); // find by Account Record lookup(const Phone&); // find by Phone Record lookup(const Name&); // find by Name Account acct; Phone phone; Record r1 = lookup(acct); // call version that takes an Account Record r2 = lookup(phone); // call version that takes a Phone其中,雖然我們定義的三個函數(shù)各不相同,但它們都有同一個名字。編譯器根據(jù)實參的類型確定應(yīng)該調(diào)用哪一個函數(shù)。
對于重載的函數(shù)來說,它們應(yīng)該在形參數(shù)量或形參類型上有所不同。在上面的代碼中,雖然每個函數(shù)都只接受一個參數(shù),但是參數(shù)的類型不同。
不允許兩個函數(shù)除了返回類型外其他所有的要素都相同。假設(shè)有兩個函數(shù),它們的形參列表一樣但是返回類型不同,則第二個函數(shù)的聲明是錯誤的:
Record lookup(const Account&); bool lookup(const Account&); // error: only the return type is different判斷兩個形參的類型是否相異
有時候兩個形參列表看起來不一樣,但實際上是相同的:(似非而是)
// each pair declares the same function Record lookup(const Account &acct); Record lookup(const Account&); // parameter names are ignoredtypedef Phone Telno; Record lookup(const Phone&); Record lookup(const Telno&); // Telno and Phone are the same type第一對聲明中,第一個函數(shù)給它的形參起了名字,第二個函數(shù)沒有。形參的名字僅僅起到幫助記憶的作用,有沒有它并不影響形參列表的內(nèi)容。
第二對聲明看起來類型不同,但事實上Telno不是一種新類型,它只是 Phone的別名而已。類型別名為已存在的類型提供另外一個名字,它并不是創(chuàng)建新類型。因此,第二對中兩個形參的區(qū)別僅在于一個使用類型原來的名字,另一個使用它的別名,從本質(zhì)上來說它們沒什么不同。
重載和const形參
前文介紹,頂層const(第2章)不影響傳入函數(shù)的對象。一個擁有頂層const的形參無法和另一個沒有頂層const的形參區(qū)分開來:
Record lookup(Phone); Record lookup(const Phone); // redeclares Record lookup(Phone) Record lookup(Phone*); Record lookup(Phone* const); // redeclares Record lookup(Phone*)在這兩組函數(shù)聲明中,每一組的第二個聲明和第-一個聲明是等價的。
另一方面,如果形參是某種類型的指針或引用,則通過區(qū)分其指向的是常量對象還是非常量對象可以實現(xiàn)函數(shù)重載,此時的const是底層的:
// functions taking const and nonconst references or pointers have different parameters // declarations for four independent, overloaded functions Record lookup(Account&); // function that takes a reference to Account Record lookup(const Account&); // new function that takes a const reference Record lookup(Account*); // new function, takes a pointer to Account Record lookup(const Account*); // new function, takes a pointer to const在上面的例子中,編譯器可以通過實參是否是常量來推斷應(yīng)該調(diào)用哪個函數(shù)。
因為const不能轉(zhuǎn)換成其他類型(第4章“其他隱式類型轉(zhuǎn)換”節(jié)內(nèi)容),所以我們只能把const對象(或指向const的指針)傳遞給const形參。
相反的,因為非常量可以轉(zhuǎn)換成const,所以上面的4個函數(shù)都能作用于非常量對象或者指向非常量對象的指針。不過,接下來將要介紹的,當(dāng)我們傳遞一個非常量對象或者指向非常量對象的指針時,編譯器會優(yōu)先選用非常量版本的函數(shù)。
建議:何時不應(yīng)該重載函數(shù)
盡管函數(shù)重載能在一定程度上減輕我們?yōu)楹瘮?shù)起名字、記名字的負擔(dān),但是最好只重載那些確實非常相似的操作。有些情況下,給函數(shù)起不同的名字能使得程序更易理解。舉個例子,下面是幾個負責(zé)移動屏幕光標(biāo)的函數(shù):
Screen& moveHome(); screen& moveAbs(int, int); Screen& moveRel(int, int, string direction);乍看上去,似平可以把這組函數(shù)統(tǒng)一命名為move,從而實現(xiàn)函數(shù)的重載:
Screen& move(); Screen& move(int, int); Screen& move(int, int, string direction);其實不然,重載之后這些函數(shù)失去了名字中本來擁有的信息。盡管這些函數(shù)確實都是在移動光標(biāo),但是具體移動的方式卻各不相同。以moveHome為例,它表示的是移動光標(biāo)的一種特殊實例。
一般來說,是否重載函數(shù)要看哪個更容易理解:
//哪種形式更容易理解呢? myscreen.moveHome();//我們認(rèn)為應(yīng)該是這一個! myscreen.move();const_cast和重載
const_cast為第4章內(nèi)容。
const_cast只能改變運算對象的底層const。
const char *pc; char *p = const_cast<char*>(pc);// 正確:但是通過p 寫值是未定義的行為對于將常量對象轉(zhuǎn)換成非常量對象的行為,我們一般稱其為“去掉const性質(zhì)(cast away the const)”。一旦我們?nèi)サ袅四硞€對象的const性質(zhì),編譯器就不再阻止我們對該對象進行寫操作了。
回憶上文的shorterString函數(shù):
// return a reference to the shorter of two strings const string &shorterString(const string &s1, const string &s2) {return s1.size() <= s2.size() ? s1 : s2; }這個函數(shù)的參數(shù)和返回類型都是 const string 的引用。我們可以對兩個非常量的string實參調(diào)用這個函數(shù),但返回的結(jié)果仍然是const string 的引用。
因此我們需要一種新的 shorterString函數(shù),當(dāng)它的實參不是常量時,得到的結(jié)果是一個普通的引用,使用const_cast可以做到這一點:
string &shorterString(string &s1, string &s2) {auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));return const_cast<string&>(r); }在這個版本的函數(shù)中,首先將它的實參強制轉(zhuǎn)換成對const 的引用,然后調(diào)用了shorterString函數(shù)的const版本。const版本返回對const string的引用,這個引用事實上綁定在了某個初始的非常量實參上。因此,我們可以再將其轉(zhuǎn)換回一個普通的string&,這顯然是安全的。
(MyNote:因為const不能轉(zhuǎn)換成其他類型(第4章“其他隱式類型轉(zhuǎn)換”節(jié)內(nèi)容),如果想修改函數(shù)返回的const字符串對象,顯然是不行的。)
調(diào)用重載的函數(shù)
定義了一組重載函數(shù)后,我們需要以合理的實參調(diào)用它們。函數(shù)匹配( function matching)是指一個過程,在這個過程中我們把函數(shù)調(diào)用與一組重載函數(shù)中的某一個關(guān)聯(lián)起來,函數(shù)匹配也叫做重載確定(overload resolution)。編譯器首先將調(diào)用的實參與重載集合中每一個函數(shù)的形參進行比較,然后根據(jù)比較的結(jié)果決定到底調(diào)用哪個函數(shù)。
在很多(可能是大多數(shù))情況下,程序員很容易判斷某次調(diào)用是否合法,以及當(dāng)調(diào)用合法時應(yīng)該調(diào)用哪個函數(shù)。通常,重載集中的函數(shù)區(qū)別明顯,它們要不然是參數(shù)的數(shù)量不同,要不就是參數(shù)類型毫無關(guān)系。
此時,確定調(diào)用哪個函數(shù)比較容易。但是在另外一些情況下要想選擇函數(shù)就比較困難了,比如當(dāng)兩個重載函數(shù)參數(shù)數(shù)量相同且參數(shù)類型可以相互轉(zhuǎn)換時(第4章“類型轉(zhuǎn)換”)。我們將在本章“函數(shù)匹配”節(jié)介紹當(dāng)函數(shù)調(diào)用存在類型轉(zhuǎn)換時編譯器處理的方法。
現(xiàn)在我們需要掌握的是,當(dāng)調(diào)用重載函數(shù)時有三種可能的結(jié)果:
- 編譯器找到一個與實參最佳匹配(best match)的函數(shù),并生成調(diào)用該函數(shù)的代碼。
- 找不到任何一個函數(shù)與調(diào)用的實參匹配,此時編譯器發(fā)出無匹配(no match)的錯誤信息。
- 有多于一個函數(shù)可以匹配,但是每一個都不是明顯的最佳選擇。此時也將發(fā)生錯誤,稱為二義性調(diào)用(ambiguous call)。
重載與作用域
WARNING:一般來說,將函數(shù)聲明置于局部作用域內(nèi)不是一個明智的選擇。但是為了說明作用域和重載的相互關(guān)系,我們將暫時違反這一原則而使用局部函數(shù)聲明。
對于剛接觸C++的程序員來說,不太容易理清作用域和重載的關(guān)系。其實,重載對作用域的一般性質(zhì)并沒有什么改變:如果我們在內(nèi)層作用域中聲明名字,它將隱藏外層作用域中聲明的同名實體。在不同的作用域中無法重載函數(shù)名:
string read(); void print(const string &); void print(double); // overloads the print function void fooBar(int ival) {bool read = false; // new scope: hides the outer declaration of readstring s = read(); // error: read is a bool variable, not a function// bad practice: usually it's a bad idea to declare functions at local scopevoid print(int); // new scope: hides previous instances of printprint("Value: "); // error: print(const string &) is hiddenprint(ival); // ok: print(int) is visibleprint(3.14); // ok: calls print(int); print(double) is hidden }大多數(shù)讀者都能理解調(diào)用read函數(shù)會引發(fā)錯誤。因為當(dāng)編譯器處理調(diào)用read的請求時,找到的是定義在局部作用域中的read。這個名字是個布爾變量,而我們顯然無法調(diào)用一個布爾值,因此該語句非法。
調(diào)用print函數(shù)的過程非常相似。在fooBar內(nèi)聲明的print(int)隱藏了之前兩個print函數(shù),因此只有一個print函數(shù)是可用的:該函數(shù)以int值作為參數(shù)。
當(dāng)我們調(diào)用print函數(shù)時,編譯器首先尋找對該函數(shù)名的聲明,找到的是接受 int值的那個局部聲明。一旦在當(dāng)前作用域中找到了所需的名字,編譯器就會忽略掉外層作用域中的同名實體。剩下的工作就是檢查函數(shù)調(diào)用是否有效了。
Note:在C++語言中,名字查找發(fā)生在類型檢查之前。
第一個調(diào)用傳入一個字符串字面值,但是當(dāng)前作用域內(nèi)print 函數(shù)唯一的聲明要求參數(shù)是int類型。字符串字面值無法轉(zhuǎn)換成int類型,所以這個調(diào)用是錯誤的。在外層作用域中的print (const string&)函數(shù)雖然與本次調(diào)用匹配,但是它已經(jīng)被隱藏掉了,根本不會被考慮。
當(dāng)我們?yōu)閜rint函數(shù)傳入一個double類型的值時,重復(fù)上述過程。編譯器在當(dāng)前作用域內(nèi)發(fā)現(xiàn)了print(int)函數(shù),double類型的實參轉(zhuǎn)換成int類型,因此調(diào)用是合法的。
假設(shè)我們把print(int)和其他print函數(shù)聲明放在同一個作用域中,則它將成為另一種重載形式。此時,因為編譯器能看到所有三個函數(shù),上述調(diào)用的處理結(jié)果將完全不同:
void print(const string &); void print(double); // overloads the print function void print(int); // another overloaded instance void fooBar2(int ival) {print("Value: "); // calls print(const string &)print(ival); // calls print(int)print(3.14); // calls print(double) }特殊用途語言特性
默認(rèn)實參
某些函數(shù)有這樣一種形參,在函數(shù)的很多次調(diào)用中它們都被賦予一個相同的值,此時,我們把這個反復(fù)出現(xiàn)的值稱為函數(shù)的默認(rèn)實參(default argument)。調(diào)用含有默認(rèn)實參的函數(shù)時,可以包含該實參,也可以省略該實參。
例如,我們使用string對象表示窗口的內(nèi)容。一般情況下,我們希望該窗口的高、寬和背景字符都使用默認(rèn)值。但是同時我們也應(yīng)該允許用戶為這幾個參數(shù)自由指定與默認(rèn)值不同的數(shù)值。為了使得窗口函數(shù)既能接納默認(rèn)值,也能接受用戶指定的值,我們把它定義成如下的形式:
typedef string::size_type sz; string screen(sz ht = 24, sz wid = 80, char backgrnd = '');其中我們?yōu)槊恳粋€形參都提供了默認(rèn)實參,默認(rèn)實參作為形參的初始值出現(xiàn)在形參列表中。我們可以為一個或多個形參定義默認(rèn)值,不過需要注意的是,一旦某個形參被賦予了默認(rèn)值,它后面的所有形參都必須有默認(rèn)值。
使用默認(rèn)實參調(diào)用函數(shù)
如果我們想使用默認(rèn)實參,只要在調(diào)用函數(shù)的時候省略該實參就可以了。例如,screen 函數(shù)為它的所有形參都提供了默認(rèn)實參,所以我們可以使用0、1、2或3個實參調(diào)用該函數(shù):
string window; window = screen(); // equivalent to screen(24,80,' ') window = screen(66);// equivalent to screen(66,80,' ') window = screen(66, 256); // screen(66,256,' ') window = screen(66, 256, '#'); // screen(66,256,'#')函數(shù)調(diào)用時實參按其位置解析,默認(rèn)實參負責(zé)填補函數(shù)調(diào)用缺少的尾部實參(靠右側(cè)位置)。例如,要想覆蓋backgrnd的默認(rèn)值,必須為ht和wid提供實參:
window = screen(, , '?'); // error: can omit only trailing arguments window = screen('?'); // calls screen('?',80,' ')需要注意,第二個調(diào)用傳遞一個字符值,是合法的調(diào)用。然而盡管如此,它的實際效果卻與書寫的意圖不符。
該調(diào)用之所以合法是因為’?‘是個char,而函數(shù)最左側(cè)形參的類型string::size_type是一種無符號整數(shù)類型,所以char類型可以轉(zhuǎn)換成函數(shù)最左側(cè)形參的類型。當(dāng)該調(diào)用發(fā)生時,char類型的實參隱式地轉(zhuǎn)換成string::size_type,然后作為height的值傳遞給函數(shù)。在我們的機器上,’?'對應(yīng)的十六進制數(shù)是0x3F,也就是十進制數(shù)的63,所以該調(diào)用把值63傳給了形參height。
當(dāng)設(shè)計含有默認(rèn)實參的函數(shù)時,其中一項任務(wù)是合理設(shè)置形參的順序,盡量讓不怎么使用默認(rèn)值的形參出現(xiàn)在前面,而讓那些經(jīng)常使用默認(rèn)值的形參出現(xiàn)在后面。
默認(rèn)實參聲明
對于函數(shù)的聲明來說,通常的習(xí)慣是將其放在頭文件中,并且一個函數(shù)只聲明一次,但是多次聲明同一個函數(shù)也是合法的。不過有一點需要注意,在給定的作用域中一個形參只能被賦予一次默認(rèn)實參。換句話說,函數(shù)的后續(xù)聲明只能為之前那些沒有默認(rèn)值的形參添加默認(rèn)實參,而且該形參右側(cè)的所有形參必須都有默認(rèn)值。假如給定
//表示高度和寬度的形參沒有默認(rèn)值 string screen(sz, sz, char = ' ');我們不能修改一個已經(jīng)存在的默認(rèn)值:
string screen(sz, sz, char = '*"); //錯誤:重復(fù)聲明但是可以按照如下形式添加默認(rèn)實參:
string screen(sz = 24, sz = 80, char); //正確:添加默認(rèn)實參Best Practices:通常,應(yīng)該在函數(shù)聲明中指定默認(rèn)實參,并將該聲明放在合適的頭文件中。
默認(rèn)實參初始值
局部變量不能作為默認(rèn)實參。除此之外,只要表達式的類型能轉(zhuǎn)換成形參所需的類型,該表達式就能作為默認(rèn)實參:
// the declarations of wd, def, and ht must appear outside a function sz wd = 80; char def = ' '; sz ht(); string screen(sz = ht(), sz = wd, char = def); string window = screen(); // calls screen(ht(), 80, ' ')用作默認(rèn)實參的名字在函數(shù)聲明所在的作用域內(nèi)解析,而這些名字的求值過程發(fā)生在函數(shù)調(diào)用時:
void f2() {def = '*'; // changes the value of a default argumentsz wd = 100; // hides the outer definition of wd but does not change the default//這里wd是局部變量,上面的wd是全部變量。window = screen(); // calls screen(ht(), 80, '*') }我們在函數(shù)f2內(nèi)部改變了def 的值,所以對screen的調(diào)用將會傳遞這個更新過的值。另一方面,雖然我們的函數(shù)還聲明了一個局部變量用于隱藏外層的 wd,但是該局部變量與傳遞給screen的默認(rèn)實參沒有任何關(guān)系。
內(nèi)聯(lián)函數(shù)和constexpr函數(shù)
上文我們編寫了一個小函數(shù)shorterString,它的功能是比較兩個string 形參的長度并返回長度較小的string的引用。把這種規(guī)模較小的操作定義成函數(shù)有很多好處,主要包括:
- 閱讀和理解shorterString函數(shù)的調(diào)用要比讀懂等價的條件表達式容易得多。
- 使用函數(shù)可以確保行為的統(tǒng)一,每次相關(guān)操作都能保證按照同樣的方式進行。
- 如果我們需要修改計算過程,顯然修改函數(shù)要比先找到等價表達式所有出現(xiàn)的地方再逐一修改更容易。
- 函數(shù)可以被其他應(yīng)用重復(fù)利用,省去了程序員重新編寫的代價。
然而,使用shorterstring 函數(shù)也存在一個潛在的缺點:調(diào)用函數(shù)一般比求等價表達式的值要慢一些。
在大多數(shù)機器上,一次函數(shù)調(diào)用其實包含著一系列工作:
- 調(diào)用前要先保存寄存器,并在返回時恢復(fù);
- 可能需要拷貝實參;
- 程序轉(zhuǎn)向一個新的位置繼續(xù)執(zhí)行。
內(nèi)聯(lián)函數(shù)可避免函數(shù)調(diào)用的開銷
將函數(shù)指定為內(nèi)聯(lián)函數(shù)(inline),通常就是將它在每個調(diào)用點上“內(nèi)聯(lián)地”展開。
假設(shè)我們把shorterString函數(shù)定義成內(nèi)聯(lián)函數(shù),則如下調(diào)用
cout<< shorterstring (s1, s2) <<endl;將在編譯過程中展開成類似于下面的形式
cout << (s1.size() < s2.size() ? s1 : s2) << endl;從而消除了shorterString函數(shù)的運行時開銷。
在shorterString函數(shù)的返回類型前面加上關(guān)鍵字inline,這樣就可以將它聲明成內(nèi)聯(lián)函數(shù)了:
//內(nèi)聯(lián)版本:尋找兩個string對象中較短的那個 inline const string &shorterstring(const string &s1,const string &s2){return s1.size() <= s2.size() ? s1 : s2; }Note:內(nèi)聯(lián)說明只是向編譯器發(fā)出的一個請求,編譯器可以選擇忽略這個請求。
一般來說,內(nèi)聯(lián)機制用于優(yōu)化規(guī)模較小、流程直接、頻繁調(diào)用的函數(shù)。很多編譯器都不支持內(nèi)聯(lián)遞歸函數(shù),而且一個75行的函數(shù)也不大可能在調(diào)用點內(nèi)聯(lián)地展開。
constexpr函數(shù)
constexpr函數(shù)(constexpr function)是指能用于常量表達式(第2章內(nèi)容)的函數(shù)。定義 constexpr函數(shù)的方法與其他函數(shù)類似,不過要遵循幾項約定:函數(shù)的返回類型及所有形參的類型都得是字面值類型(第2章內(nèi)容),而且函數(shù)體中必須有且只有一條return語句:
constexpr int new_sz() { return 42;} constexpr int foo = new_sz();//正確: foo是一個常量表達式我們把new_sz定義成無參數(shù)的constexpr函數(shù)。因為編譯器能在程序編譯時驗證new_sz函數(shù)返回的是常量表達式,所以可以用new_sz函數(shù)初始化constexpr類型的變量foo。
執(zhí)行該初始化任務(wù)時,編譯器把對constexpr函數(shù)的調(diào)用替換成其結(jié)果值。為了能在編譯過程中隨時展開,constexpr函數(shù)被隱式地指定為內(nèi)聯(lián)函數(shù)。
constexpr函數(shù)體內(nèi)也可以包含其他語句,只要這些語句在運行時不執(zhí)行任何操作就行。例如,constexpr函數(shù)中可以有空語句、類型別名以及using聲明。
我們允許constexpr函數(shù)的返回值并非一個常量:
//如果arg是常量表達式,則scale(arg)也是常量表達式 constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }當(dāng)scale的實參是常量表達式時,它的返回值也是常量表達式;反之則不然:
int arr[scale(2)]; //正確: scale(2)是常量表達式 int i = 2;//i不是常量表達式 ,const int i = 2;才是 int a2[scale(i)] ;//錯誤:scale (i)不是常量表達式如上例所示,當(dāng)我們給scale函數(shù)傳入一個形如字面值2的常量表達式時,它的返回類型也是常量表達式。此時,編譯器用相應(yīng)的結(jié)果值替換對scale函數(shù)的調(diào)用。
如果我們用一個非常量表達式調(diào)用scale函數(shù),比如int類型的對象i,則返回值是一個非常量表達式。當(dāng)把 scale函數(shù)用在需要常量表達式的上下文中時,由編譯器負責(zé)檢查函數(shù)的結(jié)果是否符合要求。如果結(jié)果恰好不是常量表達式,編譯器將發(fā)出錯誤信息。
Note:constexpr函數(shù)不一定返回常量表達式。
把內(nèi)聯(lián)函數(shù)和constexpr函數(shù)放在頭文件內(nèi)
和其他函數(shù)不一樣,內(nèi)聯(lián)函數(shù)和 constexpr函數(shù)可以在程序中多次定義。畢竟,編譯器要想展開函數(shù)僅有函數(shù)聲明是不夠的,還需要函數(shù)的定義。不過,對于某個給定的內(nèi)聯(lián)函數(shù)或者constexpr函數(shù)來說,它的多個定義必須完全一致。基于這個原因,內(nèi)聯(lián)函數(shù)和constexpr函數(shù)通常定義在頭文件中。
調(diào)試幫助
C++程序員有時會用到一種類似于頭文件保護(第2章有相關(guān)論述)的技術(shù),以便有選擇地執(zhí)行調(diào)試代碼。基本思想是,程序可以包含一些用于調(diào)試的代碼,但是這些代碼只在開發(fā)程序時使用。當(dāng)應(yīng)用程序編寫完成準(zhǔn)備發(fā)布時,要先屏蔽掉調(diào)試代碼。這種方法用到兩項預(yù)處理功能:assert和 NDEBUG。
assert預(yù)處理宏
assert是一種預(yù)處理宏(preprocessor marco)。所謂預(yù)處理宏其實是一個預(yù)處理變量,它的行為有點類似于內(nèi)聯(lián)函數(shù)。assert宏使用一個表達式作為它的條件:
assert (expr) ;首先對expr求值,如果表達式為假(即 0),assert輸出信息并終止程序的執(zhí)行。如果表達式為真(即非0),assert什么也不做。
assert宏定義在cassert頭文件中。如我們所知,預(yù)處理名字由預(yù)處理器而非編譯器管理,因此我們可以直接使用預(yù)處理名字而無須提供using聲明。也就是說,我們應(yīng)該使用assert而不是std::assert,也不需要為assert提供using聲明。
和預(yù)處理變量一樣,宏名字在程序內(nèi)必須唯一。含有cassert頭文件的程序不能再定義名為assert 的變量、函數(shù)或者其他實體。在實際編程過程中,即使我們沒有包含cassert頭文件,也最好不要為了其他目的使用assert。很多頭文件都包含了cassert,這就意味著即使你沒有直接包含 cassert,它也很有可能通過其他途徑包含在你的程序中。
assert 宏常用于檢查“不能發(fā)生”的條件。例如,一個對輸入文本進行操作的程序可能要求所有給定單詞的長度都大于某個閾值。此時,程序可以包含一條如下所示的語句:
assert(word.size() > threshold);NDEBUG預(yù)處理變量
assert的行為依賴于一個名為NDEBUG 的預(yù)處理變量的狀態(tài)。如果定義了NDEBUG,則assert 什么也不做。默認(rèn)狀態(tài)下沒有定義NDEBUG,此時 assert將執(zhí)行運行時檢查。
我們可以使用一個#define語句定義NDEBUG,從而關(guān)閉調(diào)試狀態(tài)。
同時,很多編譯器都提供了一個命令行選項使我們可以定義預(yù)處理變量:
$ cc -D NDEBUG main.c # use /D with the Microsoft compiler這條命令的作用等價于在main.c文件的一開始寫#define NDEBUG。
定義NDEBUG能避免檢查各種條件所需的運行時開銷,當(dāng)然此時根本就不會執(zhí)行運行時檢查。因此,assert應(yīng)該僅用于驗證那些確實不可能發(fā)生的事情。我們可以把assert當(dāng)成調(diào)試程序的一種輔助手段,但是不能用它替代真正的運行時邏輯檢查,也不能替代程序本身應(yīng)該包含的錯誤檢查。
除了用于assert外,也可以使用NDEBUG編寫自己的條件調(diào)試代碼。如果NDEBUG未定義,將執(zhí)行#ifndef和#endif之間的代碼;如果定義了NDEBUG,這些代碼將被忽略掉:
void print(const int ia[], size_t size) { #ifndef NDEBUG// _ _func_ _ is a local static defined by the compiler that holds the function's namecerr << __func__ << ": array size is " << size << endl; #endif // ...在這段代碼中,我們使用變量__func__輸出當(dāng)前調(diào)試的函數(shù)的名字。編譯器為每個函數(shù)都定義了__func__,它是const char的一個靜態(tài)數(shù)組,用于存放函數(shù)的名字。
除了C++編譯器定義的__func__之外,預(yù)處理器還定義了另外4個對于程序調(diào)試很有用的名字:
- __FILE__存放文件名的字符串字面值。
- __LINE__存放當(dāng)前行號的整型字面值。
- __TIMEB__存放文件編譯時間的字符串字面值。
- __DATE__存放文件編譯日期的字符串字面值。
可以使用這些常量在錯誤消息中提供更多信息:
if (word.size() < threshold)cerr << "Error: " << _ _FILE_ _<< " : in function " << _ _func_ _<< " at line " << _ _LINE_ _ << endl<< " Compiled on " << _ _DATE_ _<< " at " << _ _TIME_ _ << endl<< " Word read was \"" << word<< "\": Length too short" << endl;如果我們給程序提供了一個長度小于threshold的string對象,將得到下面的錯誤消息:
Error : wdebug.cc : in function main at line 27Compiled on Jul 11 2012 at 20:50:03Word read was "foo" : Length too short函數(shù)匹配
例子:調(diào)用應(yīng)該選用哪個重載函數(shù)
在大多數(shù)情況下,我們?nèi)菀状_定某次調(diào)用應(yīng)該選用哪個重載函數(shù)。然而,當(dāng)幾個重載函數(shù)的形參數(shù)量相等以及某些形參的類型可以由其他類型轉(zhuǎn)換得來時,這項工作就不那么容易了。以下面這組函數(shù)及其調(diào)用為例:
void f(); void f (int) ; void f (int, int) ; void f (double, double = 3.14); f (5.6);//調(diào)用void f (double, double)確定候選函數(shù)和可行函數(shù)
函數(shù)匹配的第一步是選定本次調(diào)用對應(yīng)的重載函數(shù)集,集合中的函數(shù)稱為候選函數(shù)(candidate function)。候選函數(shù)具備兩個特征:
在這個例子中,有4個名為f的候選函數(shù)。
第二步考察本次調(diào)用提供的實參,然后從候選函數(shù)中選出能被這組實參調(diào)用的函數(shù),這些新選出的函數(shù)稱為可行函數(shù)(viable function)。可行函數(shù)也有兩個特征:
我們能根據(jù)實參的數(shù)量從候選函數(shù)中排除掉兩個。不使用形參的函數(shù)和使用兩個int形參的函數(shù)顯然都不適合本次調(diào)用,這是因為我們的調(diào)用只提供了一個實參,而它們分別有0個和兩個形參。
使用一個 int形參的函數(shù)和使用兩個double形參的函數(shù)是可行的,它們都能用一個實參調(diào)用。其中最后那個函數(shù)本應(yīng)該接受兩個double值,但是因為它含有一個默認(rèn)實參,所以只用一個實參也能調(diào)用它。
Note:如果函數(shù)含有默認(rèn)實參,則我們在調(diào)用該函數(shù)時傳入的實參數(shù)量可能少于它實際使用的實參數(shù)量。
在使用實參數(shù)量初步判別了候選函數(shù)后,接下來考察實參的類型是否與形參匹配。和一般的函數(shù)調(diào)用類似,實參與形參匹配的含義可能是它們具有相同的類型,也可能是實參類型和形參類型滿足轉(zhuǎn)換規(guī)則。在上面的例子中,剩下的兩個函數(shù)都是可行的:
-
f(int)是可行的,因為實參類型double 能轉(zhuǎn)換成形參類型int。
-
f(double,double)是可行的,因為它的第二個形參提供了默認(rèn)值,而第一個形參的類型正好是 double,與函數(shù)使用的實參類型完全一致。
Note:如果沒找到可行函數(shù),編譯器將報告無匹配函數(shù)的錯誤。
尋找最佳匹配(如果有的話)
函數(shù)匹配的第三步是從可行函數(shù)中選擇與本次調(diào)用最匹配的函數(shù)。在這一過程中,逐一檢查函數(shù)調(diào)用提供的實參,尋找形參類型與實參類型最匹配的那個可行函數(shù)。下一節(jié)將介紹“最匹配”的細節(jié),它的基本思想是,實參類型與形參類型越接近,它們匹配得越好。
在我們的例子中,調(diào)用只提供了一個(顯式的)實參,它的類型是double。如果調(diào)用f(int) ,實參將不得不從double轉(zhuǎn)換成int。另一個可行函數(shù)f(double,double)則與實參精確匹配。精確匹配比需要類型轉(zhuǎn)換的匹配更好,因此,編譯器把f(5.6)解析成對含有兩個double形參的函數(shù)的調(diào)用,并使用默認(rèn)值填補我們未提供的第二個實參。
含有多個形參的函數(shù)匹配
當(dāng)實參的數(shù)量有兩個或更多時,函數(shù)匹配就比較復(fù)雜了。對于前面那些名為f的函數(shù),我們來分析如下的調(diào)用會發(fā)生什么情況:
(42,2.56);選擇可行函數(shù)的方法和只有一個實參時一樣,編譯器選擇那些形參數(shù)量滿足要求且實參類型和形參類型能夠匹配的函數(shù)。此例中,可行函數(shù)包括 f(int,int)和 f(double,double)。
接下來,編譯器依次檢查每個實參以確定哪個函數(shù)是最佳匹配。如果有且只有一個函數(shù)滿足下列條件,則匹配成功:
-
該函數(shù)每個實參的匹配都不劣于其他可行函數(shù)需要的匹配。The match for each argument is no worse than the match required by any other viable function
-
至少有一個實參的匹配優(yōu)于其他可行函數(shù)提供的匹配。There is at least one argument for which the match is better than the match provided by any other viable function
如果在檢查了所有實參之后沒有任何一個函數(shù)脫穎而出,則該調(diào)用是錯誤的。編譯器將報告二義性調(diào)用的信息。
在上面的調(diào)用中,只考慮第一個實參時我們發(fā)現(xiàn)函數(shù)f(int,int)能精確匹配;要想匹配第二個函數(shù),int類型的實參必須轉(zhuǎn)換成double類型。顯然需要內(nèi)置類型轉(zhuǎn)換的匹配劣于精確匹配,因此僅就第一個實參來說,f(int, int)比 f(double,double)更好。
接著考慮第二個實參2.56,此時f(double,double)是精確匹配;要想調(diào)用f(int,int)必須將2.56從double類型轉(zhuǎn)換成int類型。因此僅就第二個實參來說,f(double,double)更好。
(MyNote:公說公有理婆說婆有理。)
編譯器最終將因為這個調(diào)用具有二義性而拒絕其請求:因為每個可行函數(shù)各自在一個實參上實現(xiàn)了更好的匹配,從整體上無法判斷孰優(yōu)孰劣。看起來我們似乎可以通過強制類型轉(zhuǎn)換其中的一個實參來實現(xiàn)函數(shù)的匹配,但是在設(shè)計良好的系統(tǒng)中,不應(yīng)該對實參進行強制類型轉(zhuǎn)換。
Best Practices:調(diào)用重載函數(shù)時應(yīng)盡量避免強制類型轉(zhuǎn)換。如果在實際應(yīng)用中確實需要強制類型轉(zhuǎn)換,則說明我們設(shè)計的形參集合不合理。
實參類型轉(zhuǎn)換
為了確定最佳匹配,編譯器將實參類型到形參類型的轉(zhuǎn)換劃分成幾個等級,具體排序如下所示:
- 實參類型和形參類型相同。
- 實參從數(shù)組類型或函數(shù)類型轉(zhuǎn)換成對應(yīng)的指針類型。
- 向?qū)崊⑻砑禹攲觕onst或者從實參中刪除頂層const。
(2~4項為第4章“類型轉(zhuǎn)換”內(nèi)容)
需要類型提升和算術(shù)類型轉(zhuǎn)換的匹配
WARNING:內(nèi)置類型的提升和轉(zhuǎn)換可能在函數(shù)匹配時產(chǎn)生意想不到的結(jié)果,但幸運的是,在設(shè)計良好的系統(tǒng)中函數(shù)很少會含有與下面例子類似的形參。
分析函數(shù)調(diào)用前,我們應(yīng)該知道小整型一般都會提升到int類型或更大的整數(shù)類型。
假設(shè)有兩個函數(shù),一個接受int、另一個接受short,則只有當(dāng)調(diào)用提供的是short類型的值時才會選擇short版本的函數(shù)。有時候,即使實參是一個很小的整數(shù)值,也會直接將它提升成int類型;此時使用short版本反而會導(dǎo)致類型轉(zhuǎn)換:
void ff(int); void ff(short); ff('a');// char提升成int;調(diào)用f(int)所有算術(shù)類型轉(zhuǎn)換的級別都一樣。例如,從int向unsigned int 的轉(zhuǎn)換并不比從int向double的轉(zhuǎn)換級別高。舉個具體點的例子,考慮
void manip(long); void manip(float); manip(3.14);//錯誤:二義性調(diào)用字面值3.14的類型是double,它既能轉(zhuǎn)換成long也能轉(zhuǎn)換成float。因為存在兩種可能的算數(shù)類型轉(zhuǎn)換,所以該調(diào)用具有二義性。
函數(shù)匹配和const實參
如果重載函數(shù)的區(qū)別在于它們的引用類型的形參是否引用了const,或者指針類型的形參是否指向const,則當(dāng)調(diào)用發(fā)生時編譯器通過實參是否是常量來決定選擇哪個函數(shù):
Record lookup (Account&); //函數(shù)的參數(shù)是Account的引用 Record lookup(const Account& ); //函數(shù)的參數(shù)是一個常量引用 const Account a; Account b; lookup(a); //調(diào)用lookup (const Account&) lookup(b); //調(diào)用lookup(Account&)在第一個調(diào)用中,我們傳入的是const對象a。因為不能把普通引用綁定到const對象上,所以此例中唯一可行的函數(shù)是以常量引用作為形參的那個函數(shù),并且調(diào)用該函數(shù)與實參a精確匹配。
在第二個調(diào)用中,我們傳入的是非常量對象b。對于這個調(diào)用來說,兩個函數(shù)都是可行的,因為我們既可以使用b初始化常量引用也可以用它初始化非常量引用。然而,用非常量對象初始化常量引用需要類型轉(zhuǎn)換,接受非常量形參的版本則與b精確匹配。因此,應(yīng)該選用非常量版本的函數(shù)。
指針類型的形參也類似。如果兩個函數(shù)的唯一區(qū)別是它的指針形參指向常量或非常量,則編譯器能通過實參是否是常量決定選用哪個函數(shù):
- 如果實參是指向常量的指針,調(diào)用形參是const*的函數(shù);
- 如果實參是指向非常量的指針,調(diào)用形參是普通指針的函數(shù)。
函數(shù)指針
函數(shù)指針指向的是函數(shù)而非對象。和其他指針一樣,函數(shù)指針指向某種特定類型。函數(shù)的類型由它的返回類型和形參類型共同決定,與函數(shù)名無關(guān)。例如:
//比較兩個string對象的長度 bool lengthCompare (const string &, const string &);該函數(shù)的類型是bool (const string&,const string&)。要想聲明一個可以指向該函數(shù)的指針,只需要用指針替換函數(shù)名即可:
//pf指向一個函數(shù),該函數(shù)的參數(shù)是兩個const string的引用,返回值是bool類型 bool (*pf)(const string &, const string &);//未初始化- 從我們聲明的名字開始觀察,pf前面有個*,因此pf是指針;
- 右側(cè)是形參列表,表示pf指向的是函數(shù);
- 再觀察左側(cè),發(fā)現(xiàn)函數(shù)的返回類型是布爾值。
因此,pf 就是一個指向函數(shù)的指針,其中該函數(shù)的參數(shù)是兩個const string 的引用,返回值是bool類型。
Note:*pf兩端的括號必不可少。如果不寫這對括號,則pf是一個返回值為bool指針的函數(shù):
//聲明一個名為 pf的函數(shù),該函數(shù)返回bool* bool *pf (const string &, const string &);使用函數(shù)指針
當(dāng)我們把函數(shù)名作為一個值使用時,該函數(shù)自動地轉(zhuǎn)換成指針。例如,按照如下形式我們可以將lengthCompare的地址賦給pf:
pf = lengthCompare;//pf指向名為lengthcompare的函數(shù) pf = &lengthCompare;//等價的賦值語句:取地址符是可選的此外,我們還能直接使用指向函數(shù)的指針調(diào)用該函數(shù),無須提前解引用指針:
bool b1 = pf("hello","goodbye" ) ; //調(diào)用lengthcompare函數(shù) bool b2 = (*pf)("hello","goodbye" );//一個等價的調(diào)用 bool b3 = lengthCompare("hello","goodbye");//另一個等價的調(diào)用在指向不同函數(shù)類型的指針間不存在轉(zhuǎn)換規(guī)則。但是和往常一樣,我們可以為函數(shù)指針賦一個nullptr或者值為0的整型常量表達式,表示該指針沒有指向任何一個函數(shù):
string::size_type sumLength(const string&, const string&); bool cstringCompare(const char*, const char* ) ; pf = 0 ; //正確:pf不指向任何函數(shù) pf = sumLength; //錯誤:返回類型不匹配 pf = cstringCompare; //錯誤:形參類型不匹配 pf = lengthCompare; //正確:函數(shù)和指針的類型精確匹配重載函數(shù)的指針
當(dāng)我們使用重載函數(shù)時,上下文必須清晰地界定到底應(yīng)該選用哪個函數(shù)。如果定義了指向重載函數(shù)的指針
void ff(int*); void ff(unsigned int) ; void (*pf1)(unsigned int) = ff; // pf1指向ff(unsigned)編譯器通過指針類型決定選用哪個函數(shù),指針類型必須與重載函數(shù)中的某一個精確匹配
void (*pf2)(int) = ff;//錯誤:沒有任何一個ff與該形參列表匹配* double (*pf3)(int*) = ff;//錯誤:ff和pf3的返回類型不匹配函數(shù)指針形參
和數(shù)組類似,雖然不能定義函數(shù)類型的形參,但是形參可以是指向函數(shù)的指針。此時,形參看起來是函數(shù)類型,實際上卻是當(dāng)成指針使用:
//第三個形參是函數(shù)類型,它會自動地轉(zhuǎn)換成指向函數(shù)的指針 void useBigger (const string &s1,const string &s2,bool pf (const string &, const string &)); //等價的聲明:顯式地將形參定義成指向函數(shù)的指針 void useBigger(const string &s1,const string &s2,bool (*pf)(const string &, const string &)) ;我們可以直接把函數(shù)作為實參使用,此時它會自動轉(zhuǎn)換成指針:
//自動將函數(shù)lengthcompare轉(zhuǎn)換成指向該函數(shù)的指針 useBigger(s1, s2, lengthCompare);正如useBigger的聲明語句所示,直接使用函數(shù)指針類型顯得冗長而煩瑣。類型別名和 decltype(第2章內(nèi)容)能讓我們簡化使用了函數(shù)指針的代碼:
// Func和Func2是函數(shù)類型 typedef bool Func(const string&, const string&); typedef decltype(lengthCompare) Func2; //等價的類型// FuncP和FuncP2是指向函數(shù)的指針 typedef bool (*FuncP)(const string&, const string&) ; typedef decltype(lengthCompare) *FuncP2;//等價的類型我們使用typedef定義自己的類型。Func和Func2是函數(shù)類型,而FuncP和 FuncP2是指針類型。需要注意的是,decltype返回函數(shù)類型,此時不會將函數(shù)類型自動轉(zhuǎn)換成指針類型。因為decltype的結(jié)果是函數(shù)類型,所以只有在結(jié)果前面加上*才能得到指針。可以使用如下的形式重新聲明useBigger:
// useBigger的等價聲明,其中使用了類型別名 void useBigger(const string&, const string&, Func); void useBigger(const string&, const string&, FuncP2);這兩個聲明語句聲明的是同一個函數(shù),在第一條語句中,編譯器自動地將Func表示的函數(shù)類型轉(zhuǎn)換成指針。
返回指向函數(shù)的指針
和數(shù)組類似,雖然不能返回一個函數(shù),但是能返回指向函數(shù)類型的指針。然而,我們必須把返回類型寫成指針形式,編譯器不會自動地將函數(shù)返回類型當(dāng)成對應(yīng)的指針類型處理。與往常一樣,要想聲明一個返回函數(shù)指針的函數(shù),最簡單的辦法是使用類型別名:
using F = int(int*, int); //F是函數(shù)類型,不是指針 using PF = int(*)(int*, int); //PF是指針類型其中我們使用類型別名將F定義成函數(shù)類型,將PF定義成指向函數(shù)類型的指針。必須時刻注意的是,和函數(shù)類型的形參不一樣,返回類型不會自動地轉(zhuǎn)換成指針。我們必須顯式地將返回類型指定為指針:
PF f1(int); //正確:PF是指向函數(shù)的指針,f1返回指向函數(shù)的指針 F f1(int); //錯誤:F是函數(shù)類型,f1不能返回一個函數(shù) F *f1(int); //正確:顯式地指定返回類型是指向函數(shù)的指針當(dāng)然,我們也能用下面的形式直接聲明f1:
int (*f1(int))(int*, int) ;按照由內(nèi)向外的順序閱讀這條聲明語句:
出于完整性的考慮,有必要提醒讀者我們還可以使用尾置返回類型的方式聲明一個返回函數(shù)指針的函數(shù):
auto f1(int) -> int(*) (int* , int) ;將auto和decltype用于函數(shù)指針類型
如果我們明確知道返回的函數(shù)是哪一個,就能使用decltype簡化書寫函數(shù)指針返回類型的過程。
例如假定有兩個函數(shù),它們的返回類型都是string::size_type,并且各有兩個const strings類型的形參,此時我們可以編寫第三個函數(shù),它接受一個string類型的參數(shù),返回一個指針,該指針指向前兩個函數(shù)中的一個:
string::size_type sumLength(const string&, const string&); string::size_type largerLength(const string&, const string&);//根據(jù)其形參的取值,getFcn函數(shù)返回指向sumLength或者largerLength的指針 decltype(sumLength) *getFcn (const string &);聲明getFcn唯一需要注意的地方是,牢記當(dāng)我們將decltype作用于某個函數(shù)時,它返回函數(shù)類型而非指針類型。因此,我們顯式地加上*以表明我們需要返回指針,而非函數(shù)本身。
總結(jié)
以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(6 / 19):函数的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何阅读论文?
- 下一篇: CNN的几种经典模型