天天看點

C++primer 學習筆記——第六章 函數

一、函數基礎

函數:傳回類型+函數名字+形參清單+函數體

通過調用運算符來執行函數。調用運算符的形式是一對圓括号,它作用于一個表達式,該表達式是函數或者指向函數的指針。

形參與實參

實參是形參的初始值。

實參的類型和數量必須與對應的形參類型和數量比對。即使某個形參不被函數使用,也必須為它提供一個實參。

函數的形參清單

函數的形參清單可以為空,但是不能省略。

void f1()  {/*...*/}    //隐式地定義空形參清單
void f2(void)  {/*...*/} //顯式地定義空形參清單
           

形參清單中的形參通常用逗号隔開,每個形參都是含有一個聲明符的聲明。即使兩個形參的類型一樣,也必須把兩個類型都寫出來:

int f3(int v1,v2) {/*...*/}  //錯誤
int f4(int v1,int v2) {/*...*/}  //正确
           

任意兩個形參都不能同名,而且函數最外層作用于中的局部變量也不能使用與函數形參一樣的名字。

函數傳回類型

函數的傳回類型不能是數組類型或函數類型,但可以是指向數組或函數的指針。

1.局部對象

形參和函數内部定義的變量統稱為局部變量,僅在函數的作用域内可見,同時局部變量還會隐藏在外層作用域中同名的其他所有聲明中

在所有函數體之外定義的對象存在于程式的整個執行過程中。

自動對象:我們把隻存在于塊執行期間的對象稱為自動對象。

局部靜态對象:

某些時候,有必要令局部變量的生命周期貫穿函數調用以及之後的時間。可以将局部變量定義成static類型進而獲得這樣的對象。局部靜态對象在程式的執行路徑第一次經過對象定義語句時初始化,直到程式終止時銷毀,在此期間即使對象所在的函數結束執行也不會對它有影響。

如果局部靜态變量沒有顯式的初始值,它将執行值初始化,内置類型的局部靜态變量初始化為0。

2.函數聲明

函數的名字必須在使用之前聲明。類似于變量,函數隻能定義一次,但可以聲明多次。唯一的例外:如果一個函數永遠也不會被用到,那麼它可以隻有聲明沒有定義。

函數聲明無需函數體,隻要一個分号代替就可以。

因為函數的聲明不包含函數體,是以也就無須形參的名字。函數聲明也稱作函數原型。

在頭檔案中進行函數聲明

函數也應該在頭檔案中聲明而在源檔案中定義。

定義函數的源檔案應該把含有函數聲明的頭檔案包含進來,編譯器負責驗證函數的定義和聲明是否比對。

3.分離式編譯

分離式編譯允許我們把程式分割到幾個檔案中去,每個檔案獨立編譯。

二、參數傳遞

每次調用函數時都會重新建立它的形參,并用傳入的實參對形參進行初始化。

和其他變量一樣,形參的類型決定了形參和實參互動的方式。如果形參是引用類型,它将綁定到對應的實參上;否則,将實參的值拷貝後賦給形參。

當形參是引用類型時,我們說它對應的實參被引用傳遞或者函數被傳引用調用。

當實參的值被拷貝給形參時,形參和實參是兩個獨立的對象。我們說這樣的實參被值傳遞或者函數被傳值調用。

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++語言中,建議使用引用類型的形參代替指針。

2、傳引用參數

通過使用引用形參,允許函數改變一個或多個實參的值。

使用引用避免拷貝

拷貝的類型性對象或者容器比較低效,甚至有的類類型(包括IO類型在内)根本就不支援拷貝操作。當某種類型不支援拷貝操作時,函數隻能通過形參通路該類型的對象。

如果函數無須改變引用形參的值,最好将其聲明為常量引用。

//比較兩個string對象的長度
bool isShorter(const string &s1,const string &s2)
{
    return s1.size()<s2.size();
}
           

使用引用形參傳回額外資訊

一個函數隻能傳回一個值,然而有時函數需要同時傳回多個值,引用形參為我們一次傳回多個結果提供了有效的途徑:

//傳回s中c第一次出現的位置索引
//引用形參occurs負責統計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!=size();++i){
        if(s[i]=c){
            if(ret==size())
                ret=i;
            ++occurs;
        }
    } 
    return ret;    //出現次數通過occurs隐式地傳回
}
           

3、const形參和實參(十分重要)

當用實參初始化形參時會忽略掉頂層const,當形參有頂層const時,傳遞給它常量對象或者非常量對象都是可以的:

