c++ primer 第五版 筆記 第六章

第六章 函數

函數:一個命名的代碼塊。通過調用函數執行響應的代碼。函數可以有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;
}

函數執行的步驟:

  1. 隱式的定義並初始化它的形參。因此調用fact函數的時候,首先創建一個val的int變量,然後將它初始化爲調用時所用的實參5

  2. 開始執行函數體

  3. 當遇到return語句時,結束函數體的執行。同時,將return語句的值,返回給調用者

形參和實參

實參時形參的初始值。他們依據位置一一對應。雖然位置一一對應,但是卻沒有規定他們的求值順序。

實參類型必須和形參類型一一匹配。這個和初始化變量的要求一模一樣。

形參列表

形參列表可以爲空,這樣就只有一個小括號。

同時,爲了和c語言兼容,也可以在空形參列表的位置,寫上void

形參列表以逗號分隔,每個元素跟變量聲明一樣,注意,就算多個變量類型,一樣,也要分開寫。如下:

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

形參名,不能重名

形參屬於,函數體內部的局部變量,超出這個範圍,訪問失敗

返回類型

  1. void類型,表示函數不返回任何值。
  2. 函數不能返回數組類型,和函數類型,但是可以返回指向他們的指針

6.1.1 局部對象

作用域:名字可見的範圍
生命週期:對象存在的時間

函數的函數體,就是一個語句塊,這個語句塊構成一個作用域,形參和函數體內部定義的變量都在這個作用域中。

形參和函數體內部定義的變量稱爲局部變量。之所以稱爲局部,是因爲超出函數體之後,這些將不復存在。

局部對象還會隱藏外層作用域的同名聲明。

在函數體內部的對象,分爲以下幾種:

  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

使用引用避免拷貝

  1. 拷貝大類型對象時,比較耗時,因此可以使用引用

  2. 對於有些類型來說,不支持拷貝操作,因此在函數穿參中,必須使用引用累心給,比如(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引用

定義成普通的引用,常會引入下面的不便:

  1. 讓使用者認爲,裏面的值可以被改變
  2. 不能使用const對象,字面值,或者需要類型轉換的對象,進行傳參。

因此,常常將形參定義成const 引用

6.2.4 數組形參

當形參時數組時,有如下兩個限制:

  1. 不能拷貝數組
  2. 使用數組的時候,會將其轉換成指針

舉例如下:

//儘管形式不一樣,但是這三個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];

理解如下:

  1. func是一個函數,它的形參爲int
  2. *func(int i)函數的返回類型爲一個指針
  3. (*func(int i))[10]這個指針指向一個數組,數組維度爲10
  4. 數組的元素爲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);
}

調用重載的函數

函數匹配:將函數調用與一組重載函數中的某個函數關聯起來的過程。它又叫做重載確定。

調用重載函數時,有三種可能的結果:

  1. 編譯器找到一個最佳匹配的函數,並調用它
  2. 找不到任何與調用匹配的函數,此時編譯器發出無匹配的錯誤信息
  3. 找到多個函數可以匹配,但是每一個匹配都不是最佳匹配,此時編譯器發出二義性調用的錯誤。

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

運用上面的步驟:

  1. 候選函數爲:
void f();
void f(int);
void f(int,int);
void f(double,double = 3.14);
  1. 可行函數爲:
void f(int);
void f(double,double = 3.14);
  1. 最佳匹配
    類型越接近匹配越完美,因此,最佳匹配爲:
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 &);//爲初始化

解說:

  1. pf是一個指針
  2. 這個指針指向一個函數,這個函數的返回類型爲bool
  3. 這個函數的形參爲兩個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);

解析:

  1. f1是一個函數,
  2. 它的返回類型是一個指針,
  3. 這個指針指向一個函數,
  4. 指向的函數的形參爲int*,int
  5. 指向的函數的返回類型爲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作用在函數上面時,返回的時函數類型,
因此如果想要聲明函數指針,則應該多加一個星號*

上面難免錯誤,遇到錯誤的請指出,謝謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章