第6章 函數
函數基礎(Function Basics)
典型的函數定義包括返回類型(return type)、函數名字、由0個或多個形式參數(parameter,簡稱形參)組成的列表和函數體(function body)。函數執行的操作在函數體中指明。
// 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 it while (val > 1) ret *= val--; // assign ret * val to ret and decrement val return ret; // return the result }
程序通過調用運算符(call operator)來執行函數。調用運算符的形式之一是一對圓括號()
,作用於一個表達式,該表達式是函數或者指向函數的指針;圓括號內是一個用逗號隔開的實際參數(argument,簡稱實參)列表,用來初始化函數形參。調用表達式的類型就是函數的返回類型。
int main() { int j = fact(5); // j equals 120, i.e., the result of fact(5) cout << "5! is " << j << endl; return 0; }
函數調用完成兩項工作:
- 用實參初始化對應的形參。
- 將控制權從主調函數轉移給被調函數。此時,主調函數(calling function)的執行被暫時中斷,被調函數(called function)開始執行。
return
語句結束函數的執行過程,完成兩項工作:
- 返回
return
語句中的值(可能沒有值)。 - 將控制權從被調函數轉移回主調函數,函數的返回值用於初始化調用表達式的結果。
實參是形參的初始值,兩者的順序和類型必須一一對應。
函數的形參列表可以爲空,但是不能省略。
void f1() { /* ... */ } // implicit void parameter list void f2(void) { /* ... */ } // explicit void parameter list
形參列表中的形參通常用逗號隔開,每個形參都是含有一個聲明符的聲明,即使兩個形參類型一樣,也必須把兩個類型聲明都寫出來。
int f3(int v1, v2) { /* ... */ } // error int f4(int v1, int v2) { /* ... */ } // ok
函數的任意兩個形參不能同名,函數最外層作用域中的局部變量也不能使用與函數形參一樣的名字。
形參的名字是可選的,但是無法使用未命名的形參。即使某個形參不被函數使用,也必須爲它提供一個實參。
函數的返回類型不能是數組類型或者函數類型,但可以是指向數組或函數的指針。
局部對象(Local Objects)
形參和函數體內定義的變量統稱爲局部變量(local variable)。
局部靜態對象(local static object)在程序的執行路徑第一次經過對象定義語句時初始化,並且直到程序結束才被銷燬,對象所在的函數結束執行並不會對它產生影響。在變量類型前添加關鍵字static
可以定義局部靜態對象。
如果局部靜態對象沒有顯式的初始值,它將執行值初始化。
函數聲明(Function Declarations)
和變量類似,函數只能定義一次,但可以聲明多次。函數聲明也叫做函數原型(function prototype)。
函數應該在頭文件中聲明,在源文件中定義。定義函數的源文件應該包含含有函數聲明的頭文件。
分離式編譯(Separate Compilation)
分離式編譯允許我們把程序按照邏輯關係分割到幾個文件中去,每個文件獨立編譯。這一過程通常會產生後綴名是*.obj或.o*的文件,該文件包含對象代碼(object code)。之後編譯器把對象文件鏈接(link)在一起形成可執行文件。
參數傳遞(Argument Passing)
形參初始化的機理與變量初始化一樣。
形參的類型決定了形參和實參交互的方式:
- 當形參是引用類型時,它對應的實參被引用傳遞(passed by reference),函數被傳引用調用(called by reference)。引用形參是它對應實參的別名。
- 當形參不是引用類型時,形參和實參是兩個相互獨立的對象,實參的值會被拷貝給形參(值傳遞,passed by value),函數被傳值調用(called by value)。
傳值參數(Passing Arguments by Value)
如果形參不是引用類型,則函數對形參做的所有操作都不會影響實參。
使用指針類型的形參可以訪問或修改函數外部的對象。
// 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 points ip = 0; // changes only the local copy of ip; the argument is unchanged }
如果想在函數體內訪問或修改函數外部的對象,建議使用引用形參代替指針形參。
傳引用參數(Passing Arguments by Reference)
通過使用引用形參,函數可以改變實參的值。
// 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 }
使用引用形參可以避免拷貝操作,拷貝大的類類型對象或容器對象比較低效。另外有的類類型(如IO類型)根本就不支持拷貝操作,這時只能通過引用形參訪問該類型的對象。
除了內置類型、函數對象和標準庫迭代器外,其他類型的參數建議以引用方式傳遞。
如果函數無須改變引用形參的值,最好將其聲明爲常量引用。
一個函數只能返回一個值,但利用引用形參可以使函數返回額外信息。
const形參和實參(const Parameters and Arguments)
當形參有頂層const
時,傳遞給它常量對象或非常量對象都是可以的。
可以使用非常量對象初始化一個底層const
形參,但是反過來不行。
把函數不會改變的形參定義成普通引用會極大地限制函數所能接受的實參類型,同時也會給別人一種誤導,即函數可以修改實參的值。
數組形參(Array Parameters)
因爲不能拷貝數組,所以無法以值傳遞的方式使用數組參數,但是可以把形參寫成類似數組的形式。
// 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)
因爲數組會被轉換成指針,所以當我們傳遞給函數一個數組時,實際上傳遞的是指向數組首元素的指針。
因爲數組是以指針的形式傳遞給函數的,所以一開始函數並不知道數組的確切尺寸,調用者應該爲此提供一些額外信息。
以數組作爲形參的函數必須確保使用數組時不會越界。
如果函數不需要對數組元素執行寫操作,應該把數組形參定義成指向常量的指針。
形參可以是數組的引用,但此時維度是形參類型的一部分,函數只能作用於指定大小的數組。
將多維數組傳遞給函數時,數組第二維(以及後面所有維度)的大小是數組類型的一部分,不能省略。
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
main:處理命令行選項(main:Handling Command-Line Options)
可以在命令行中向main
函數傳遞參數,形式如下:
int main(int argc, char *argv[]) { /*...*/ } int main(int argc, char **argv) { /*...*/ }
第二個形參argv是一個數組,數組元素是指向C風格字符串的指針;第一個形參argc表示數組中字符串的數量。
當實參傳遞給main
函數後,argv的第一個元素指向程序的名字或者一個空字符串,接下來的元素依次傳遞命令行提供的實參。最後一個指針之後的元素值保證爲0。
在Visual Studio中可以設置main
函數調試參數
含有可變形參的函數(Functions with Varying Parameters)
C++11新標準提供了兩種主要方法處理實參數量不定的函數。
-
如果實參類型相同,可以使用
initializer_list
標準庫類型。void error_msg(initializer_list<string> il) { for (auto beg = il.begin(); beg != il.end(); ++beg) cout << *beg << " " ; cout << endl; }
-
如果實參類型不同,可以定義可變參數模板。
C++還可以使用省略符形參傳遞可變數量的實參,但這種功能一般只用在與C函數交換的接口程序中。
initializer_list
是一種標準庫類型,定義在頭文件initializer_list中,表示某種特定類型的值的數組。
initializer_list
提供的操作:
拷貝或賦值一個initializer_list
對象不會拷貝列表中的元素。拷貝後,原始列表和副本共享元素。
initializer_list
對象中的元素永遠是常量值。
如果想向initializer_list
形參傳遞一個值的序列,則必須把序列放在一對花括號內。
if (expected != actual) error_msg(ErrCode(42), {"functionX", expected, actual}); else error_msg(ErrCode(0), {"functionX", "okay"});
因爲initializer_list
包含begin
和end
成員,所以可以使用範圍for
循環處理其中的元素。
省略符形參是爲了便於C++程序訪問某些特殊的C代碼而設置的,這些代碼使用了名爲varargs
的C標準庫功能。通常,省略符形參不應該用於其他目的。
省略符形參應該僅僅用於C和C++通用的類型,大多數類類型的對象在傳遞給省略符形參時都無法正確拷貝。
返回類型和return語句(Return Types and the return Statement)
return
語句有兩種形式,作用是終止當前正在執行的函數並返回到調用該函數的地方。
return; return expression;
無返回值函數(Functions with No Return Value)
沒有返回值的return
語句只能用在返回類型是void
的函數中。返回void
的函數可以省略return
語句,因爲在這類函數的最後一條語句後面會隱式地執行return
。
通常情況下,如果void
函數想在其中間位置提前退出,可以使用return
語句。
一個返回類型是void
的函數也能使用return
語句的第二種形式,不過此時return
語句的expression必須是另一個返回void
的函數。
強行令void
函數返回其他類型的表達式將產生編譯錯誤。
有返回值函數(Functions That Return a Value)
return
語句的第二種形式提供了函數的結果。只要函數的返回類型不是void
,該函數內的每條return
語句就必須返回一個值,並且返回值的類型必須與函數的返回類型相同,或者能隱式地轉換成函數的返回類型(main
函數例外)。
在含有return
語句的循環後面應該也有一條return
語句,否則程序就是錯誤的,但很多編譯器無法發現此錯誤。
函數返回一個值的方式和初始化一個變量或形參的方式完全一樣:返回的值用於初始化調用點的一個臨時量,該臨時量就是函數調用的結果。
如果函數返回引用類型,則該引用僅僅是它所引用對象的一個別名。
函數不應該返回局部對象的指針或引用,因爲一旦函數完成,局部對象將被釋放。
// disaster: this function returns a reference to a local object const string &manip() { string ret; // transform ret in some way if (!ret.empty()) return ret; // WRONG: returning a reference to a local object! else return "Empty"; // WRONG: "Empty" is a local temporary string }
如果函數返回指針、引用或類的對象,則可以使用函數調用的結果訪問結果對象的成員。
調用一個返回引用的函數會得到左值,其他返回類型得到右值。
C++11規定,函數可以返回用花括號包圍的值的列表。同其他返回類型一樣,列表也用於初始化表示函數調用結果的臨時量。如果列表爲空,臨時量執行值初始化;否則返回的值由函數的返回類型決定。
-
如果函數返回內置類型,則列表內最多包含一個值,且該值所佔空間不應該大於目標類型的空間。
-
如果函數返回類類型,由類本身定義初始值如何使用。
vector<string> process() { // . . . // expected and actual are strings if (expected.empty()) return {}; // return an empty vector else if (expected == actual) return {"functionX", "okay"}; // return list-initialized vector else return {"functionX", expected, actual}; }
main
函數可以沒有return
語句直接結束。如果控制流到達了main
函數的結尾處並且沒有return
語句,編譯器會隱式地插入一條返回0的return
語句。
main
函數的返回值可以看作是狀態指示器。返回0表示執行成功,返回其他值表示執行失敗,其中非0值的具體含義依機器而定。
爲了使main
函數的返回值與機器無關,頭文件cstdlib定義了EXIT_SUCCESS
和EXIT_FAILURE
這兩個預處理變量,分別表示執行成功和失敗。
int main() { if (some_failure) return EXIT_FAILURE; // defined in cstdlib else return EXIT_SUCCESS; // defined in cstdlib }
建議使用預處理變量EXIT_SUCCESS
和EXIT_FAILURE
表示main
函數的執行結果。
如果一個函數調用了它自身,不管這種調用是直接的還是間接的,都稱該函數爲遞歸函數(recursive function)。
// calculate val!, which is 1 * 2 * 3 . . . * val int factorial(int val) { if (val > 1) return factorial(val-1) * val; return 1; }
在遞歸函數中,一定有某條路徑是不包含遞歸調用的,否則函數會一直遞歸下去,直到程序棧空間耗盡爲止。
相對於循環迭代,遞歸的效率較低。但在某些情況下使用遞歸可以增加代碼的可讀性。循環迭代適合處理線性問題(如鏈表,每個節點有唯一前驅、唯一後繼),而遞歸適合處理非線性問題(如樹,每個節點的前驅、後繼不唯一)。
main
函數不能調用它自身。
返回數組指針(Returning a Pointer to an Array)
因爲數組不能被拷貝,所以函數不能返回數組,但可以返回數組的指針或引用。
返回數組指針的函數形式如下:
Type (*function(parameter_list))[dimension]
其中Type表示元素類型,dimension表示數組大小,*(*function (parameter_list))*兩端的括號必須存在。
C++11允許使用尾置返回類型(trailing return type)簡化複雜函數聲明。尾置返回類型跟在形參列表後面,並以一個->
符號開頭。爲了表示函數真正的返回類型在形參列表之後,需要在本應出現返回類型的地方添加auto
關鍵字。
// fcn takes an int argument and returns a pointer to an array of ten ints auto func(int i) -> int(*)[10];
任何函數的定義都能使用尾置返回類型,但是這種形式更適用於返回類型比較複雜的函數。
如果我們知道函數返回的指針將指向哪個數組,就可以使用decltype
關鍵字聲明返回類型。但decltype
並不會把數組類型轉換成指針類型,所以還要在函數聲明中添加一個*
符號。
int odd[] = {1,3,5,7,9}; int even[] = {0,2,4,6,8}; // returns a pointer to an array of five int elements decltype(odd) *arrPtr(int i) { return (i % 2) ? &odd : &even; // returns a pointer to the array }
函數重載(Overloaded Functions)
同一作用域內的幾個名字相同但形參列表不同的函數叫做重載函數。
main
函數不能重載。
不允許兩個函數除了返回類型以外的其他所有要素都相同。
頂層const
不影響傳入函數的對象,一個擁有頂層const
的形參無法和另一個沒有頂層const
的形參區分開來。
Record lookup(Phone); Record lookup(const Phone); // redeclares Record lookup(Phone) Record lookup(Phone*); Record lookup(Phone* const); // redeclares Record lookup(Phone*)
如果形參是某種類型的指針或引用,則通過區分其指向的對象是常量還是非常量可以實現函數重載,此時的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
const_cast
可以用於函數的重載。當函數的實參不是常量時,將得到普通引用。
// 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; } string &shorterString(string &s1, string &s2) { auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); return const_cast<string&>(r); }
函數匹配(function matching)也叫做重載確定(overload resolution),是指編譯器將函數調用與一組重載函數中的某一個進行關聯的過程。
調用重載函數時有三種可能的結果:
- 編譯器找到一個與實參最佳匹配(best match)的函數,並生成調用該函數的代碼。
- 編譯器找不到任何一個函數與實參匹配,發出無匹配(no match)的錯誤信息。
- 有一個以上的函數與實參匹配,但每一個都不是明顯的最佳選擇,此時編譯器發出二義性調用(ambiguous call)的錯誤信息。
重載與作用域(Overloading and Scope)
在不同的作用域中無法重載函數名。一旦在當前作用域內找到了所需的名字,編譯器就會忽略掉外層作用域中的同名實體。
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 read string s = read(); // error: read is a bool variable, not a function // bad practice: usually it's a bad idea to declare functions at local scope void print(int); // new scope: hides previous instances of print print("Value: "); // error: print(const string &) is hidden print(ival); // ok: print(int) is visible print(3.14); // ok: calls print(int); print(double) is hidden }
在C++中,名字查找發生在類型檢查之前。
特殊用途語言特性(Features for Specialized Uses)
默認實參(Default Arguments)
默認實參作爲形參的初始值出現在形參列表中。可以爲一個或多個形參定義默認值,不過一旦某個形參被賦予了默認值,它後面的所有形參都必須有默認值。
typedef string::size_type sz; string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
調用含有默認實參的函數時,可以包含該實參,也可以省略該實參。
如果想使用默認實參,只要在調用函數的時候省略該實參即可。
雖然多次聲明同一個函數是合法的,但是在給定的作用域中一個形參只能被賦予一次默認實參。函數的後續聲明只能爲之前那些沒有默認值的形參添加默認實參,而且該形參右側的所有形參必須都有默認值。
// no default for the height or width parameters string screen(sz, sz, char = ' '); string screen(sz, sz, char = '*'); // error: redeclaration string screen(sz = 24, sz = 80, char); // ok: adds default
默認實參只能出現在函數聲明和定義其中一處。通常應該在函數聲明中指定默認實參,並將聲明放在合適的頭文件中。
// 函數聲明 void fun(int n); int main() { // Error: 編譯器向前查找函數聲明 // fun調用形式與聲明不符 fun(); return EXIT_SUCCESS; } // 函數定義 void fun(int n = 0) { /*...*/ }
局部變量不能作爲函數的默認實參。
用作默認實參的名字在函數聲明所在的作用域內解析,但名字的求值過程發生在函數調用時。
// 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, ' ') void f2() { def = '*'; // changes the value of a default argument sz wd = 100; // hides the outer definition of wd but does not change the default window = screen(); // calls screen(ht(), 80, '*') }
內聯函數和constexpr函數(Inline and constexpr Functions)
內聯函數會在每個調用點上“內聯地”展開,省去函數調用所需的一系列工作。定義內聯函數時需要在函數的返回類型前添加關鍵字inline
。
// inline version: find the shorter of two strings inline const string &horterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; }
在函數聲明和定義中都能使用關鍵字inline
,但是建議只在函數定義時使用。
一般來說,內聯機制適用於優化規模較小、流程直接、調用頻繁的函數。內聯函數中不允許有循環語句和switch
語句,否則函數會被編譯爲普通函數。
constexpr
函數是指能用於常量表達式的函數。constexpr
函數的返回類型及所有形參的類型都得是字面值類型。另外C++11標準要求constexpr
函數體中必須有且只有一條return
語句,但是此限制在C++14標準中被刪除。
constexpr int new_sz() { return 42; } constexpr int foo = new_sz(); // ok: foo is a constant expression
constexpr
函數的返回值可以不是一個常量。
// scale(arg) is a constant expression if arg is a constant expression constexpr size_t scale(size_t cnt) { return new_sz() * cnt; } int arr[scale(2)]; // ok: scale(2) is a constant expression int i = 2; // i is not a constant expression int a2[scale(i)]; // error: scale(i) is not a constant expression
constexpr
函數被隱式地指定爲內聯函數。
和其他函數不同,內聯函數和constexpr
函數可以在程序中多次定義。因爲在編譯過程中,編譯器需要函數的定義來隨時展開函數。對於某個給定的內聯函數或constexpr
函數,它的多個定義必須完全一致。因此內聯函數和constexpr
函數通常定義在頭文件中。
調試幫助(Aids for Debugging)
變量名稱 | 內容 |
---|---|
__func__ |
當前函數名稱 |
__FILE__ |
當前文件名稱 |
__LINE__ |
當前行號 |
__TIME__ |
文件編譯時間 |
__DATE__ |
文件編譯日期 |
函數匹配(Function Matching)
函數實參類型與形參類型越接近,它們匹配得越好。
重載函數集中的函數稱爲候選函數(candidate function)。
可行函數(viable function)的形參數量與函數調用所提供的實參數量相等,並且每個實參的類型與對應的形參類型相同,或者能轉換成形參的類型。
調用重載函數時應該儘量避免強制類型轉換。
實參類型轉換(Argument Type Conversions)
所有算術類型轉換的級別都一樣。
如果載函數的區別在於它們的引用或指針類型的形參是否含有底層const
,則調用發生時編譯器通過實參是否是常量來決定函數的版本。
Record lookup(Account&); // function that takes a reference to Account Record lookup(const Account&); // new function that takes a const reference const Account a; Account b; lookup(a); // calls lookup(const Account&) lookup(b); // calls lookup(Account&)
函數指針(Pointers to Functions)
要想聲明一個可以指向某種函數的指針,只需要用指針替換函數名稱即可。
// compares lengths of two strings bool lengthCompare(const string &, const string &); // pf points to a function returning bool that takes two const string references bool (*pf)(const string &, const string &); // uninitialized
可以直接使用指向函數的指針來調用函數,無須提前解引用指針。
pf = lengthCompare; // pf now points to the function named lengthCompare pf = &lengthCompare; // equivalent assignment: address-of operator is optional bool b1 = pf("hello", "goodbye"); // calls lengthCompare bool b2 = (*pf)("hello", "goodbye"); // equivalent call bool b3 = lengthCompare("hello", "goodbye"); // equivalent call
對於重載函數,編譯器通過指針類型決定函數版本,指針類型必須與重載函數中的某一個精確匹配。
void ff(int*); void ff(unsigned int); void (*pf1)(unsigned int) = ff; // pf1 points to ff(unsigned)
可以把函數的形參定義成指向函數的指針。調用時允許直接把函數名當作實參使用,它會自動轉換成指針。
// third parameter is a function type and is automatically treated as a pointer to function void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); // equivalent declaration: explicitly define the parameter as a pointer to function void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &)); // automatically converts the function lengthCompare to a pointer to function useBigger(s1, s2, lengthCompare);
關鍵字decltype
作用於函數時,返回的是函數類型,而不是函數指針類型。
函數可以返回指向函數的指針。但返回類型不會像函數類型的形參一樣自動地轉換成指針,必須顯式地將其指定爲指針類型。