void fcn(const int i){/*fcn能夠讀取i,但是不能向i寫值*/}
           

調用fcn函數時,既可以傳入const int也可以傳入int。

允許我們定義若幹具有相同名字的函數,不過前提是不同函數的形參清單應該有明顯的差別。因為頂層const被忽略掉了,是以在下面的代碼中傳入兩個fcn函數的形參可以完全一樣。是以第二個fcn是錯誤的,盡管形式上有差别,但實際上它的形參和第一個fcn的形參沒什麼不同:

void fcn(const int i){/*fcn能夠讀取i,但是不能向i寫值*/}
void fcn(int i){/*...*/}//錯誤:重複定義了fcn(int)
           

指針或引用形參與const

我們可以使用非常量初始化一個底層const對象,但是反過來不行;同時一個普通的引用必須用同類型的對象初始化;

不能用字面值常量初始化一個非常量引用,但是可以用字面值初始化常量引用;

這些規則同樣适用于參數傳遞:

int i=0;
const int ci=i;
string::size_type ctr=0;
reset(&i);    //調用形參類型是int*的reset函數
reset(&ci);   //錯誤:不能用指向const int對象的指針初始化int*
reset(i);     //調用形參類型時int&的reset函數
reset(ci);    //錯誤:不能把普通引用綁定到const對象ci上
reset(42);    //錯誤:不能把普通引用綁定到字面值上
reset(ctr);   //錯誤:類型不比對,ctr是無符号類型
find_char("Hello world!",'o',ctr);   //可以使用字面值初始化常量引用
           

盡量使用常量引用

将函數不會改變的形參定義為普通引用是一種比較常見的錯誤,一是會帶給函數的調用者一種誤導,即函數可以修改它的實參的值。二是使用引用而非常量引用也會極大的限制函數所能接受的實參類型——不能把const對象、字面值或者需要類型轉換的對象傳遞給普通的引用形參。

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]
           

如果我們傳遞給print函數的是一個數組,則實參自動地轉換成指向數組首元素的指針,數組的大小對函數的調用并沒有影響。

數組有确切大小,但指針沒有,是以我們要通過一些手段來管理(數組的)指針。管理指針的三種常用的技術:

①使用标記指定數組長度

這種方法要求數組本身包含一個結束标志,适用于那些有明顯結束标記且該标記不會與普通資料混淆的情況。

②使用标準庫規範

這種技術傳遞指向數組首元素和尾後元素的指針

void print(const int *beg,const int *end)
{
    while(beg!=end)
        cout<<*beg++<<endl;  //輸出目前元素并将指針向前移動一個位置
}
           

③顯式傳遞一個表示數組大小的實參

第三種管理數組形參的方法是專門定義一個表示數組大小的形參

//const int ia[]等價于const int* ia
//size表示數組的大小,将它顯式的傳給函數用于控制對ia元素的通路
void print(const int ia[],size_t size)
{
    for(size_t i=0;i!=size;++i){
        cout<<ia[i]<<endl;
    } 
}
           

數組形參和const

當函數不需要對數組元素執行寫操作時,數組形參應該是指向const的指針。隻有當函數确實要改變元素值的時候,才把形參定義成指向非常量的指針。

數組引用形參

形參也可以是數組的引用,此時,引用形參綁定到對應的實參上,也就是綁定到數組上:

void print(int (&arr)[10])
{
    for(auto elem:arr)
        cout<<elem<<endl;
}
           

&arr兩端的括号必不可少:

f(int &arr[10]) //錯誤:将arr聲明成了引用的數組(10個元素都是引用)
f(int (&arr)[10])  //正确:arr是具有10個整數的整形數組的引用
           

這一用法也無形限制了print函數的可用性,我們隻能将函數作用于大小為10的函數:

int i=0,j[2]={0,1};
int k[10]={0,1,2,3,4,5,6,7,8,9};
print(&i); //錯誤:實參不是含有10個整數的數組
print(j);  //錯誤:實參不是含有10個整數的數組
print(k);  //正确:實參是含有10個整數的數組
           

傳遞多元數組

和所有數組一樣,當将多元數組傳遞給函數時,真正傳遞的是指向數組首元素的指針。

因為我們處理的是數組的數組,是以首元素本身就是一個數組,指針就是一個指向數組的指針。數組第二維(以及後面所有次元)的大小都是數組類型的一部分,不能省略:

