第六章 函數
函數:一個命名的代碼塊。通過調用函數執行響應的代碼。函數可以有0個或者多個參數。通常會產生一個結果。
重載:同一個名字可以對應幾個不同的函數
6.1 函數基礎
函數的格式:
返回類型 函數名字 (參數列表){
函數體
}
參數列表,以逗號分隔
函數的調用:
函數或者指向函數的指針(實參列表)
實參列表用逗號隔開
例子:
//返回類型 函數名 (參數列表)
int fact(int val){
int ret = 1;
while(val >1)
ret *= val--;
return ret;
}
//調用
int main(){
//函數或者指向函數的指針(實參列表)
int j = fact(5);
cout << "5! is " << j << endl;
return 0;
}
函數執行的步驟:
-
隱式的定義並初始化它的形參。因此調用fact函數的時候,首先創建一個val的int變量,然後將它初始化爲調用時所用的實參5
-
開始執行函數體
-
當遇到return語句時,結束函數體的執行。同時,將return語句的值,返回給調用者
形參和實參
實參時形參的初始值。他們依據位置一一對應。雖然位置一一對應,但是卻沒有規定他們的求值順序。
實參類型必須和形參類型一一匹配。這個和初始化變量的要求一模一樣。
形參列表
形參列表可以爲空,這樣就只有一個小括號。
同時,爲了和c語言兼容,也可以在空形參列表的位置,寫上void
形參列表以逗號分隔,每個元素跟變量聲明一樣,注意,就算多個變量類型,一樣,也要分開寫。如下:
int f3(int v1,v2){/*....*/}//錯誤,
int f4(int v1,int v2){/*...*/}//正確
形參名,不能重名
形參屬於,函數體內部的局部變量,超出這個範圍,訪問失敗
返回類型
- void類型,表示函數不返回任何值。
- 函數不能返回數組類型,和函數類型,但是可以返回指向他們的指針
6.1.1 局部對象
作用域:名字可見的範圍
生命週期:對象存在的時間
函數的函數體,就是一個語句塊,這個語句塊構成一個作用域,形參和函數體內部定義的變量都在這個作用域中。
形參和函數體內部定義的變量稱爲局部變量。之所以稱爲局部,是因爲超出函數體之後,這些將不復存在。
局部對象還會隱藏外層作用域的同名聲明。
在函數體內部的對象,分爲以下幾種:
- 自動對象:把只存在於塊執行期間的對象,稱爲自動對象。
自動對象的創建:從變量的定義語句開始。一旦程序的控制路徑到達變量的定義語句,則創建該對象。
自動對象的銷燬:一旦到達定義語句所在作用域的末尾,則銷燬該對象。
形參是一種自動對象,函數開始時,爲形參申請存儲空間。因爲形參的作用域爲函數體,因此,函數一旦結束,則形參被銷燬。
而形參的初始化,是通過實參:用實參的值,初始化形參。而其他在函數體內部定義的對象,如果沒有進行初始化,那麼他們執行默認初始化,因此對於內置類型來說,如果沒有初始化,那麼他們的值就是未定義的。
- 局部靜態變量:將局部對象用static關鍵字修飾,就變成了局部靜態對象。
局部靜態對象的初始化:從定義語句開始。
局部靜態對象的銷燬:程序終止時,銷燬。
例子如下:
size_t count_calls(){
static size_t ctr = 0;
return ++ctr;
}
int main(){
for(size_t i=0;i!=10;++i)
cout << count_calls() << endl;
return 0;
}
count_calls內部的ctr使用了static關鍵字修飾,因此是一個局部靜態變量,當第一次調用count_calls函數時,將初始化ctr變量,然後自增ctr。當後續幾次調用count_calls時,因爲已經初始化了ctr,所以不再初始化,而是,直接自增ctr。這樣ctr一直保存有count_calls被調用的次數
6.1.2 函數聲明
函數在使用之前必須先聲明,函數的聲明跟,函數定義一樣。唯一的區別是:將函數體用分號代替。注意此處,的分號,並不是代替了空的語句塊,而是表示語句的結束。
又因爲在聲明中沒有函數體,所以形參名也就用不上,所以聲明中可以省略形參名。但是加上形參名會更好,因爲,這樣有助於程序的理解。
函數聲明也叫做,函數的原型。因爲聲明包括了函數的三要素:返回類型,函數名,形參類型。
這三要數可以唯一標識一個函數類型。
通常將函數聲明放在頭文件中,然後在需要用到的地方,include這個頭文件即可。這樣可以保證,所有需要用到這個函數的聲明都是一致的。
6.1.3 分離式編譯
隨着程序規模的變大,程序員希望將程序,按照邏輯進行劃分。c++提供了這種功能,這種功能稱爲分離式編譯.分離式編譯可以將程序分開到不同的文件中,每個文件單獨編譯。這樣程序員就可以將不同的邏輯放在不同的文件中,每個文件單獨編譯。
舉個例子(書中例子):
一個fact.cc文件,聲明在Chapter6.h中,一個factMain.cc文件,包含程序入口main
使用下面的命令,同時編譯兩個文件
$ CC factMain.cc fact.cc #產生factMain.exe 或者a.out
$ CC factMian.cc fact.cc -o main #產生main或者main.exe
分離式編譯如下:
$ CC -c factMain.cc #編譯factMain.cc生成中間文件factMain.o
$ CC -c fact.cc #編譯fact.cc,生成中間文件fact.o
$ CC factMain.o fact.o #將中間文件生成可執行文件
$ CC factMian.o fact.o -o main #將中間文件生成可執行文件
不同的編譯器,分離式編譯使用的步驟有細微差別,可參考編譯器手冊。
6.2 參數傳遞
參數傳遞分爲兩種:引用傳遞,和值傳遞
6.2.1 值傳遞
將實參的值,複製給形參。他與非引用類型變量的初始化一樣。修改形參,不會對實參有任何影響。他們是兩個不同的變量
注意:當需要傳遞的形參類型是指針類型時,實際還是進行的值傳遞,即將一個實參指針,複製給了形參。他們時兩個不同的指針,只不過指向了同一個地址而已
void reset(int *ip){
*ip = 0;//改變了ip所指對象的值
ip = 0;//只改變了ip這個局部變量,實參爲改變
}
int i= 42;
reset (&i); //改變了i的值,並非i的地址
cout << "i = " << i << endl;//i = 0
熟悉c的程序員常常使用指針類型的形參,訪問函數外部的對象。在c++語言中,建議使用引用類型的形參代替指針類型
6.2.2 引用傳遞
先來個例子
int n = 0;i = 42;
int &r = n; //r綁定到了n
r = 42; //現在的n爲42
r = i; //現在的n和i值形同
i = r; //i的值和n相同
引用形參和上面的引用變量類似,可以讓函數改變引用所綁定的對象。
舉例如下:
void reset(int &i){
i = 0;//會改變i所綁定對象的值
}
因爲i時引用類型,所以當改變i的值,就相當於改變i所綁定對象的值。
int j = 42;
reset(j); //j使用引用傳遞,j的值會被改變
cout << "j = "<< j << endl;//輸出j = 0
使用引用避免拷貝
-
拷貝大類型對象時,比較耗時,因此可以使用引用
-
對於有些類型來說,不支持拷貝操作,因此在函數穿參中,必須使用引用累心給,比如(IO類型)
舉例如下:
bool isShorter(const string &s1,const string &s2){
return s1.size() < s2.size();
}
因爲string對象可能會比較大,爲了避免無所謂的複製,此處的函數形參定義爲引用類型,同時,又因爲不會修改內容,定義了const類型。
使用引用形參還可以返回額外的信息
一個函數只能返回一個值,如果一個邏輯需要返回多個值,可以使用引用形參。將需要返回的結果,放入引用形參中。
舉例如下:
下面邏輯,希望返回c第一齣現的位置,同時,統計其次數,因爲需要返回兩個參數,所以將c出現次數通過引用形參返回。
string::size_type find_char(const string&s,char c,string::size_type &occurs){
auto ret = s.size();
occurs = 0;
for(decltype(ret) i = 0;i != s.size() ;++i){
if(s[i] == c){
if(ret == s.size())
ret = i;
++occurs;
}
}
return ret;
}
6.2.3 const形參和實參
跟變量的初始化一樣,當實參用於初始化形參時,其頂層const將會被忽略。
void fcn(const int i){/*fcn 能夠讀取i,但是不能向i寫值*/}
調用fcn時,既可以傳入const int,也可以傳入int。忽略掉頂層const 可能產生意向不到的結果:
void fcn(const int i){}
void fcn(int i){} //錯誤:重複定義了fcn(int)
因爲頂層const被忽略,所以上面兩個相當於同一個定義。
儘量使用const引用
定義成普通的引用,常會引入下面的不便:
- 讓使用者認爲,裏面的值可以被改變
- 不能使用const對象,字面值,或者需要類型轉換的對象,進行傳參。
因此,常常將形參定義成const 引用
6.2.4 數組形參
當形參時數組時,有如下兩個限制:
- 不能拷貝數組
- 使用數組的時候,會將其轉換成指針
舉例如下:
//儘管形式不一樣,但是這三個print函數是等價的。
//每一個函數都有一個const int*類型的形參
void print(const int*);
void print(const int[]); //形參爲數組
void print(const int[10]);//形參爲數組,並且希望有十個元素,但是實際有多少元素,未定
int i = 0;j[2] = {0,1};
print(&i); //正確:&i的類型爲int *
print(j); //正確:j將轉換成int* 並指向j[0]
如果給實參是一個數組,自動將這個數組轉換成一個指向數組首元素的指針。
注意:以數組作爲形參的函數,也必須確保使用數組的時候不會越界
正是由於數組以指針的形式被傳遞出去,所以數組的大小就需要另外的方式傳遞給函數,下面有幾種常見的方式傳遞給函數:
使用標記表示數組長度
這種方式表示,在數組本身內容中,有表示數組結束的標記,比如c風格字符串,他以空字符作爲結尾,這樣一旦遇到空字符就可以表示達到了末尾。
void print(const char *cp){
if(cp)
while(*cp)
cout << *cp ++;
}
這種方式的唯一要求是:數組的內容和標記不能混淆。
使用標準庫規範
向函數傳遞數組的首元素和尾後元素的指針。例子如下:
void print(const int* beg,const int *end){
while(beg!=end)
cout << *beg++ << endl;
}
調用形式如下:
int j[2] = {0,1};
print(begin(j),end(j));
其中begin和end是標準庫提供的支持,這樣就可以將一個數組的範圍提供給一個函數了。
顯示的傳遞一個表示數組大小的形參
例如如下:
void print(const int ia[] ,size_t size){
for(size_t i = 0;i!= size;i++){
cout << ia[i] << endl;
}
}
調用形式如下:
int j[] = {0,1};
print(j,end(j)-begin(j));
數組引用形參
形參和普通的變量一樣,可以定義爲數組的引用。此時,引用形參綁定到了對應的實參上面,也就是綁定到了數組上面。
例如如下:
void print(int (&arr)[10]){
for(auto elem:arr)
cout <<elem << endl;
}
注意:arr兩邊的括號必不可少
f(int &arr[10]) //錯誤:聲明成了數組的引用 f(int (&arr)[10]) //正確
調用如下:
int i = 0,j[2] = {0,1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
print(&i); //錯誤:實參不是含有10個int的數組
print(j); //錯誤:實參不是含有10個int的數組
print(k); //正確:實參時含有10個int的數組
傳遞多維數組
跟普通的數組一樣,傳遞多維數組,實際上,傳遞是的指向多維數組首元素的指針。因爲多維數組,實質上是數組的數組,所以,首元素就是一個指向數組的指針。數組第二維的大小就是數組類型的一部分,不能夠勝率:
void print(int (*matrix)[10],int rowSize){}
將上面函數的matrix爲,一個指向數組的指針,這個數組有10int類型。
等價定義
void print(int matrix[][10],int rowSize){}
6.2.5 main函數處理命令行參數
沒有什麼特殊,不再做筆記
6.2.6 含有可變形參的函數
有時候,不知道函數需要多少個形參,此時可以使用可變形參的函數,可變形參的函數有兩種方法:
initializer_list形參
如果實參數量未知,但是類型,相同,我們可以使用initializer_list類型的形參。它提供的操作如下:
注意:initialize_lise對象中的元素是常量值,無法對其進行改變
例子如下:
void error_msg(initializer_list<string> il){
for(auto beg = il.begin();beg != il.end();++beg)
cout << *beg << " ";
cout << endl;
}
使用的例子如下:
if(expected!=actual)
error_msg({"functoinX",expected,acuta;});
else
error_msg({"functionX","okay"});
省略符形參
省略符形參應該僅僅用於c和c++通用的類型。特別應該注意的是,大多數類類型的對象在傳遞給省略符形參時都無法正確拷貝
省略符形參只能出現在形參的最後一個位置,它的形式無外乎如下兩種:
void foo(parm_list,...);
void foo(...);
對於有類型的形參,則跟其他普通的形參一樣,會進行相應的類型檢查。省略符形參對應的實參無須進行類型檢查。在第一種形式中,形參聲明後面的逗號是可選。
此種用法還不會,帶後續完善加強
6.3 返回類型和return語句
return語句終止當前正在執行的函數並將控制權返回到調用該函數的地方。
return語句有兩種形式:
return;
return expression;
6.3.1 無返回值的函數
無返回值的return語句,只能用在返回類型是void的函數,而void函數不一定非要return語句。舉例如下:
void swap(int &v1,int &v2)}{
if( v1 == v2)
return;
int tmp = v2;
v2 =v1;
v1 = tmp;
//此處無須顯示的return語句
}
6.3.2 有返回值得函數
如果一個函數的返回類型不爲空,那麼這個函數的每一條return
語句都需要返回一個值。並且這個返回值類型必須與函數的返回類型相同,或者隱式地轉換成函數的返回類型。
例如:
bool str_subrange(const string &str1,const string &str2){
if(str1,size() == str2.size()){
return str1 == str2;
}
auto size = (str1.size() < str2.size())?
str1.size():str2.size();
for(decltype(size) i = 0;i!=size;i++){
if(str1[i] != str2[2]){
return;//錯誤,沒有返回值
}
}
//錯誤:控制流可能尚未返回值,就結束了函數的執行
//編譯器可能檢查不出這一錯誤
}
for循環內部的return語句是錯誤的,因爲他沒有返回值,編譯器能夠檢查這種錯誤
for語句之後沒有提供return語句。在上面的程序中,如果string對象是另外一個的子集,則函數在執行完for循環之後還將繼續執行,顯然應該有一個return語句,來專門處理這種情況。編譯器可能檢查不出這種錯誤。一旦檢查不出來,這種行爲是未定義的。
值是如何被返回的
返回一個值得方式和初始化一個變量或者形參的方式完全一樣:返回值用於初始化調用點的一個臨時變量,該臨時量就是函數調用的結果。
不要返回局部對象的引用或者指針
函數完成之後,所佔用的存儲空間也隨之被釋放。因此,局部變量將被銷燬,如果返回局部變量的指針,或者引用,那麼這個指針指向的對象,或者這個引用綁定的對象是不存在的。
引用返回左值
函數的返回類型決定了函數返回的是左值還是右值。調用一個返回引用的函數得到的是左值,其他返回類型得到的是右值。
列表初始化返回值
舉例如下:
vector<string> process(){
if(expected.empty())
return {};
else if(expected == actual)
return {"functoinX","okay"};
else
return {"functionX",expected,actual};
}
上面函數的所有return語句,都返回一個用大括號括起來的,列表。
這就跟使用列表進行對變量初始化一樣。所以對於內置類型來說,花括號之內只能有一個值。
main的返回值
前面有介紹過:如果函數的返回類型不是void,則必須返回一個值。
唯一的例外是:允許main函數沒有return語句直接返回。如果控制語句達到了main函數的結尾而且沒有return語句,則編譯器將隱式地插入一條返回0的return語句。
main函數的返回值,被認爲程序執行狀態的指示器。返回0表示成功,非0值表示執行失敗,其中非0值的具體含義依機器而定。
爲了使返回值與機器無關,cstdlib頭文件定義了兩個預處理變量,可以使用這兩個預處理變量表示成功或者失敗。
int main(){
if(some_failure)
return EXIT_FAILURE;//定義在cstdlib頭文件
else
return EXIT_SUCCESS;//定義在cstdlib頭文件
}
6.3.3 返回數組指針
因爲數組不能拷貝,因此函數只能返回數組的指針,或者數組的引用。
格式如下:
type (*function(parameter_list))[dimension]
int (*func(int i))[10];
理解如下:
- func是一個函數,它的形參爲int
- *func(int i)函數的返回類型爲一個指針
- (*func(int i))[10]這個指針指向一個數組,數組維度爲10
- 數組的元素爲int類型
上面的寫法對於新手來說有點不易理解,有幾下幾種寫法可以更加容易理解
尾置返回類型
auto func(int i) -> int(*)[10];
將返回類型放在最後,然後以前函數類型的位置,使用一個auto關鍵字
使用decltype
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
decltype(odd) * arrPtr(int i){
return (i%2)? &odd:&even;//返回一個指向數組的指針
}
注意:decltype並不負責將數組類型轉換成對應的指針類型,所以decltype的結果是一個數組,要想表示指針,還需要加上一個星號*
使用類型別名
typedef int arrT[10];
using arrT = int[10];
arrT * func(int i);
6.4 函數重載
重載函數:同一作用域中的幾個函數名相同但是形參列表不同。
void print(const char *cp);
void print(const int *beg,const int*end);
void print(const int ia[],size_t size);
注意:main函數不能重載
對於重載函數來說:他們應該在形參類型,後者形參數量上有所不同。
注意下面的情況:
Record lookup(const Account & acct);
Record lookup(const Account &); //省略掉了名字
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); //Telno和Phone類型相同
上面兩組都是相同的函數。因爲他們的形參類型和形參的數量並沒有變化。
在第二組中形參的類型,本質上是同一種類型
再思考下面的情況:
Record lookup(Phone);
Record lookup(const Phone); //重複聲明Record lookup(Phone)
Record lookup(Phone *):
Record lookup(Phone * const);//重複聲明Record lookup(Phone *)
這兩組聲明中,第二個都跟第一個是同樣的聲明。因爲,頂層const會被忽略掉,因此,對於含有頂層const的形參,和沒有頂層const的形參,無法區別開來。
再思考下面的例子:
Record lookup(Account &);
Record lookup(const Account &);//新函數作用於常量引用
Record lookup(Account *);
Record lookup(const Account *);//新函數,作用於常量指針
上面兩組函數,都是不同的函數,因爲編譯器可以通過實參是否爲常量來決定調用那一個函數。
const_cast和重載
如果有下面的函數
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);
}
調用重載的函數
函數匹配:將函數調用與一組重載函數中的某個函數關聯起來的過程。它又叫做重載確定。
調用重載函數時,有三種可能的結果:
- 編譯器找到一個最佳匹配的函數,並調用它
- 找不到任何與調用匹配的函數,此時編譯器發出無匹配的錯誤信息
- 找到多個函數可以匹配,但是每一個匹配都不是最佳匹配,此時編譯器發出二義性調用的錯誤。
6.4.1 重載與作用域
記住:如果在內層作用域中聲明的名字,它將隱藏外層作用域中聲明的同名實體。
例子如下:
string read();
void print(const string &);
void print(double);
void foorBar(int ival){
bool read = false;//新作用域,隱藏外層的read
string s = read();//錯誤,read是一個布爾值,而非函數
//不好的習慣:通常來說,在局部作用域中聲明函數不是一個好的選擇
void print(int);
print("Value: ");//錯誤:print(const string &) 被隱藏掉了
print(ival); //正確:當前print(int);可見
print(3.14); //正確:調用print(int); print(double)被隱藏掉
}
因爲c++中,名字查找在類型檢查之前,所以,當調用print函數時,先找到了局部作用域中聲明的函數,然後再進行類型的檢查。發現3.14時,將會將其轉換成int類型,然後調用局部作用域中的函數聲明。
6.5 特殊用途語言特性
6.5.1 默認實參
例子如下:
typedef string::size_type sz;
string screen(sz ht = 24,sz wid = 80,char background = ' ');
注意:一旦一個形參被賦予了默認值,那麼這個形參之後的所有形參都必須有默認值。
在傳遞參數的時候,帶有默認實參的形參,可以不用傳遞實參,也可以傳遞實參。
默認實參的聲明
在一個作用域中一個形參只能被賦予一次默認實參。例子如下:
string screen(sz,sz,char = ' ');
string screen(sz,sz,char = ' *');//錯誤重複聲明
string screen(sz = 24,sz = 80,char);//正確
通常,應該在函數聲明中指定默認實參,並將該聲明放在合適的頭文件中
默認實參初始值
局部變量不能作爲默認實參。除此之外,只要表達式的類型能轉換成形參所需的類型,該表達式就能作爲默認實參。
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(),sz = wd,char = def);
string window = screen();//調用screen(ht(),80,' ');
用作默認實參的名字在函數聲明所在的作用域解析,而這些名字的求值過程在發生在函數調用時:
void f2(){
def = ' *'; //改變了默認值
sz wd = 100; //隱藏了外層定義的wd,但是沒有改變默認值
window = screen(); //調用screen(ht(),80,'*');
}
6.5.2 內聯函數和constexpr函數
對於一些頻繁調用的小的函數而言,可以使用內聯函數,以此來減少函數調用產生的跳轉消耗
例子如下:
inline const string &
shorterString(const string &s1,const string &s2){
return s1.size() <= s2.size() ? s1:s2;
}
一般來說,內聯機制用於優化規模較小,流程直接,頻繁調用的函數。
不過很多編譯器都不支持內聯遞歸函數。
constexpr函數
constexpr函數:能用於常量表達式的函數。
constexpr函數:函數的返回類型及所有形參的類型都是字面值類型,而且函數體中必須有且只有一條return語句
constexpr int new_sz(){return 42;}
constexpr int foo = new_sz();//正確,foo是一個常量表達式
執行改初始化時,編譯器把對constexpr函數的調用替換成其結果值。爲了能夠在編譯時隨時展開,constexpr函數被隱式地指定爲內聯函數
constexpr函數體內也可以包含其他語句,只要這些語句運行時不執行任何操作就行。例如,可以包含空語句,類型別名,已經using聲明等。
//如果cnt是一個常量表達式,則scale爲constexpr函數,否則,不是
constexpr size_t scale(size_t cnt){return new_sz() * cnt;}
int arra[scale(2)];//正確,scale(2)是一個常量表達式
int i = 2;
int a2[scale(i)];//錯誤:scale(i)不是一個常量表達式
因此從上面可以知道:constexpr函數不一定返回常量表達式
把內聯函數和constexpr函數放在頭文件
和其他函數不一樣,內聯函數和constexpr函數可以在程序中多次定義。畢竟,編譯器要想展開函數僅有函數聲明是不夠的,還需要函數的定義。不過對於某個給定的內聯函數或者constexpr函數lai9shuo,它的多個定義必須完全一樣。基於這個原因,內聯函數和constexpr函數通常定義在頭文件中。
6.5.3調試幫助
assert預處理
assert(expr);
如果expr爲假,則輸出信息並終止程序。如果爲真,assert什麼也不做
assert定義在cassert頭文件中。
在開發階段,讓這個assert生效,而在正式的軟件版本中,讓assert失效。這依賴NDEBUG預處理變量
如果定義了NDEBUG則assert什麼也不做。默認狀態下沒有定義NDEBUG。
這樣只需要在正式版本中,定義NDEBUG即可。就可以忽略掉assert的運行時開銷。
除此之外,還可以更具NDEBUG自己定義相應的調試代碼。例子如下:
void print(const int ia[],size_t size){
#ifndef NDEBUG
cerr << __func__ << ": array size is "<< size << endl;
#endif
//...
}
上面函數中,如果沒有定義NDEBUG,則執行#ifndef和#endif之間的代碼。如果定義了NDEBUG,這些代碼將被忽略掉
c++除了定義了__func__之外,還定義了下面:
__FILE__ 存放文件名字的字符串字面值
__LINE__ 存放當前行號的整形字面值
__TIME__ 存放文件編譯時間的字符串字面值
__DATE__ 存放文件編譯日期的字符串字面值
6.6 函數匹配
在重載函數中,函數的匹配有下面的幾個過程。
確定候選函數和可行函數
第一步:選定本次調用對應的重載函數集合,集合中的函數稱爲候選函數。
候選函數具備兩個特徵:1.與被調用的函數同名;2.其聲明在調用點可見。
第二步:從候選函數中選出能被這組實參調用的函數,這些函數稱爲可行函數。
可行函數有兩個特徵:1.形參數量與實參數量相同;2.每個形參類型與實參類型相同,或者實參類型能夠轉換成形參類型。
尋找最佳匹配
第三步:從可行函數中選擇與本次調用最匹配的函數。這一步的過程爲:逐一檢查函數調用提供的實參,尋找形參類型與實參類型最匹配的可行函數。類型越接近,匹配越完美
例子如下:
void f();
void f(int);
void f(int,int);
void f(double,double = 3.14);
f(5.6);
運用上面的步驟:
- 候選函數爲:
void f();
void f(int);
void f(int,int);
void f(double,double = 3.14);
- 可行函數爲:
void f(int);
void f(double,double = 3.14);
- 最佳匹配
類型越接近匹配越完美,因此,最佳匹配爲:
void f(double,double = 3.14);
現在思考下面的調用的匹配過程:
f(42,2.56);
1.候選函數:
void f();
void f(int);
void f(int,int);
void f(double,double = 3.14);
2.可行函數
void f(int,int);
void f(double,double = 3.14);
3.最佳匹配,逐一對每個實參進行類型檢查
對於第一形參來說,最佳匹配爲
void f(int,int);
對於第二個形參來說,最佳匹配爲
void f(double,double = 3.14);
無法判斷哪一個可行函數最佳,因此,編譯器報,二義性錯誤
6.6.1 實參類型轉換
爲了確定最佳匹配,編譯器將實參類型到形參類型轉換劃分成了幾個等級,具體排序如下:
1·精確匹配,包括如下情況:
實參類型和形參類型相同
實參從數組類型或者函數類型轉換成對應的指針類型
向實參添加頂層const或者從實參中刪除頂層const
2.通過const轉換實現的匹配
3.通過類型提升實現的匹配
4.通過算術類型或者指針類型轉換實現的匹配
5.通過類類型轉換實現的匹配
例子:
void ff(int);
void ff(short);
ff('a'); //char提升爲int,調用f(int)
注意:所有的算術類型的轉換級別都一樣。例如,int向unsigned int的轉換並不比int向double的類型轉換級別高:
void mainp(long);
void mainp(float);
manip(3.14);//二義性錯誤,因爲3.14爲double,可同時轉換成float和long,並且兩者的轉換級別相同
Record lookup(Account &);
Record lookup(const Account &);
const Account a;
Account b;
lookup(a);//調用Record lookup(const Account &);
lookup(b)://調用Record lookup(Account &);
在第一次調用中,傳入的是const對象,不能將const對象綁定到普通的引用中,所以,最佳匹配爲
Record lookup(const Account &);
在第二次調用中,傳入的是一個非const對象,而非const對象可以綁定const引用,也可以綁定到非const引用。然後用非常量對象綁定給const引用,需要進行一次類型轉換,而非const引用形參的函數,則是精確匹配,所以最佳匹配爲這個函數
Record lookup(Account &);
指針也有上面類似的行爲
6.7 函數指針
函數類型由返回類型,和形參類型決定。
可以聲明一個指針,指向函數,格式如下:
bool (*pf)(const string &,const string &);//爲初始化
解說:
- pf是一個指針
- 這個指針指向一個函數,這個函數的返回類型爲bool
- 這個函數的形參爲兩個const string &
注意:pf兩端的括號必不可少
函數指針的使用
pf = lengthCompare;//pf指向lengthCompare函數
pf = &lengthCompare;//等價的賦值語句:取地址符是可選的
bool b1 = pf("hello","goodbye");//調用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;//正確:pf不指向任何函數
pf = sumLength://錯誤:返回類型不匹配
pf = cstringCompare;//錯誤:形參類型不匹配
pf = lengthCompare;//正確:函數和指針的類型精確匹配
函數指針形參
void useBigger(const string&s1,const string &s2,
bool pf(const string &,const string &));
//等價的聲明
void useBigger(const string &s1,const string &s2,
bool (*pf)(const string &,const string &));
//自動將函數lengthCompre轉換成指向該函數的指針
useBigger(s1,s2,lengthCompare);
可以使用類型別名來簡化這種寫法:
//Func和Func2是函數類型
typedef bool Func(const string &,const string &);
typedef decltype(lengthCompare) Func2;//等價的類型
//FuncP和FuncP2指向函數的指針
typedef bool (*FuncP)(const string & ,const string &);
typedef decltype(lengthCompare) *FuncP2; //等價的類型
void useBigger(cost string &,const string &,Func);
void useBigger(cons string &,const string &,FuncP2);//等價的聲明
返回指向函數的指針
不能返回函數,但是能夠返回指向函數的指針
寫法類似下面這樣:
int (*f1(int))(int *,int);
解析:
- f1是一個函數,
- 它的返回類型是一個指針,
- 這個指針指向一個函數,
- 指向的函數的形參爲int*,int
- 指向的函數的返回類型爲int
上面的寫法較爲繁瑣,可以簡化爲下面的寫法:
使用尾置類型
auto f1(int) -> int(*)(int *,int);
可以使用類型別名
using F = int (int *,int);//F函數類型,不是指針
using PF = int(*)(int *,int); //PF函數指針
PF f1(int);//正確:PF是指向函數的指針,f1返回的是指向函數的指針
F f1(int);//錯誤:F是函數類型,f1不能返回函數類型
F* f1(int);//正確:顯示地指定返回類型爲函數指針
使用auto和decltype
string::size_type sumLength(const string &,const string &);
string::size_type largerLength(const string &,const string &);
decltype(sumLength) * getFcn(const string &);
注意:跟decltype作用在數組一樣,decltype作用在函數上面時,返回的時函數類型,
因此如果想要聲明函數指針,則應該多加一個星號*
上面難免錯誤,遇到錯誤的請指出,謝謝