Argument(引數):用來初始化函數之參數的initializers。
舉例:
fact
函數的定義:
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
}
在此函數中,val
為參數。若我們在main()
裡面呼叫fact
函數的時候:
int main()
{
int j = fact(5); // j equals 120, i.e., the result of fact(5)
cout << "5! is " << j << endl;
return 0;
}
此時括號中的5即為引數。儘管引數的擺放有順序,但是計算引數時卻未必依照該順序逐一計算,compiler可以任意挑選哪個引數先被計算。引數的型別限制與initializer的型別規則試一致的,我們必須傳入與函數參數相同數目的引數,這保證了參數在函數被呼叫的時候必定會被初始化。
例(延續上方的fact
函數):
fact("hello"); // error: wrong argument type
fact(); // error: too few arguments
fact(42, 10, 0); // error: too many arguments
fact(3.14); // ok: argument is converted to int (且為implicitly,經過修剪)
故: fact(3.14)同義於fact(3)。
函數可以沒參數,但定義時一定要有括號:
void f1(){ /* ... */ } // implicit void parameter list
為了與C語言相容,也可以用下面的語法:
void f2(void){ /* ... */ } // explicit void parameter list
就算兩個參數型別一樣,仍必須把兩個型別都寫出來:
int f3(int v1, v2) { /* ... */ } // error
int f4(int v1, int v2) { /* ... */ } // ok
函數可以不回傳值(使用void
),但不能回傳一個陣列型別或者函數型別,後面的章節會提到如何定義回傳(1)指向陣列的pointer(或是reference)以及(2)指向函數的指標方式(6.7)。
在C++裡面,名稱有作用域(scope),而物件則有生命週期(lifetime)。
(1)名稱的作用域是指該名稱在程式中可見(visible)的部分。
(2)物件的生命週期是指在程式執行過程中,該物件存在的時間。
函數的block形成一個新的作用域,而函數的:
(1)參數
(2)定義在函數體裡面的變數
我們稱作局部變數(local variables)。它們只在函數的內部可見,而且會隱藏外層作用域中擁有相同名稱的聲明(They are “local” to that function and hide declarations of the same name made in an outer scope.)。
定義於所有函數外部的物件存在於整個程式執行的過程中,而局部變數的生命週期取決於它被定義的方式。
對應於一般的局部變數所對應的物件來說,它們被創建於當函數的控制路徑(control path)通過該變數的定義時,並且在通過該變數被定義的block末端被銷毀。這種只存在於一個block之中的物件稱為automatic objects,當該block執行結束後,裡面定義之automatic objects的值就會變成未定義的。而參數是一種automatic objects。
函數體中,作為局部變數的automatic objects如果在被定義時若沒有initializer,則會被默認初始化,這代表未被初始化的內建型別會有未定義的值(若在函數體外則會被初始化成0,見2.2.1)。
當我們用static
定義某個局部變數時,可以使它的生命週期超越函數被呼叫的時候,Local static
Objects在該變數的定義被第一次執行之前就被初始化了,並且直到程式終止才被銷毀。
Example
size_t count_calls()
{
static size_t ctr = 0; // value will persist across calls
return ++ctr;
}
int main()
{
for (size_t i = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}
上述程式輸出結果為:
1
2
3
4
5
6
7
8
9
10
如果一個局部靜態變數沒被explicit initialized,則它會被value initialized(3.3.1),也就是說內建類型的type會被初始化成0。
如同其他的名稱一樣,函數的名子必須在使用前就被聲明。如同變數一樣,一個函數只能被定義一次但可以被多次聲明(例外:見15.3)。函數的聲明與定義的差別在於函數聲明沒有函數體,而是用;
取代之,因此參數不需要有名子,可以被忽略,但為了讓使用者知道函數的功能也可以加上名子。
Example
// parameter names chosen to indicate that the iterators denote a range of values to print
void print(vector<int>::const_iterator beg,
vector<int>::const_iterator end);
上面的beg
與end
都可以被省略。
函數聲明的三個元素:(1)回傳型別(2)函數名稱(3)參數型別
這些元素描述了函數的interface(接口),函數的聲明又被稱作function prototype(函數原型)。
建議:將函數於header file中宣告,並把該header file include到要用的source file中,並於source file中定義。
優點:
(1)確保同一函數的聲明保持一致。
(2)當函數的interface要改動時只需改變header file中的聲明即可。
參數的初始化與變數的初始化運作方式是相同的。
根據參數的類型可將引數被傳遞的方式分為下面兩種:
(1) passed by value
參數不是reference,則引數的值會被複製到參數,至此之後,參數與引數為互相獨立的物件,我們又稱此函數被called by value。
(2) passed by reference
參數為reference,則參數會被綁定到引數上(與一般的reference一樣),我們又稱此函數被called by reference。
改變參數的值不會影響到傳入該函數的引數。
跟其他非reference的值一樣,當我們將指標當成參數,會把引數指標的值複製到參數中,不過透過指標我們可以indirect access它指向的物件(與引數指向的物件一樣)。
Example
// 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
}
Best Practice
習慣C語言程式設計的程式設計師常使用point parameters參訪(access)參數外的變數,而在C++中,程式設計師則通常使用reference parameters。
對參數的操作同時就是對參照的引數進行操作。
複製一個很大的class type或是containers是沒有效率的一件事,甚至有些class type(包括IO型別)根本就不能被複製,這時候我們就會用reference形式的參數來操作這些對象,避免複製的發生。
Example 當我們想比較兩個可能很長的字串的長度,我們想避免複製,因此我們將使用reference類型的參數。由於比較長度並不需要改變字串的內容,因此我們將參數定義成對
const
的reference:
// compare the length of two strings
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
Best Practice
如果沒要在函數中改變reference parameters的值,則應該將其reference toconst
。
函數雖然只能回傳一個值,但透過reference parameters我們可以傳出多個值。
Example
想要知道某個字元在字串第一次出現的(1)位置(2)出現次數,我們可以定義出一個新的型別來包含這兩種資訊,然而一個更簡單的作法可以透過reference parameters來達成:
// returns the index of the first occurrence of c in s
// the reference parameter occurs counts how often c occurs
string::size_type find_char(const string &s, char c,
string::size_type &occurs)
{
auto ret = s.size(); // position of the first occurrence, if any
occurs = 0; // set the occurrence count parameter
for (decltype(ret) i = 0; i != s.size(); ++i) {
if (s[i] == c) {
if (ret == s.size())
ret = i; // remember the first occurrence of c
++occurs; // increment the occurrence count
}
}
return ret; // count is returned implicitly in occurs
}
當s
為字串,ctr
為一個size_type
(見3.2.2)的物件,我們呼叫find_char
如下:
auto index = find_char(s, 'o', ctr);
最後我們可以獲得o
字元出現的第一個位置於index
中,出現次數則呈現於ctr
之中。
在2.4.3的討論中我們知道const
物件的複製是忽略top-level const的,所以當我們用複製引數的方式來初始化參數的時候,top-level const
是會被忽略的。也就是說參數的top-level const
不會被考慮進去,所以我們可以把const
或是nonconst
物件傳給一個有top-level const
的參數中,例如:
Int i = 5;
void fcn(const int i) { /* fcn can read but not write to i */ }
也因此這造成了以下的結果:
void fcn(const int i) { /* fcn can read but not write to i */ }
void fcn(int i) { /* . . . */ } // error: redefines fcn(int)
C++裡面雖然允許函數的重載,然而上面兩個可載入的引數型別是完全相同的(因為參數i的top-level const會被忽略),如果上面程式碼合法會造成不知道到底要傳入的函數是哪一個,因此被視作違法。
參數被初始化的方式與變數初始化的規則一樣,特別要記住我們可以用一個nonconst
的物件初始化一個有low-level const
的參數,反過來則不行。
Example
// 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
}
// 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
}
當我們用reference的方式傳遞參數,應盡可能的使用reference to const
的方式,這是因為如果用一般的reference會有下列的缺點:
(1) 使得該函數的呼叫者(caller)以為它會改變引數的值
(2) 可以傳入的引數類型被限縮(因為const不能轉成nonconst)
(3) 以上的負面效應甚至可能會被放大
Example
//函數A的程式碼如下(使用reference to const傳遞引數)
int A(const string &s1){
//……
return B(s1,i);
}
//如果B的定義如下(未使用reference to const傳遞引數)
int B(string &s2, int a){
//……
}
以上的程式碼會在編譯時就發生錯誤,這是因為s1
本身是一個reference to aconst string
,然而s2只允許非const
的string
傳入,因此發生錯誤,雖然看起來我們可以把A
的定義改成int A(string &s1)
就不會發生編譯錯誤,然而這麼做卻只是把這種設計不良的錯誤給擴大而已。
由於array不能被copy(見3.5.1),而且經常被轉換成pointer(見3.5.3),當我們把array傳入函式時,其實傳入的是一個指標。雖然我們不能把array傳入函式,我們仍可以把參數寫成看起來像array的樣子:
//以下三種宣告表面上看起來不同,實際上卻是等價的
//每種函式都有一個型別為const int*的參數
void print(const int*);
void print(const int[]); // 表示該函數需要一個array
void print(const int[10]); // 只有說明用途,但是編譯器"不會"檢查傳入的陣列大小
當我們呼叫print
時,編譯器只會檢查引數的型別是否為const int*
,也就是說當我們把array傳入print
時,它會自動被轉成指向該array第一個元素的pointer,與該array的大小無關。
Warning 就如同任何使用到array的程式碼,我們須注意把array當參數的函式中,使用該參數時是否有超過array的邊界。
由於array是被當成pointer傳入函式的,所以函式該array的大小,必須由caller提供額外資訊,以下為三種常用的方法:
(1) Using a Marker to Specify the Extent of an Array
例:
C-style的字串最後一個字元一定都是空字元(null character),因此我們可以用它來處理字串。
Example
void print(const char *cp){
if (cp) // if cp is not a null pointer
while (*cp) // so long as the character it point to is not a null character
cout << *cp++; // print the character and advance the pointer
}
(2) Using the Standard Library Conventions
可以利用類似標準函式庫中的iterator,將指向第一個與最後一個元素的pointer傳入函數中
Example
void print(const int *beg, const int *end){
// print every element starting at beg up to but not including end
while (beg != end)
cout << *beg++ << endl; // print the current element
// and advance the pointer
}
當我們要呼叫該函數時,可以利用begin
與end
函數(見3.5.3)來提供這兩個pointer:
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, see § 3.5.3
(3) Explicitly Passing a Size Parameter
此法常用在C語言與叫老的C++程式中,第二個參數指出了該陣列的大小:
// 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;
}
}
我們也能用reference to array(見3.5.1)的方式定義參數:
// ok: param eter 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
的括號是必須的(見3.5.1):
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
雖然這種傳遞方式比較安全,但array的大小被限制了,降低了實用性:
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.1.1中會學到該如何傳入reference to任意大小的array
在C++裡,多維陣列其實就是array of arrays,就如同之前的array一樣,傳入函式的其實是一個指向該array第一個元素的pointer,且為指向一個array的pointer,而第二層(以及之後的層)的array的size是該array的元素型別的一部分,所以必須被標明。
例如說如果我們要傳入一個二維陣列,可以這樣寫:
// 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的int
arrays的pointer。我們也可以使用array的語法來定義(且與上面的宣告等價),跟上面一樣,編譯器會忽略第一個維度的大小,所以最好忽略它:
void print(int matrix[][10], int rowSize) { /* . . . */ }
Note
包住*matrix的括號是必要的:
int *matrix[10]; // array of ten pointers
int (*matrix)[10]; // pointer to an array of ten ints
main
即為一個將陣列傳入函式的例子,之前所定義的main
函式的parameter list都是空的:
int main() { … }
然而有時候我們需要把引數傳入main
裡面,這些引數常被用來給使用者標示想要該程式執行的operation,假設main
程式存在於名為prog
的執行檔中,則我們可以用下面的方式傳遞option給該程式:
Prog –d –o ofile data 0
(在linux中則可在執行檔所在目錄中輸入./ Prog –d –o ofile data 0
)
上面的command-line option會被傳入兩個(optional)參數:
int main(int argc, char *argv[]) { … }
第二個參數是一組指向C-style character string的pointer所成之陣列,第一個argc
參數則傳遞了該陣列的字串數目,我們也可以用另一種方式定義main
:
int main(int argc, char **argv) { … } //標明argv指向一個char*
當引數被傳進main
,argv
中第一個元素指向的會是該程式的名稱或者空字串(而非user input),接下來的元素則會傳遞我們在command line提供的引數,而最後一個pointer的下一個元素則必為0
(Null pointer)。
Example
若prog.c的程式碼如下:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]){
printf("the value of argc is %d\n",argc);
int count = 0;
while(count < argc){
printf("argv [%d] = %s\n",count,argv[count]);
count ++;
}
return 0;
}
則將其編譯成執行檔後在該目錄command line輸入:
/.prog –d –o ofile data0
所得到的輸出為:
the value of argc is 5
argv [0] = ./prog
argv [1] = -d
argv [2] = -o
argv [3] = ofile
argv [4] = data0
當引數的數量是未知時,新標準有兩種主要方法來傳遞數量變動的引數:
(1) initializer_list
Parameters
適用於未知數量的引數型別都是相同的情況,定義於initializer_list
標頭檔中,其用法和vector
類似,當我們要定義一個initializer_list
時,必須標明它元素的型別:
initializer_list<string> ls; // initializer_list of strings
initializer_list<int> li; // initializer_list of ints
與vector
不同的是initializer_list
的元素永遠都是常量,initializer_list
也有自己的begin
與end
iterators,功能如之前所述
Example
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); ++beg)
cout << *beg << " " ;
cout << endl;
}
當我們傳遞一連串值到initializer_list
參數時,必須要包在花括號裡:
// expected, actual are strings
if (expected != actual)
error_msg({"functionX", expected, actual});
else
error_msg({"functionX", "okay"});
(2) variadic template
適用於當未知數量的引數之型別不一致時,見16.4。
return
語句使執行中的函數終止並將控制權歸還到該函數被呼叫的地方,其形式有兩種:
return
return expression;
不包含回傳值的return
只會被用於回傳型別為void
的函式,並且void
函式並不要求一定要有return
,有一個implicit的return
會放在該函式的最後一條語句之後。通常void
函式會使用return
來中途離開該函數,用法類似break
語句。例如當我們想交換兩物件時,當兩個物件相等時,可用return
來直接跳過交換的部分:
void swap(int &v1, int &v2)
{
// if the values are already the same, no need to swap, just return
if (v1 == v2)
return;
// if we're here, there's work to do
int tmp = v2;
v2 = v1;
v1 = tmp;
// no explicit return necessary
}
void
函式也可以用到return expression
這種形式,然而它只能回傳某個也是回傳void
的函式,其他不是void
函式的expression會導致編譯錯誤。
C++不能確保回傳結果的正確性(correctness),但它能保證每個return
會有適當的型別:
// incorrect return values, this code will not compile
bool str_subrange(const string &str1, const string &str2)
{
// same sizes: return normal equality test
if (str1.size() == str2.size())
return str1 == str2; // ok: == returns bool
// find the size of the smaller string
auto size = (str1.size() < str2.size())
? str1.size() : str2.size();
// look at each element up to the size of the smaller string
for (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
}
上面的error #2如果沒被編譯器偵測出來,則在執行時會發生未定義的情況。
傳回值的方式就跟變數與參數被初始化時一樣:回傳值在函式呼叫處被用來初始化一個暫時量,該暫時量即為函式回傳的結果。
Example
// return the plural version of word if ctr is greater than 1
string make_plural(size_t ctr, const string &word,
const string &ending)
{
return (ctr > 1) ? word + ending : word;
}
就跟其他reference一樣,如果函式回傳的是一個reference,則回傳的就是被refer的物件的另一個name而已。
Example
// 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;
}
上例中,參數以及回傳值的型別都是reference to const string,因此這些string
在函數被呼叫以及回傳結果時都不會經過copy。
當一個函式完成(complete)之後,其記憶體空間也被釋放,所以當函式終止後,指向其local objects的pointer或reference都會失效:
//災難: 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
}
同樣地,回傳指向該函式local objects的pointer也是不對的。
call operator跟dot(.
)與arrow operator(->
)(見4.1.2)有相同的優先權,而且它們全都是左結合。也就是說如果某函式回傳一個物件型別的reference或pointer的話,就能用該回傳結果去呼叫該物件的成員。
Example
// call the size member of the string returned by shorterString
auto sz = shorterString(s1, s2).size(); //sz得到s1或s2的size
對於回傳reference的函式進行呼叫會得到一個左值,故我們可以對其賦值(如果不是const
reference的話),其他的回傳型別則得到右值。
Example
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 value
get_val(s, 0) = 'A'; // changes s[0] to A
cout << s << endl; // prints A value
return 0;
}
array無法被copy,故函式無法回傳一個array,但我們可以回傳一個指向array的指針或是array的reference,其函式宣告語法與變數宣告類似,只是函式的宣告多了一個緊跟在函式名稱之後的parameters list,如下:
Type (*function(parameter_list))[dimension]
Example
假設func
函式有一個int
參數,並回傳一個指向大小為10
的int
array的pointer則根據上面的語法其宣告如下:
int (*func(int i))[10];
這種宣告方式略為複雜,因此我們可以利用下列幾種方式來簡化:
(1) 使用type alias(見2.5.1)
Example
typedef int arrT[10]; // arrT is a synonym for the type array of ten ints
using arrtT = int[10]; // equivalent declaration of arrT; see § 2.5.1
arrT* func(int i); // func returns a pointer to an array of five ints
(2) 使用Trailing Return Type
trailing return可被定義於任何的函式之中,但在處理回傳複雜型別的函式中最有用,trailing return type跟在parameter list接上->
之後,並且將auto
放在原本return type該出現的地方:
// fcn takes an int argument and returns a pointer to an array of ten ints
auto func(int i) -> int(*)[10];
(3) Using 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
}
Note
main
函式無法被重載
函數允許回傳型別的不同,但是如果僅僅是回傳型別不同也會引發錯誤。
Record lookup(const Account&);
int lookup(const int&); //ok
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 ignored
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); // Telno and Phone are the same type
在6.2.3有提到top-level const
不會影響到物件是否能傳入函式中,也就是擁有top-level const
的參數和沒有top-level const
的參數其實無法辨別:
Record lookup(Phone);
Record lookup(const Phone); // redeclares Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone* const); // redeclares Record lookup(Phone*)
另一方面,low-level const
的有無是可以重載成兩個函式的:
//使用const and nonconst references 或 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
雖然看起來第二個以及第四個被宣告的函數可以藉由轉換把nonconst
的參數傳入,然而在6.6.1會提到,編譯器偏好nonconst
的函式版本,也就是第一個及第三個。
在4.11.3中有提到const_cast
的功能,它在函數重載的部分很有用:
Example
假設我們已經寫好了以下的函式:
// 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;
}
上面的函式輸入與回傳的是對const string
的reference,如果用兩個對nonconst string
的reference呼叫該函式,會得到一個對const string
的reference,但如果今天我們想得到的是一個對string
的reference (沒有const
),我們可以利用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);
}
首先我們把參數變成reference to const string
,接著傳入原來版本的函式中,得到reference to const string
的結果後再轉回reference to string
。
如果我們在inner scope宣告一個name,則該name會隱藏在outer scope宣告之相同的name,也就是說name並不能跨越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: 隱藏先前所宣告的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
}
如果我們想讓參數有自己的默認引數,我們可以將它如一般的變數宣告成default arguments:
typedef string::size_type sz; // typedef see § 2.5.1 (p. 67)
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
必須注意如果某個參數有default arguments,則之後所有的參數都必須有default arguments。
當我們要使用default arguments時,只要在呼叫函式的時候忽略輸入該引數即可。
Example
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,'#')
默認引數只能負責填補右後端缺少的引數:
window = screen(, , '?'); // error: can omit only trailing arguments
window = screen('?'); /* 編譯成功:但calls screen('?',80,' ')(因為?被轉換成63並初始化到ht中)*/
函數雖然能被多次宣告,然而default arguments只能被宣告一次,之前已被宣告過的在下一次宣告就不能再次宣告default arguments了:
// 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
local variables不能作為默認引數,除此之外只要能透過轉換任何expression都能當成默認引數:
// 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, '*')
}
當我們呼叫f2
時,def
的值被改成*
,造成我們呼叫screen時其默認引數的值也被改變。
在6.3.2中我們定義了這個函式:
const string &shorterString(const string &s1, const string
&s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
雖然函式內容只有一條語句,但是它兼具了以下的優點(與不用函式直接coding比起來):
- 易讀
- 確保在每一處的行為都一致
- 若要改變內容只需在函數定義處做修改就好
- 可以被其他應用重複利用
然而一般在呼叫函式的時候會經過許多工作,降低了效能。
為了避免效能的降低,我們可以標記函式為inline
,這麼做就會要求編譯器在該函數被呼叫時,直接在該處展開它的程式碼。
Example
cout << shorterString(s1, s2) << endl;
如果用了inline,可能會在編譯時被展開成下面的樣子:
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
如此一來便降低了運行時的負擔。其定義方式為在return type的前面加上inline
:
// inline version: find the shorter of two strings
inline const string &
shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
Note
inline
只是向編譯器提出要求,編譯器可以無視這個要求。
一般來說,inline
用於規模較小、直接且常呼叫的函式,很多編譯器不在recursive functions支援inline
。
constexpr
Functions是可以被用在constant expression裡面的函式,constexpr Functions被定義的方式與其他函式相同,然而有幾個限制:
(1) return type 以及所有的參數都必須是literal type。
(2) 函式的body必須要恰好有一個return
語句。
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; }
上面的scale函式如果傳入的引數是const expression則輸出也為const expression反之則不是:
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
Note
constexpr
函式回傳的不一定是const
expression。
為了一致性,通常inline
與constexpr
會直接定義於標頭檔中。
例.決定下列哪個函式會被呼叫
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // calls void f(double, double)
步驟1
檢查(挑選candidate functions)
(1) 函式name
(2) 函式在呼叫處是否可見visible
⇒以上四個符合
步驟2
檢查
(1) 引數個數
(2) 引數型別與參數型別是否相符或可透過轉換
⇒ void f(int)
與void f(double, double = 3.14)
相符(第二個有默認引數)
步驟3
找出與該呼叫最佳匹配的函式。
何謂最佳?即引數與參數的型別越相近越佳(e.g.不須經由轉換比需要經由轉換佳)。
如同其他pointer,function pointer指向一個特定型別的函式,一個函式的型別取決於它的回傳型別以及參數型別,但是函式名稱不包含其中,例如:
// compares lengths of two strings
bool lengthCompare(const string &, const string &);
上面的函式型別為 bool(const string&, const string&)
,當我們要宣告一個指向此函式的指標時,我們將指標宣告於函式名稱原本該出現的地方:
// pf points to a function returning bool that takes two const string references
bool (*pf)(const string &, const string &); // 未初始化的指標
Note
包圍住*pf
的括號必須存在,否則我們會宣告成一個回傳bool
pointer的函式:
// declares a function named pf that returns a bool*
bool *pf(const string &, const string &);
當我們將函式的名稱當成值使用時,它會自動被轉成一個pointer,比如,我們可以藉由下面兩種方式將lengthCompare
的位址賦值給pf
:
pf = lengthCompare; // pf now points to the function named lengthCompare
pf = &lengthCompare; // 與上面等價:address-of operator is optional
此外我們可以使用function pointer來使用它所指向的函式:
bool b1 = pf("hello", "goodbye"); // calls lengthCompare
bool b2 = (*pf)("hello", "goodbye"); //與上面等價
bool b3 = lengthCompare("hello", "goodbye"); // 同上
兩個指向不同函式型別的指針之間不存在轉換,但我們可以用nullptr跟0來賦值,表示他們並未指向任何函式:
string::size_type sumLength(const string&, const string&);
bool cstringCompare(const char*, const char*);
pf = 0; // ok: pf points to no function
pf = sumLength; // error: return type differs
pf = cstringCompare; // error: parameter types differ
pf = lengthCompare; // ok: function and pointer types match exactly
當函式有重載的情形,編譯器會根據指針的型別來決定指向哪一個,指針型別必須與某個重載函式的型別確實匹配:
void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; // pf1 points to ff(unsigned)
void (*pf2)(int) = ff; // error: no ff with a matching parameter list
double (*pf3)(int*) = ff; // error: return type of ff and pf3 don't match
我們不能直接把參數的型別定義成函式的型別,但可以是函式的指針,就如同array一樣,我們可以把參數寫的像函式,但實際上它是被當成指標看待:
// 第三個參數是函式型別,但它被自動當成pointer to function對待
void useBigger(const string &s1, const string &s2,
bool pf(const string &, const string &));
//與上方等價:explicitly define the parameter as a pointer to function
void useBigger(const string &s1, const string &s2,
bool (*pf)(const string &, const string &));
我們可以直接把函式名稱傳入該函式,它會被自動轉成pointer:
// automatically converts the function lengthCompare to a pointer to function
useBigger(s1, s2, lengthCompare);
某些函數的型別很複雜,如果搭配typedef
跟decltype
,可以簡化程式碼:
// Func and Func2 have function type
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; // equivalent type
// FuncP and FuncP2 have pointer to function type
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2; // equivalent type
該注意的是decltype
回傳的是函式型別而非指標,如果要的是指標的話則一定要加上*
,有了上面的簡化,useBigger
可以這樣宣告:
// equivalent declarations of useBigger using type aliases
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, FuncP2);
函式不能直接回傳函式型別,但可以回傳pointer to function,一個比較容易的方法是使用type alias來簡化語法:
using F = int(int*, int); // F is a function type, not a pointer
using PF = int(*)(int*, int); // PF is a pointer type
要記住的是不像在參數中函數型別可以自動轉為pointer to function,在宣告中我們必須清楚標明該回傳型別是一個pointer:
PF f1(int); // ok: PF is a pointer to function; f1 returns a pointer to function
F f1(int); // error: F is a function type; f1 can't return a function
F *f1(int); // ok: explicitly specify that the return type is a pointer to function
當然我們也可以直接宣告,只是比較麻煩:
int (*f1(int))(int*, int);
另外我們也可以使用trailing return(見6.3.3):
auto f1(int) -> int (*)(int*, int);
如果知道了我們想要回傳的是哪個函數,我們可以用decltype
來簡化回傳型別的程式碼。
Example
string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
// depending on the value of its string parameter,
// getFcn returns a pointer to sumLength or to largerLength
decltype(sumLength) *getFcn(const string &);