//matrix指向數組的首元素,該數組的元素是由10個整數構成的數組
void print(int(*matrix)[10],int rowSize){/*...*/}

//等價定義
//matrix的聲明看起來是一個二維數組,實際上形參是指向含有10個整數的數組的指針
void print(int matrix[][10],int rowSize){/*...*/}
           

再一次強調,*matrix兩端的括号不可省略:

int *matrix[10];     //10個指針構成的數組
int (*matrix)[10];   //指向含有10個整數的數組的指針
           

5、main:處理指令行選項

 C++的main函數可以沒有輸入參數,也可以有輸入參數,而且隻能有兩個參數,習慣上coding如下:

int main(int argc, char* argv[]) 或者 int main(int argc, char** argv)
           

 其中,argc = argument count :表示傳入main函數的數組元素個數,為int類型,而 argv = argument vector :表示傳入main函數的指針數組,為char**類型。第一個數組元素argv[0]是程式名稱,并且包含程式所在的完整路徑。argc至少為1,即argv數組至少包含程式名。

#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
    for(int i=0;i<argc;i++)
        cout<<argv[i]<<endl;
    return 0;
}
           

 一般編譯器預設使用argc和argv兩個名稱作為main函數的參數,但這兩個參數如此命名并不是必須的,你可以使用任何符合C++語言命名規範的變量名,但要保證第一個參數類型為int型,第二個參數為char**型,如下圖所示。

#include <iostream>
using namespace std;
int main(int count, char* input_parameters[])
{
    for(int i=0;i<count;i++)
        cout<<input_parameters[i]<<endl;
    return 0;
}
           

 由于main函數不能被其他函數調用,是以不可能在程式内部取得實際值。main函數的參數值是從作業系統指令行上擷取的。在window系統中,假如編譯連結成的可執行檔案為my_project.exe,則在指令提示符(快捷鍵windows+R,輸入cmd)中,鍵入如下指令(可執行檔案 參數 參數 參數 ...):

my_project.exe jisongxie 1996
           

 将會傳遞三個參數給main函數,第一個argv[0]是前面提到的檔案名,第二個argv[1]是"jisongxie",第三個argv[2]是“1996”。同理,可以傳入更多的參數。在ubuntu系統中,可以通過終端進行相同的操作。

當使用argv中的實參時,一定要記得可選的實參從argv[1]開始;argv[0]儲存程式的名字,而非使用者輸入。

  傳入的參數數組類型為char *字元串類型,可以通過atoi,atof函數進行類型的轉換。

  1、atoi,即ascii to integer,把字元串轉換成int

  2、atof,即ascii to float,把字元串轉換成double

  3、atol,即ascii to long int,把字元串轉換成long int

  4、atoll,即ascii to long long int,把字元串轉換成long long int

  例如上述輸入的1996,可以得到如下:

 int year = atoi(argv[2]);  // year = 1996
           

是以,通過上述的指令行輸入以及程式裡面的類型轉換,可以通過指令行視窗傳入值(字元串和數字)到程式中運作。

6、含有可變形參的函數

編寫實參數量不同的函數有三種方法:initializer_list形參、可變參數模闆(第16章)、省略符形參

initializer_list形參

如果函數的實參數量未知但是全部實參的類型相同,我們可以使用initializer_list類型的形參。initializer_list是一種标準庫類型,用于辨別某種特定類型的值得數組。該類型定義在同名的頭檔案中。

initializer_list提供的操作

initializer_list<T> lst;
initializer_list<T> lst{a,b,c...};
lst2(lst);
lst2=lst;
lst.size();
lst.begin();
lst.end();

和vector一樣,initializer_list也是一種模闆類型:

initializer_list<string> ls;
initializer_list<int> li;
           

和vector不同的是,initializer_list對象中的元素永遠是常量值,我們無法改變initializer_list對象中元素的值。

含有initializer_list形參的函數也可以同時擁有其他形參:

void error_msg(ErrorCode e,initializer_list<string> il)
{
    cout<<e.msg()<<":";
    for(const auto &elem:il) 
        cout<<elem<<" ";
    cout<<endl;
}
           

如果想向initializer_list形參中傳遞一個值得序列,則必須把序列放在一對花括号内:

if(expected!=actual)
    error_msg(ErrCode(42),{"functionX",expected,actual});
else
    error_msg(ErrCode(0),{"functionX","okay"});
           

省略符形參

省略符形參應該僅僅用于C和C++通用的類型。

void foo(parm_list,...);
void foo(...);
           

三、傳回類型和return語句

return有兩種形式:

return;

return expression;

1、無傳回值函數

沒有傳回值得return語句隻能用在傳回類型是void的函數中。傳回void的函數不要求非得有return語句,因為在這類函數的最後一句後面會隐式地執行return。

如果void函數如果想在它的中間位置提前退出,可以使用return語句。return的這種做法有點類似于我們用break語句。

強行令void函數傳回其他類型的表達式将産生編譯錯誤。

2、有傳回值函數

return語句傳回值的類型必須與函數的傳回類型相同,或者能隐式地轉換成函數的傳回類型。

在含有return語句的循環後面應該也有一條return語句,如果沒有的話該程式就是錯誤的。很多編譯器都無法發現此類錯誤。

值是如何被傳回的

傳回一個值的方式和初始化一個變量和形參的方式完全一樣:傳回的值用于初始化調用點的一個初始值,該臨時量就是函數調用的結果。

如果傳回類型為非引用類型,則意味着傳回值将被拷貝到調用點:

//如果ctr的值大于1,傳回Word的複數形式
string make_plural(size_t ctr,const string &word
                              const string &ending)
{
  return (ctr>1)? word+ending : word;
}
           

如果函數傳回引用,則該引用僅僅是它所引用對象的一個别名:

const string &shorterString(const string &s1,const string &s2)
{
    return s1.size()<=s2.size()? s1 : s2;
}
           

不要傳回局部對象的引用或指針

函數完成後,它所占的存儲空間也随之被釋放掉。是以,函數終止意味着局部變量的引用将指向不再有效的記憶體區域,是以傳回局部對象的引用是錯誤的;同樣,傳回局部對象的指針也是錯誤的。一旦函數完成,局部對象被釋放,指針将指向一個不存在的對象。

//嚴重錯誤:這個函數試圖傳回局部對象的引用
const string &manip
{
    string ret;
    if(!ret.empty())
        return ret;    //錯誤:傳回局部變量的引用
    else
        return "Empty";//錯誤:“Empty”是一個局部變量
}
           

傳回類類型的函數和調用運算符

調用運算符的優先級與點運算符和箭頭運算符相同,并且也符合左結合律

auto sz=shorterString(s1,s2).size();//得到較短string對象的長度
           

引用傳回左值

調用一個傳回引用的函數得到左值,其他傳回類型得到右值。可以像使用其他左值那樣來使用傳回引用的函數的調用,特别的是,我們能為傳回類型是非常量引用的函數的結果指派:

char &get_val(string &str,string::size_type ix)
{
    return str[ix];  //假定索引值是有效的
}

int main()
{
    string s("a value");
    cout<<s<<endl;
    get_value(s,0)='A';
    cout<<s<<endl;    //輸出A value
}
           

如果傳回類型是常量引用,我們不能給調用的結果指派。

清單初始化傳回值

函數可以傳回花括号包圍的值的清單,用來初始化函數傳回的臨時量;如果清單為空,臨時量執行值初始化:

vector<string> process()
{ 
    //...
    //expected和actual是string對象
    if(expected.empty())
        return {};                   //傳回一個空vector對象
    else if(expected==actual)
        return{"functionX","okay"};  //傳回清單初始化的vector對象
    else 
        return{"functionX",expected,actual};
}
           

如果函數傳回的是内置類型,則花括号包圍的清單最多包含一個值,而且該值所占空間不應該大于目标類型的空間。如果函數傳回的是類類型,由類本身定義初始值如何使用。

主函數main的傳回值

傳回0表示執行成功,傳回其他值表示執行失敗,其中非0的值得含義由機器而定。

為了使傳回值與機器無關,cstdlib頭檔案定義了兩個預處理變量,我們可以使用這兩個變量分别表示成功與失敗:

int main()
{
    if(some_failure)
        return EXIT_FAILURE;
    else
        return EXIT_SUCCESS;
}
           

遞歸

一個函數直接或者間接地調用它自身,稱該函數為遞歸函數:

//計算val的階乘
int factorial(int val)
{
    if(val>1)
        return factorial(val-1)*val;
    return 1;
}
           

main函數不能調用它自己

3、傳回數組指針

因為數組不能被拷貝,是以函數不能傳回數組。不過,函數可以傳回數組的指針或引用。其中最直接的方法是使用類型别名:

typedef int arrT[10];
using arrT=int[10];
arrT* func(int i); //func傳回一個指向含有10個整數的數組的指針
           

聲明一個傳回數組指針的函數

傳回數組指針的函數形式如下所示:

Type(*function(parameter_list))[dimension]

例子:int (*func(int i))[10]

func(int i)表示調用func函數時需要一個int類型的實參。

(*func(int i))意味着我們可以對函數調用的結果執行解引用操作

(*func(int i))[10]表示解引用将得到一個大小是10的數組

int (*func(int i))[10]表示數組中的元素是int類型

使用尾置傳回類型

auto func(int  i)->int(*)[10];

//func接受一個int的實參,傳回一個指針,該指針指向含有10個整數的數組

使用decltype

如果我們知道函數傳回的指針将指向哪個數組,就可以使用decltype關鍵字聲明傳回類型:

int odd[]={1,3,5,7,9};
int even[]={0,2,4,6,8};
//傳回一個指針,該指針指向含有5個整數的數組
decltype(odd) *arrPtr(int i)
{
    return(i%2)?&odd:&even; 
}
           

decltype并不負責把數組類型轉換成對應的指針,是以decltype的結果是個數組,想要表示arrPtr傳回指針還必須在函數聲明時加一個*符号。

四、函數重載

main函數不能重載。

定義重載函數

對于重載函數,他們應該在形參數量或形參類型上有所不同:

Record lookup(const Account&);
Record lookup(const Phone&);
Record lookup(const Name&);
Account acct;
Phone phone;
Record R1=lookup(acct);
Record R2=lookup(phone);
           

不允許兩個函數除了傳回類型之外其他所有的要素都相同:

Record lookup(const Account&);
bool lookup(const Account&);  //錯誤:與上一個函數相比隻有傳回類型不同
           

判斷兩個形參的類型是否相異

//每對聲明的是同一個函數
Record lookup(const Account &acct);
Record lookup(const Account&);  //省略了形參的名字

typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&);  //Telno和phone的類型相同
           

重載和const形參

一個擁有頂層const的形參無法與一個沒有頂層const的形參分開來:

Record lookup(Phone);
Record lookup(const Phone);//重複定義

Record lookup(Phone*);
Record lookup(phone* const); //重複定義
           

如果形參是某種類型的指針或引用,則通過區分其指向的是常量對象還是非常量對象可以實作函數重載,此時的const是底層的:

//定義了4個獨立的重載函數
Record lookup(Account&);
Record lookup(const Account&);

Record lookup(Account*);
Record lookup(const Account*);
           

const_cast和重載

當實參不是常量時,得到的結果是一個普通的引用,使用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);
}
           

1、重載與作用域

一般來說,将函數聲明置于局部作用域内不是一個明智的選擇。因為如果在内層作用域内聲明名字,它将隐藏外層作用域中聲明的同名實體:

string read();
void print(const string &);
void print(double);
void fooBar(int ival)
{
    bool read=false;  //外層的read()被隐藏
    string s=read();  //錯誤:read是一個布爾值,而不是函數
    void print(int);  //隐藏之前的print函數
    print("Value");   //錯誤:print(const string &)被隐藏
    print(ival);      //正确:目前print(int)可見
    print(3.14);      //正确:調用print(int);print(double)被隐藏
}
           

五、特殊用途語言特性

1、預設實參

某些函數的形參,在函數的很多次調用中都被賦予一個相同的值,此時我們把這個反複出現的值稱為函數的預設實參。調用含有預設實參的函數時,可以包含該實參,也可以省略該實參。

typedef string::size_type sz;
string screen(sz ht=24,sz wid=80,char backgrnd=' ');
           

需要注意的是,一旦某個形參被賦予了預設值,它後面的所有形參都必須有預設值。

使用預設實參函數

如果想要使用預設實參,隻要在調用函數的時候省略該實參就可以了:

string window;
window=screen();                 //等價于screen(24,80,' ')
window=screen(66);               //等價于screen(66,80,' ')
window=screen(66,256);           //等價于screen(66,256,' ')
window=screen(66,256,'#');       //等價于screen(66,256,'#')
           

函數調用時實參按照其位置解析,預設實參負責填補函數調用缺少的尾部實參(靠右側的位置)

預設實參聲明

通常,應該在函數聲明中指定預設實參,并将該聲明放在合适的頭檔案中。

函數的後續聲明隻能為之前那些沒有預設值的形參添加預設實參,并且該形參右側的所有形參都必須有預設值。

2、内聯函數和constexpr函數

内聯函數可以避免函數調用的開銷

将函數指定為内聯函數(inline),通常就是将它在每個調用點上“内聯地”展開:

cout<<shorterString(s1,s2)<<endl;

//編譯過程類似于下面的形式
cout<<(s1.size()<s2.size()?s1:s2)<<endl;
           

constexpr函數

constexpr函數是指能用于常量表達式的函數,函數的傳回類型及所有形參的類型都得是字面值類型,而且函數體中必須有且隻有一條return語句:

constexpr int new_sz() {return 42;}
constexpr int foo=new_sz(); //正确:foo是一個常量表達式
           

constexpr函數不一定傳回常量表達式

通常把内聯函數和constexpr函數放在頭檔案中

六、函數比對

确定候選函數和可行函數

如果函數含有預設實參,則我們在調用該函數時傳入的實參數量可能少于它實際使用的實參數量。

如果沒找到可行函數,編譯器将報告無比對函數的錯誤

尋找最佳比對

含有多個形參的函數比對

調用重載函數時應盡量避免強制類型轉換。如果在實際應用中确實需要強制類型轉換,則說明我們設計的形參集合不合理

1、實參類型轉換

需要類型提升和算術類型轉換的比對

小整形一般都會提升到int類型或更大的整數類型:

void ff(int);
void ff(short);
ff('a');  //char提升為int;調用ff(int)
           

所有算術類型轉換的級别都是一樣的

void manip(long);
void manip(float);
manip(3.14);  //錯誤:二義性調用
           

函數比對和const實參

如果實參是指向常量的指針,調用形參是const*的函數;如果實參是指向非常量的指針,調用形參是普通指針的函數。引用類型的形參也一樣。

七、函數指針

函數指針指向的是函數而非對象。和其他類型一樣,函數指針指向某種特定類型。函數的類型由它的傳回類型和形參類型共同決定,與函數名無關。例如:

bool lengthCompare(const string&,const string&);
           

該函數的類型是bool(const string&,const string&)。想要聲明一個可以指向該函數的指針,隻需要用指針替換函數名即可:

//pf指向一個函數
bool (*pf)(const string&,const string&);  //未初始化
           

*pf兩端的括号必不可少:

//聲明一個名為pf的函數,該函數傳回bool*
bool *pf(const string&,const string&);
           

使用函數指針

當我們把函數名作為一個值使用時,該函數自動地轉換成指針:

pf=lengthCompare;    //pf指向名為lengthCompare的函數
pf=&lengthCompare;   //等價的指派語句:取位址符是可選的
           

我們還可以直接使用指向函數的指針調用該函數,無須提前解引用指針:

bool b1=pf("hello","goodbye");            //調用lengthCompare函數
bool b2=(*pf)("hello","goodbye");         //一個等價的調用
bool b3=lengthCompare("hello","goodbye"); //另一個等價的調用
           

在指向不同函數類型的指針間不存在轉換規則:

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&));
           

我們可以直接把函數作為實參使用,此時它會自動地轉換成指針:

//自動地将函數lengthCompare轉換成指向函數的指針
useBigger(s1,s2,lengthCompare);
           

直接使用函數指針類型顯得冗長而繁瑣,可以用類型别名和decltype來簡化代碼。

傳回指向函數的指針

和數組類似,雖然我們不能傳回一個函數,但是能傳回指向函數類型的指針。然而我們必須把傳回類型寫成指針形式,編譯器不會自動地将函數傳回類型當成對應的指針類型處理:

//使用類型别名
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);  //正确:顯式的指定傳回類型是指向函數的指針
           

也可以直接聲明f1:

int (*f1(int))(int*,int);
           

f1(int)說明f1是個函數

*f1(int)說明f1傳回一個指針

(*f1(int))(int*,int),指針的類型本身也包含形參清單,是以指針指向函數

int (*f1(int))(int*,int),該函數傳回的類型是int

将auto和decltype用于函數指針類型

如果我們明确知道傳回的函數是哪一個,就能使用decltype簡化書寫函數指針傳回類型的過程:

string::size_type sumLength(const string&,const string&);
string::size_type largerLength(const string&,const string&);
//傳回指向指向sumLength或者largerLength的指針
decltype(sumLength) *getFun(const string &);
           

我們将decltype作用域某個函數時,它傳回函數類型而非指針類型。是以,我們顯式地加上*以表示我們需要傳回指針,而非函數本身。

繼續閱讀