日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

《C++ Primer 5th》笔记(6 / 19):函数

發布時間:2023/12/13 c/c++ 53 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《C++ Primer 5th》笔记(6 / 19):函数 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

    • 函數基礎
      • 局部對象
      • 函數聲明
      • 分離式編譯
    • 參數傳遞
      • 傳遞參數
      • 傳引用參數
        • 使用引用避免拷貝
        • 使用引用形參返回額外信息
      • const形參和實參
        • 指針或引用形參與const
        • 盡量使用常量引用
      • 數組形參
        • 使用標記指定數組長度
        • 使用標準庫規范
        • 顯示傳遞一個表示數組大小的形參
        • 數組形參和const
        • 數組引用形參
        • 傳遞多維數組
      • main:處理命令行選項
      • 含有可變形參的函數
        • initializer_list形參
        • 省略符形參
    • 返回類型和return語句
      • 無返回值函數
      • 有返回值函數
        • 值是如何被返回的
        • 不要返回局部對象的引用或指針
        • 返回類類型的函數和調用運算符
        • 引用返回左值
        • 列表初始化返回值
        • 主函數main的返回值
        • 遞歸
      • 返回數組指針
        • 聲明一個返回數組指針的函數
        • 使用尾置返回類型
        • 使用decltype
    • 函數重載
      • 定義重載函數
        • 判斷兩個形參的類型是否相異
        • 重載和const形參
        • 建議:何時不應該重載函數
        • const_cast和重載
        • 調用重載的函數
      • 重載與作用域
    • 特殊用途語言特性
      • 默認實參
        • 使用默認實參調用函數
        • 默認實參聲明
        • 默認實參初始值
      • 內聯函數和constexpr函數
        • 內聯函數可避免函數調用的開銷
        • constexpr函數
        • 把內聯函數和constexpr函數放在頭文件內
      • 調試幫助
        • assert預處理宏
        • NDEBUG預處理變量
    • 函數匹配
      • 例子:調用應該選用哪個重載函數
        • 確定候選函數和可行函數
        • 尋找最佳匹配(如果有的話)
        • 含有多個形參的函數匹配
      • 實參類型轉換
        • 需要類型提升和算術類型轉換的匹配
        • 函數匹配和const實參
    • 函數指針
      • 使用函數指針
        • 重載函數的指針
        • 函數指針形參
        • 返回指向函數的指針
        • 將auto和decltype用于函數指針類型

函數是一個命名了的代碼塊,我們通過調用函數執行相應的代碼。函數可以有0個或多個參數,而且(通常)會產生一個結果??梢灾剌d函數,也就是說,同一個名字可以對應幾個不同的函數。

函數基礎

一個典型的函數(function)定義包括以下部分:

  • 返回類型(return type)
  • 函數名字
  • 由0個或多個形參(parameter)組成的列表,形參以逗號隔開,形參的列表位于一對圓括號之內
  • 函數體。函數執行的操作在語句塊中說明,該語句塊稱為函數體( function body)

我們通過調用運算符(call operator)來執行函數。調用運算符的形式是一對圓括號,它作用于一個表達式,該表達式是函數或者指向函數的指針;

圓括號之內是一個用逗號隔開的實參(argument)列表,我們用實參初始化函數的形參。

調用表達式的類型就是函數的返回類型。

編寫函數

舉個例子,我們準備編寫一個求數的階乘的程序。n 的階乘是從1到n所有數字的乘積,例如5的階乘是120。

1 * 2 * 3 * 4 * 5 = 120

程序如下所示:

// 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 itwhile (val > 1)ret *= val--; // assign ret * val to ret and decrement valreturn ret; // return the result }

調用函數

要調用fact函數,必須提供一個整數值,調用得到的結果也是一個整數:

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)開始執行。
  • 執行函數的第一步是(隱式地)定義并初始化它的形參。因此,當調用fact函數時,首先創建一個名為val的int變量,然后將它初始化為調用時所用的實參5。

    當遇到一條return語句時函數結束執行過程。和函數調用一樣,return語句也完成兩項工作:

  • 返回return 語句中的值(如果有的話),
  • 將控制權從被調函數轉移回主調函數。
  • 函數的返回值用于初始化調用表達式的結果,之后繼續完成調用所在的表達式的剩余部分。因此,我們對fact函數的調用等價于如下形式:

    int val = 5; // initialize val from the literal 5 int ret = 1; // code from the body of fact while (val > 1)ret *= val--; int j = ret; // initialize j as a copy of ret

    形參和實參

    實參是形參的初始值。第一個實參初始化第一個形參,第二個實參初始化第二個形參,以此類推。盡管實參與形參存在對應關系,但是并沒有規定實參的求值順序(參見4.1.3節,第123頁)。編譯器能以任意可行的順序對實參求值。

    實參的類型必須與對應的形參類型匹配,這一點與之前的規則是一致的,我們知道在初始化過程中初始值的類型也必須與初始化對象的類型匹配。函數有幾個形參,我們就必須提供相同數量的實參。因為函數的調用規定實參數量應與形參數量一致,所以形參一定會被初始化。

    在上面的例子中,fact函數只有一個int類型的形參,所以每次我們調用它的時候,都必須提供一個能轉換成int的實參:

    fact( "hello" ); //錯誤:實參類型不正確 fact(); //錯誤:實參數量不足 fact(42, 10, 0); //錯誤:實參數量過多 fact(3.14); //正確:該實參能轉換成int類型
    • 因為不能將const char*轉換成int,所以第一個調用失敗。
    • 第二個和第三個調用也會失敗,不過錯誤的原因與第一個不同,它們是因為傳入的實參數量不對。要想調用fact函數只能使用一個實參,只要實參數量不是一個,調用都將失敗。
    • 最后一個調用是合法的,因為 double可以轉換成int。執行調用時,實參隱式地轉換成int類型(截去小數部分),調用等價于
    fact (3);

    函數的形參列表

    函數的形參列表可以為空,但是不能省略。要想定義一個不帶形參的函數,最常用的辦法是書寫一個空的形參列表。不過為了與C語言兼容,也可以使用關鍵字void表示函數沒有形參:

    void f1(){/* ...*/} //隱式地定義空形參列表 void f2(void){/* ...*/} //顯式地定義空形參列表*

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

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

    任意兩個形參都不能同名,而且函數最外層作用域中的局部變量也不能使用與函數形參一樣的名字。
    形參名是可選的,但是由于我們無法使用未命名的形參,所以形參一般都應該有個名字。

    偶爾,函數確實有個別形參不會被用到,則此類形參通常不命名以表示在函數體內不會使用它。不管怎樣,是否設置未命名的形參并不影響調用時提供的實參數量。即使某個形參不被函數使用,也必須為它提供一個實參。

    函數返回類型

    大多數類型都能用作函數的返回類型。一種特殊的返回類型是void,它表示函數不返回任何值。函數的返回類型不能是數組類型或函數類型,但可以是指向數組或函數的指針,在本章,會陸續介紹。

    局部對象

    在C++語言中,名字有作用域,對象有生命周期(lifetime)。理解這兩個概念非常重要。

    • 名字的作用域是程序文本的一部分,名字在其中可見。

    • 對象的生命周期是程序執行過程中該對象存在的一段時間。

    如我們所知,函數體是一個語句塊。塊構成一個新的作用域,我們可以在其中定義變量。形參和函數體內部定義的變量統稱為局部變量(local variable)。它們對函數而言是“局部”的,僅在函數的作用域內可見,同時局部變量還會隱藏(hide)在外層作用域中同名的其他所有聲明中。

    在所有函數體之外定義的對象存在于程序的整個執行過程中。此類對象在程序啟動時被創建,直到程序結束才會銷毀。局部變量的生命周期依賴于定義的方式。

    自動對象

    對于普通局部變量對應的對象來說,當函數的控制路徑經過變量定義語句時創建該對象,當到達定義所在的塊末尾時銷毀它。我們把只存在于塊執行期間的對象稱為自動對象(automatic object)。當塊的執行結束后,塊中創建的自動對象的值就變成未定義的了。(呼之即來,揮之即去)

    形參是一種自動對象。函數開始時為形參申請存儲空間,因為形參定義在函數體作用域之內,所以一旦函數終止,形參也就被銷毀。

    我們用傳遞給函數的實參初始化形參對應的自動對象。對于局部變量對應的自動對象來說,則分為兩種情況:

    • 如果變量定義本身含有初始值,就用這個初始值進行初始化;
    • 否則,如果變量定義本身不含初始值,執行默認初始化。這意味著內置類型的未初始化局部變量將產生未定義的值。

    局部靜態對象

    某些時候,有必要令局部變量的生命周期貫穿函數調用及之后的時間??梢詫⒕植孔兞慷x成static類型從而獲得這樣的對象。局部靜態對象(local static object)在程序的執行路徑第一次經過對象定義語句時初始化,并且直到程序終止才被銷毀,在此期間即使對象所在的函數結束執行也不會對它有影響。

    (MyNote:局部靜態對象具有全局變量長命期與局部變量的私有性。)

    舉個例子,下面的函數統計它自己被調用了多少次,這樣的函數也許沒什么實際意義,但是足夠說明問題:

    size_t count_calls() {static size_t ctr = 0; // value will persist across callsreturn ++ctr; }int main() {for (size_t i = 0; i != 10; ++i)cout << count_calls() << endl;return 0; }

    這段程序將輸出從1到10(包括10在內)的數字。

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

    函數聲明

    和其他名字一樣,函數的名字也必須在使用之前聲明。類似于變量,函數只能定義一次,但可以聲明多次。唯一的例外是如第15章將要介紹的,如果一個函數永遠也不會被我們用到,那么它可以只有聲明沒有定義。

    函數的聲明和函數的定義非常類似,唯一的區別是函數聲明無須函數體,用一個分號替代即可。

    因為函數的聲明不包含函數體,所以也就無須形參的名字。事實上,在函數的聲明中經常省略形參的名字。盡管如此,寫上形參的名字還是有用處的,它可以幫助使用者更好地理解函數的功能:

    //我們選擇beg和end作為形參的名字以表示這兩個迭代器劃定了輸出值的范圍 void print (vector<int>::const_iterator beg, vector<int>:: const_iterator end) ;

    函數的三要素(返回類型、函數名、形參類型)描述了函數的接口,說明了調用該函數所需的全部信息。函數聲明也稱作函數原型(function prototype)。

    在頭文件中進行函數聲明

    回憶之前所學的知識,我們建議變量在頭文件中聲明,在源文件中定義。與之類似,函數也應該在頭文件中聲明而在源文件中定義。

    看起來把函數的聲明直接放在使用該函數的源文件中是合法的,也比較容易被人接受;但是這么做可能會很煩瑣而且容易出錯。相反,如果把函數聲明放在頭文件中,就能確保同一函數的所有聲明保持一致。而且一旦我們想改變函數的接口,只需改變一條聲明即可。

    定義函數的源文件應該把含有函數聲明的頭文件包含進來,編譯器負責驗證函數的定義和聲明是否匹配。

    Best Practices:含有函數聲明的頭文件應該被包含到定義函數的源文件中。

    (MyNote:函數聲明在頭文件,定義在源文件。)

    分離式編譯

    隨著程序越來越復雜,我們希望把程序的各個部分分別存儲在不同文件中。例如,函數存在一個文件里,把使用這些函數的代碼存在其他源文件中。為了允許編寫程序時按照邏輯關系將其劃分開來,C++語言支持所謂的分離式編譯(separate compilation)。分離式編譯允許我們把程序分割到幾個文件中去,每個文件獨立編譯。

    編譯和鏈接多個源文件

    舉個例子,假設fact函數的定義位于一個名為fact.cc的文件中,它的聲明位于名為Chapter6.h的頭文件中。顯然與其他所有用到fact函數的文件一樣,fact.cc應該包含chapter6.h頭文件(#include “Chapter6.h”)。

    另外,我們在名為factMain.cc 的文件中創建main函數,main函數將調用fact函數。要生成可執行文件(executable file),必須告訴編譯器我們用到的代碼在哪里。

    對于上述幾個文件來說,編譯的過程如下所示:

    $ cc factMain.cc fact.cc # generates factMain.exe or a.out $ cc factMain.cc fact.cc -o main # generates main or main.exe

    其中,cc是編譯器的名字,$是系統提示符,#后面是命令行下的注釋語句。接下來如果運行可執行文件,就會執行我們定義的main函數。

    如果我們修改了其中一個源文件,那么只需重新編譯那個改動了的文件。大多數編譯器提供了分離式編譯每個文件的機制,這一過程通常會產生一個后綴名是.obj(Windows)或.o(UNIX)的文件,后綴名的含義是該文件包含對象代碼(object code)。

    接下來編譯器負責把對象文件鏈接在一起形成可執行文件。在我們的系統中,編譯的過程如下所示:

    $ cc -c factMain.cc # generates factMain.o $ cc -c fact.cc # generates fact.o $ cc factMain.o fact.o # generates factMain.exe or a.out $ cc factMain.o fact.o -o main # generates main or main.exe

    你可以仔細閱讀編譯器的用戶手冊,弄清楚由多個文件組成的程序是如何編譯并執行的。

    (MyNote:這就是頭文件與源文件如何聯系在一起。)

    參數傳遞

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

    Note:形參初始化的機理與變量初始化一樣。

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

    • 當形參是引用類型時,我們說它對應的實參被引用傳遞(passed by reference)或者函數被傳引用調用(called by reference)。和其他引用一樣,引用形參也是它綁定的對象的別名;也就是說,引用形參是它對應的實參的別名。

    • 當實參的值被拷貝給形參時,形參和實參是兩個相互獨立的對象。我們說這樣的實參被值傳遞(passed by value)或者函數被傳值調用(called by value)。

    傳遞參數

    當初始化一個非引用類型的變量時,初始值被拷貝給變量。此時,對變量的改動不會影響初始值:

    int n = 0; //int類型的初始變量 int i = n; // i是n的值的副本 i = 42; // i的值改變;n的值不變

    傳值參數的機理完全一樣,函數對形參做的所有操作都不會影響實參。

    例如,在fact函數

    int fact(int val) {int ret = 1;// local variable to hold the result as we calculate itwhile (val > 1)ret *= val--; // assign ret * val to ret and decrement valreturn ret; // return the result }

    內對變量val執行遞減操作:

    ret *= val--;//將val的值減1

    盡管fact函數改變了val的值,但是這個改動不會影響傳入fact的實參。調用fact(i)不會改變i的值。

    指針形參

    指針的行為和其他非引用類型一樣。當執行指針拷貝操作時,拷貝的是指針的值(MyNote:指針,即地址值)??截愔?#xff0c;兩個指針是不同的指針。因為指針使我們可以間接地訪問它所指的對象,所以通過指針可以修改它所指對象的值:

    int n = 0, i = 42; int *p = &n, *q = &i; // p points to n; q points to i *p = 42; // value in n is changed; p is unchanged p = q; // p now points to i; values in i and n are unchanged

    指針形參的行為與之類似:

    // 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 pointsip = 0; // changes only the local copy of ip; the argument is unchanged }

    調用reset函數之后,實參所指的對象被置為o,但是實參本身并沒有改變:

    int i = 42; reset(&i); // changes i but not the address of i cout << "i = " << i << endl; // prints i = 0

    Best Practices:熟悉C的程序員常常使用指針類型的形參訪問函數外部的對象。在C++語言中,建議使用引用類型的形參替代指針。

    (MyNote:引用,我個人理解為特殊的指針。)

    傳引用參數

    回憶過去所學的知識,我們知道對于引用的操作實際上是作用在引用所引的對象上。

    int n = 0, i = 42; int &r = n; // r is bound to n (i.e., r is another name for n) r = 42; // n is now 42 r = i; // n now has the same value as i i = r; // i has the same value as n

    引用形參的行為與之類似。通過使用引用形參,允許函數改變一個或多個實參的值。

    舉個例子,我們可以改寫上一小節的reset程序,使其接受的參數是引用類型而非指針:

    // 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 }

    和其他引用一樣,引用形參綁定初始化它的對象。當調用這一版本的 reset 函數時,i綁定我們傳給函數的int對象,此時改變i也就是改變i所引對象的值。此例中,被改變的對象是傳入reset的實參。
    調用這一版本的reset函數時,我們直接傳入對象而無須傳遞對象的地址:

    int j = 42; reset(j); // j is passed by reference; the value in j is changed cout << "j = " << j << endl; // prints j = 0

    在上述調用過程中,形參i僅僅是j的又一個名字。在reset內部對i的使用即是對j的使用。


    MyNote:傳引用與傳指針相比,傳引用使用時,無需像傳指針那樣要用一個解引用,這樣簡潔些。

    // 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 pointsip = 0; // changes only the local copy of ip; the argument is unchanged }

    使用引用避免拷貝

    拷貝大的類類型對象或者容器對象比較低效,甚至有的類類型(包括IO類型在內)根本就不支持拷貝操作。當某種類型不支持拷貝操作時,函數只能通過引用形參訪問該類型的對象

    舉個例子,我們準備編寫一個函數比較兩個string 對象的長度。因為string對象可能會非常長,所以應該盡量避免直接拷貝它們,這時使用引用形參是比較明智的選擇。又因為比較長度無須改變string 對象的內容,所以把形參定義成對常量的引用:

    // compare the length of two strings bool isShorter(const string &s1, const string &s2) {return s1.size() < s2.size(); }

    如將要介紹的,當函數無須修改引用形參的值時最好使用常量引用。(只讀屬性)

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

    使用引用形參返回額外信息

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

    舉個例子,我們定義一個名為find_char的函數,它返回在string對象中某個指定字符第一次出現的位置。同時,我們也希望函數能返回該字符出現的總次數。

    該如何定義函數使得它能夠既返回位置也返回出現次數呢?

  • 一種方法是定義一個新的數據類型,讓它包含位置和數量兩個成員。
  • 還有另一種更簡單的方法,我們可以給函數傳入一個額外的引用實參,令其保存字符出現的次數:
  • // 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 anyoccurs = 0; // set the occurrence count parameterfor (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 }

    當我們調用find_char函數時,必須傳入三個實參:作為查找范圍的一個string對象、要找的字符以及一個用于保存字符出現次數的size_type對象。假設s是一個string對象,ctr是一個size_type對象,則我們通過如下形式調用find_char函數:

    auto index = find_char(s, 'o', ctr);

    調用完成后,如果string對象中確實存在o,那么ctr的值就是。出現的次數,index指向o第一次出現的位置;否則如果string對象中沒有o, index等于 s.size()而ctr等于0。

    const形參和實參

    當形參是const時,必須要注意第2章關于頂層const的內容。如前所述,頂層const作用于對象本身:

    const int ci = 42; // we cannot change ci; const is top-level int i = ci; // ok: when we copy ci, its top-level const is ignored int * const p = &i; // const is top-level; we can't assign to p,注意,初始化與賦值在C++中是兩碼事 *p = 0; // ok: changes through p are allowed; i is now 0

    和其他初始化過程一樣,當用實參初始化形參時會忽略掉頂層const。換句話說,形參的頂層const被忽略掉了。當形參有頂層const時,傳給它常量對象或者非常量對象都是可以的:

    void fcn(const int i) { /* fcn can read but not write to i */ }

    調用fcn函數時,既可以傳入const int也可以傳入int。忽略掉形參的頂層const可能產生意想不到的結果:

    void fcn(const int i) { /* fcn can read but not write to i */ } void fcn(int i) { /* . . . */ } // error: redefines fcn(int)

    在C++語言中,允許我們定義若干具有相同名字的函數,不過前提是不同函數的形參列表應該有明顯的區別。因為頂層const被忽略掉了,所以在上面的代碼中傳入兩個fcn函數的參數可以完全一樣。因此第二 fcn是錯誤的,盡管形式上有差異,但實際上它的形參和第一個fcn的形參沒什么不同。

    指針或引用形參與const

    形參的初始化方式和變量的初始化方式是一樣的,所以回顧通用的初始化規則有助于理解本節知識。

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

    int i = 42; const int *cp = &i; // ok: but cp can't change i const int &r = i; // ok: but r can't change i const int &r2 = 42; // ok://我們可以使用非常量初始化一個底層const對象,但是反過來不行; int *p = cp; // error: types of p and cp don't match int &r3 = r; // error: types of r3 and r don't match int &r4 = 42; // error: can't initialize a plain reference from a literal//普通引用變量綁定一個變量

    將同樣的初始化規則應用到參數傳遞上可得如下形式:

    // 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 } int i = 0; const int ci = i; string::size_type ctr = 0; reset(&i); // calls the version of reset that has an int* parameter reset(&ci); // error: can't initialize an int* from a pointer to a const int object reset(i); // calls the version of reset that has an int& parameterreset(ci); // error: can't bind a plain reference to the const object ci reset(42); // error: can't bind a plain reference to a literal reset(ctr); // error: types don't match; ctr has an unsigned type// ok: find_char's first parameter is a reference to const find_char("Hello World!", 'o', ctr);

    要想調用引用版本的reset,只能使用int類型的對象,而不能使用字面值、求值結果為int的表達式、需要轉換的對象或者const int類型的對象。類似的,要想調用指針版本的reset只能使用int*。

    另一方面,我們能傳遞一個字符串字面值作為find_char的第一個實參,這是因為該函數的引用形參是常量引用,而C++允許我們用字面值初始化常量引用。

    盡量使用常量引用

    把函數不會改變的形參定義成(普通的)引用是一種比較常見的錯誤,這么做帶給函數的調用者一種誤導,即函數可以修改它的實參的值。此外,使用引用而非常量引用也會極大地限制函數所能接受的實參類型。就像剛剛看到的,我們不能把const對象、字面值或者需要類型轉換的對象傳遞給普通的引用形參。

    這種錯誤絕不像看起來那么簡單,它可能造成出人意料的后果。以上文的find_char函數為例,那個函數(正確地)將它的string類型的形參定義成常量引用。假如我們把它定義成普通的string&:

    // bad design: the first parameter should be a const string& string::size_type find_char(string &s, char c,string::size_type &occurs);

    則只能將find_char函數作用于string對象。類似下面這樣的調用

    find_char("Hello World", 'o', ctr);//string &s = "Hello World";不行,const string &s = "Hello World";行

    將在編譯時發生錯誤。

    還有一個更難察覺的問題,假如其他函數(正確地)將它們的形參定義成常量引用,那么第二個版本的find_char無法在此類函數中正常使用。舉個例子,我們希望在一個判斷string對象是否是句子的函數中使用find_char:

    bool is_sentence(const string &s) {// if there's a single period at the end of s, then s is a sentencestring::size_type ctr = 0;return find_char(s, '.', ctr) == s.size() - 1 && ctr == 1; }

    如果find_char的第一個形參類型是string&,那么上面這條調用find_char的語句將在編譯時發生錯誤。原因在于s是常量引用,但find_char被(不正確地)定義成只能接受普通引用。

    解決該問題的一種思路是修改is_sentence的形參類型,但是這么做只不過轉移了錯誤而已,結果是is_sentence函數的調用者只能接受非常量string對象了。

    正確的修改思路是改正find_char函數的形參。如果實在不能修改find_char,就在is _sentence內部定義一個string類型的變量,令其為s的副本,然后把這個string對象傳遞給find_char。

    數組形參

    數組(第3章內容)的兩個特殊性質對我們定義和使用作用在數組上的函數有影響,這兩個性質分別是:

  • 不允許拷貝數組
  • 使用數組時(通常)會將其轉換成指針。
  • 因為不能拷貝數組,所以我們無法以值傳遞的方式使用數組參數。因為數組會被轉換成指針,所以當我們為函數傳遞一個數組時,實際上傳遞的是指向數組首元素的指針。

    盡管不能以值傳遞的方式傳遞數組,但是我們可以把形參寫成類似數組的形式:

    // despite appearances, these three declarations of print are equivalent // 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)

    盡管表現形式不同,但上面的三個函數是等價的:每個函數的唯一形參都是const int *類型的。當編譯器處理對print函數的調用時,只檢查傳入的參數是否是const int *類型:

    int i = 0, j[2] = {0, 1}; print(&i); // ok: &i is int* print(j); // ok: j is converted to an int* that points to j[0]

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

    WARNING:和其他使用數組的代碼一樣,以數組作為形參的函數也必須確保使用數組時不會越界。

    因為數組是以指針的形式傳遞給函數的,所以一開始函數并不知道數組的確切尺寸,調用者應該為此提供一些額外的信息。管理指針形參有三種常用的技術。

    使用標記指定數組長度

    管理數組實參的第一種方法是要求數組本身包含一個結束標記,使用這種方法的典型示例是C風格字符串。C風格字符串存儲在字符數組中,并且在最后一個字符后面跟著一個空字符。函數在處理C風格字符串時遇到空字符停止:

    void print(const char *cp) {if (cp) // if cp is not a null pointerwhile (*cp) // so long as the character it points to is not a null charactercout << *cp++; // print the character and advance the pointer }

    這種方法適用于那些有明顯結束標記且該標記不會與普通數據混淆的情況,但是對于像int這樣所有取值都是合法值的數據就不太有效了。

    使用標準庫規范

    管理數組實參的第二種技術是傳遞指向數組首元素和尾后元素的指針,這種方法受到了標準庫技術的啟發,使用該方法,我們可以按照如下形式輸出元素內容:

    void print(const int *beg, const int *end) {// print every element starting at beg up to but not including endwhile (beg != end)cout << *beg++ << endl; // print the current element// and advance the pointer }

    while循環使用解引用運算符和后置遞減運算符輸出當前元素并在數組內將beg向前移動一個元素,當beg和end相等時結束循環。

    為了調用這個函數,我們需要傳入兩個指針:一個指向要輸出的首元素,另一個指向尾元素的下一位置:

    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

    只要調用者能正確地計算指針所指的位置,那么上述代碼就是安全的。在這里,我們使用標準庫begin和end函數提供所需的指針。

    顯示傳遞一個表示數組大小的形參

    第三種管理數組實參的方法是專門定義一個表示數組大小的形參,在C程序和過去的C++程序中常常使用這種方法。使用該方法,可以將print函數重寫成如下形式:

    // 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;} }

    這個版本的程序通過形參size的值確定要輸出多少個元素,調用print函數時必須傳入這個表示數組大小的值:

    int j[] = { 0, 1 }; // int array of size 2 print(j, end(j) - begin(j));

    只要傳遞給函數的size值不超過數組實際的大小,函數就是安全的。

    數組形參和const

    我們的三個print函數都把數組形參定義成了指向const的指針,本章關于引用的討論同樣適用于指針。

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

    (MyNote:只讀時用const。)

    數組引用形參

    C++語言允許將變量定義成數組的引用,基于同樣的道理,形參也可以是數組的引用。此時,引用形參綁定到對應的實參上,也就是綁定到數組上:

    // ok: parameter 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兩端的括號必不可少:

    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

    因為數組的大小是構成數組類型的一部分,所以只要不超過維度,在函數體內就可以放心地使用數組。但是,這一用法也無形中限制了print函數的可用性,我們只能將函數作用于大小為10的數組:

    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章將要介紹我們應該如何編寫這個函數,使其可以給引用類型的形參傳遞任意大小的數組。

    傳遞多維數組

    我們曾經介紹過,在C++語言中實際上沒有真正的多維數組,所謂多維數組其實是數組的數組。

    和所有數組一樣,當將多維數組傳遞給函數時,真正傳遞的是指向數組首元素的指針。因為我們處理的是數組的數組,所以首元素本身就是一個數組,指針就是一個指向數組的指針。數組第二維(以及后面所有維度〉的大小都是數組類型的一部分,不能省略:

    // 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個整數的數組的指針。

    Note:再一次強調,*matrix兩端的括號必不可少:

    int *matrix[10]; // array of ten pointers int (*matrix)[10]; // pointer to an array of ten ints

    我們也可以使用數組的語法定義函數,此時編譯器會一如既往地忽略掉第一個維度,所以最好不要把它包括在形參列表內:

    // equivalent definition void print(int matrix[][10], int rowSize) { /* . . . */ }

    matrix的聲明看起來是一個二維數組,實際上形參是指向含有10個整數的數組的指針。

    main:處理命令行選項

    main函數是演示C++程序如何向函數傳遞數組的好例子。到目前為止,我們定義的main函數都只有空形參列表:

    int main () { ... }

    然而,有時我們確實需要給main傳遞實參,一種常見的情況是用戶通過設置一組選項來確定函數所要執行的操作。例如,假定main函數位于可執行文件prog之內,我們可以向程序傳遞下面的選項:

    prog -d -o ofile data0

    這些命令行選項通過兩個(可選的)形參傳遞給main函數:

    int main (int argc, char *argv[]){ ... }

    第二個形參argv是一個數組,它的元素是指向C風格字符串的指針;第一個形參argc表示數組中字符串的數量。

    因為第二個形參是數組,所以main函數也可以定義成:

    int main (int argc, char **argv){ ... }

    其中argv指向char*。

    當實參傳給main函數之后,argv的第一個元素指向程序的名字或者一個空字符串,接下來的元素依次傳遞命令行提供的實參。最后一個指針之后的元素值保證為0。

    以上面提供的命令行為例,argc應該等于5,argv應該包含如下的C風格字符串:

    argv[0] = "prog" ; //或者argv[0]也可以指向一個空字符串 argv[1] = "-d"; argv[2] = "-o"; argv[3] = "ofile"; argv[4] = "datao"; argv[5] = 0;

    WARNING:當使用argv中的實參時,一定要記得可選的實參從argv[1]開始,argv[0]保存程序的名字,而非用戶輸入。

    含有可變形參的函數

    有時我們無法提前預知應該向函數傳遞幾個實參。例如,我們想要編寫代碼輸出程序產生的錯誤信息,此時最好用同一個函數實現該項功能,以便對所有錯誤的處理能夠整齊劃一。然而,錯誤信息的種類不同,所以調用錯誤輸出函數時傳遞的實參也各不相同。

    為了編寫能處理不同數量實參的函數,C++11新標準提供了兩種主要的方法:

  • 如果所有的實參類型相同,可以傳遞一個名為initializer_list的標準庫類型;
  • 如果實參的類型不同,我們可以編寫一種特殊的函數,也就是所謂的可變參數模板,關于它的細節將在第16章介紹。
  • C++還有一種特殊的形參類型(即省略符),可以用它傳遞可變數量的實參。本節將簡要介紹省略符形參,不過需要注意的是,這種功能一般只用于與C函數交互的接口程序。

    initializer_list形參

    如果函數的實參數量未知但是全部實參的類型都相同,我們可以使用initializer_list類型的形參。initializer_list是一種標準庫類型,用于表示某種特定類型的值的數組。initializer_list類型定義在同名的頭文件中,它提供的操作如下表所示。

    ..
    initializer_list lst;默認初始化;T類型元素的空列表
    initializer_list lst{a, b, c…};lst的元素數量和初始值一樣多;lst的元素是對應初始值的副本;列表中的元素是const
    lst2(lst)拷貝或賦值一個initializer_list對象不會拷貝列表中的元素;拷貝后,原始列表和副本共享元素
    lst2 = lst同上條
    lst.size()列表中的元素數量
    lst.begin()返回指向lst中首元素的指針
    lst.end()返回指向lst中尾元素下一位置的指針

    和vector一樣,initializer_list也是一種模板類型。定義initializer_list對象時,必須說明列表中所含元素的類型:

    initializer_list<string> ls; // initializer_list of strings initializer_list<int> li; // initializer_list of ints

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

    我們使用如下的形式編寫輸出錯誤信息的函數,使其可以作用于可變數量的實參:

    void error_msg(initializer_list<string> il) {for (auto beg = il.begin(); beg != il.end(); ++beg)cout << *beg << " " ;cout << endl; }

    作用于initializer_list對象的begin和end操作類似于vector對應的成員。

    begin()成員提供一個指向列表首元素的指針,end()成員提供一個指向列表尾后元素的指針。我們的函數首先初始化 beg令其表示首元素,然后依次遍歷列表中的每個元素。在循環體中,解引用beg 以訪問當前元素并輸出它的值。

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

    // expected, actual are strings if (expected != actual)error_msg({"functionX", expected, actual}); elseerror_msg({"functionX", "okay"});

    在上面的代碼中我們調用了同一個函數error_msg,但是兩次調用傳遞的參數數量不同:第一次調用傳入了三個值,第二次調用只傳入了兩個。

    含有initializer_list形參的函數也可以同時擁有其他形參。例如,調試系統可能有個名為ErrCode的類用來表示不同類型的錯誤,因此我們可以改寫之前的程序,使其包含一個initializer_list形參和一個ErrCode形參:

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

    因為initializer_list包含begin和end成員,所以我們可以使用范圍for循環處理其中的元素。和之前的版本類似,這段程序遍歷傳給il形參的列表值,每次迭代時訪問一個元素。

    為了調用這個版本的error_msg函數,需要額外傳遞一個ErrCode實參:

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

    省略符形參

    省略符形參是為了便于C++程序訪問某些特殊的C代碼而設置的,這些代碼使用了名為varargs的C標準庫功能。通常,省略符形參不應用于其他目的。你的C編譯器文檔會描述如何使用varargs。

    WARNING:省略符形參應該僅僅用于C和C++通用的類型。特別應該注意的是,大多數類類型的對象在傳遞給省略符形參時都無法正確拷貝。

    省略符形參只能出現在形參列表的最后一個位置,它的形式無外乎以下兩種:

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

    第一種形式指定了foo 函數的部分形參的類型,對應于這些形參的實參將會執行正常的類型檢查。省略符形參所對應的實參無須類型檢查。在第一種形式中,形參聲明后面的逗號是可選的。

    返回類型和return語句

    return語句終止當前正在執行的函數并將控制權返回到調用該函數的地方。return語句有兩種形式:

    return; return expression;

    無返回值函數

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

    通常情況下,void函數如果想在它的中間位置提前退出,可以使用return語句。return的這種用法有點類似于我們用break語句退出循環。例如,可以編寫一個swap函數,使其在參與交換的值相等時什么也不做直接退出:

    void swap(int &v1, int &v2) {// if the values are already the same, no need to swap, just returnif (v1 == v2)return;// if we're here, there's work to doint tmp = v2;v2 = v1;v1 = tmp;// no explicit return necessary }

    這個函數首先檢查值是否相等,如果相等直接退出函數,如果不相等才交換它們的值。在最后一條賦值語句后面隱式地執行return。

    一個返回類型是void的函數也能使用return語句的第二種形式,不過此時return語句的expression必須是另一個返回void的函數。強行令void函數返回其他類型的表達式將產生編譯錯誤。

    有返回值函數

    return語句的第二種形式提供了函數的結果。只要函數的返回類型不是 void,則該函數內的每條return語句必須返回一個值。return語句返回值的類型必須與函數的返回類型相同,或者能隱式地轉換成函數的返回類型。

    盡管C++無法確保結果的正確性,但是可以保證每個return語句的結果類型正確。也許無法顧及所有情況,但是編譯器仍然盡量確保具有返回值的函數只能通過一條有效的return語句退出。例如:

    // incorrect return values, this code will not compile bool str_subrange(const string &str1, const string &str2) {// same sizes: return normal equality testif (str1.size() == str2.size())return str1 == str2;// ok: == returns bool// find the size of the smaller string; conditional operatorauto size = (str1.size() < str2.size()) ? str1.size() : str2.size();// look at each element up to the size of the smaller stringfor (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 }
  • 第一個錯誤是for循環內的return語句是錯誤的,因為它沒有返回值,編譯器能檢測到這個錯誤。
  • 第二個錯誤是函數在for循環之后沒有提供 return語句。
  • 在上面的程序中,如果一個string對象是另一個的子集,則函數在執行完for循環后還將繼續其執行過程,顯然應該有一條return 語句專門處理這種情況。編譯器也許能檢測到這個錯誤,也許不能。如果編譯器沒有發現這個錯誤,則運行時的行為將是未定義的。

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

    值是如何被返回的

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

    必須注意當函數返回局部變量時的初始化規則。例如我們書寫一個函數,給定計數值、單詞和結束符之后,判斷計數值是否大于1。如果是,返回單詞的復數形式。如果不是,返回單詞原形:

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

    該函數的返回類型是string,意味著返回值將被拷貝到調用點。因此,該函數將返回word的副本或者一個未命名的臨時string對象,該對象的內容是word和ending的和。

    同其他引用類型一樣,如果函數返回引用,則該引用僅是它所引對象的一個別名。舉個例子來說明,假定某函數挑出兩個string形參中較短的那個并返回其引用:

    //挑出兩個string對象中較短的那個,返回其引用 const string &shorterstring(const string &sl,const string &s2){return s1.size() <= s2.size() ? s1 : s2 ; }

    其中形參和返回類型都是const string 的引用,不管是調用函數還是返回結果都不會真正拷貝string對象。

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

    函數完成后,它所占用的存儲空間也隨之被釋放掉。因此,函數終止意味著局部變量的引用將指向不再有效的內存區域:

    // disaster: this function returns a reference to a local object const string &manip() {string ret; // transform ret in some wayif (!ret.empty())return ret; // WRONG: returning a reference to a local object! elsereturn "Empty"; // WRONG: "Empty" is a local temporary string }

    上面的兩條 return語句都將返回未定義的值,也就是說,試圖使用manip函數的返回值將引發未定義的行為。

    • 對于第一條return語句來說,顯然它返回的是局部對象的引用。
    • 在第二條return語句中,字符串字面值轉換成一個局部臨時string對象,對于manip來說,該對象和 ret一樣都是局部的。

    當函數結束時臨時對象占用的空間也就隨之釋放掉了,所以兩條return語句都指向了不再可用的內存空間。

    Tip:要想確保返回值安全,我們不妨提問:引用所引的是在函數之前已經存在的哪個對象?

    如前所述,返回局部對象的引用是錯誤的;同樣,返回局部對象的指針也是錯誤的。一旦函數完成,局部對象被釋放,指針將指向一個不存在的對象。

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

    和其他運算符一樣,調用運算符也有優先級和結合律。調用運算符的優先級與點運算符和箭頭運算符相同,并且也符合左結合律。因此,如果函數返回指針、引用或類的對象,我們就能使用函數調用的結果訪問結果對象的成員

    例如,我們可以通過如下形式得到較短string對象的長度:

    //調用string對象的size成員,該string對象是由shorterstring函數返回的 auto sz = shorterString(s1,s2).size();

    因為上面提到的運算符都滿足左結合律,所以 shorterString 的結果是點運算符的左側運算對象,點運算符可以得到該string對象的size成員,size又是第二個調用運算符的左側運算對象。

    引用返回左值

    函數的返回類型決定函數調用是否是左值。調用一個返回引用的函數得到左值,其他返回類型得到右值??梢韵袷褂闷渌笾的菢觼硎褂梅祷匾玫暮瘮档恼{用,特別是,我們能為返回類型是非常量引用的函數的結果賦值:

    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 valueget_val(s, 0) = 'A'; // changes s[0] to A這里函數調用是左值,雖然有點怪,但是語法正確的cout << s << endl;// prints A valuereturn 0; }

    把函數調用放在賦值語句的左側可能看起來有點奇怪,但其實這沒什么特別的。返回值是引用,因此調用是個左值,和其他左值一樣它也能出現在賦值運算符的左側。

    如果返回類型是常量引用,我們不能給調用的結果賦值,這一點和我們熟悉的情況是一樣的:

    shorterString ( "hi" , "bye" ) = "X";//錯誤:返回值是個常量

    列表初始化返回值

    C++11新標準規定,函數可以返回花括號包圍的值的列表。類似于其他返回結果,此處的列表也用來對表示函數返回的臨時量進行初始化。如果列表為空,臨時量執行值初始化,否則,返回的值由函數的返回類型決定。

    舉個例子,回憶前文的error_msg函數,該函數的輸入是一組可變數量的string 實參,輸出由這些string對象組成的錯誤信息。在下面的函數中,我們返回一個vector對象,用它存放表示錯誤信息的string對象:

    vector<string> process() {// . . .// expected and actual are stringsif (expected.empty())return {}; // return an empty vector else if (expected == actual)return {"functionX", "okay"}; // return list-initialized vector else return {"functionX", expected, actual}; }

    第一條return語句返回一個空列表,此時,process 函數返回的vector對象是空的。如果expected不為空,根據expected和actual是否相等,函數返回的vector對象分別用兩個或三個元素初始化。

    如果函數返回的是內置類型,則花括號包圍的列表最多包含一個值,而且該值所占空間不應該大于目標類型的空間。如果函數返回的是類類型,由類本身定義初始值如何使用。

    主函數main的返回值

    之前介紹過,如果函數的返回類型不是void,那么它必須返回一個值。

    但是這條規則有個例外:我們允許main函數沒有return語句直接結束。如果控制到達了main函數的結尾處而且沒有return語句,編譯器將隱式地插入一條返回0的return語句。

    第1章介紹的,main函數的返回值可以看做是狀態指示器。返回0表示執行成功,返回其他值表示執行失敗,其中非0值的具體含義依機器而定。為了使返回值與機器無關,cstdlib頭文件定義了兩個預處理變量,我們可以使用這兩個變量分別表示成功與失敗:

    int main() {if (some_failure)return EXIT_FAILURE; // defined in cstdlib elsereturn EXIT_SUCCESS; // defined in cstdlib }

    因為它們是預處理變量,所以既不能在前面加上std::,也不能在using聲明中出現。

    遞歸

    如果一個函數調用了它自身,不管這種調用是直接的還是間接的,都稱該函數為遞歸函數(recursive function)。舉個例子,我們可以使用遞歸函數重新實現求階乘的功能:

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

    在上面的代碼中,我們遞歸地調用factorial 函數以求得從val中減去1后新數字的階乘。當val遞減到1時,遞歸終止,返回1。

    在遞歸函數中,一定有某條路徑是不包含遞歸調用的,否則,函數將“永遠”遞歸下去,換句話說,函數將不斷地調用它自身直到程序??臻g耗盡為止。我們有時候會說這種函數含有遞歸循環(recursion loop)。在factorial函數中,遞歸終止的條件是val等于1。

    下面的表格顯示了當給factorial函數傳入參數5時,函數的執行軌跡。

    調用返回值
    factorial(5)factorial(4) * 5120
    factorial(4)factorial(3) * 424
    factorial(3)factorial(2) * 36
    factorial(2)factorial(1) * 22
    factorial(1)11

    Note:main函數不能調用它自己。

    返回數組指針

    因為數組不能被拷貝,所以函數不能返回數組。不過,函數可以返回數組的指針或引用。雖然從語法上來說,要想定義一個返回數組的指針或引用的函數比較煩瑣,但是有一些方法可以簡化這一任務,其中最直接的方法是使用類型別名:

    typedef int arrT[10];// arrT是一個類型別名,它表示的類型是含有10個整數的數組 using arrT = int [10];// arrT的等價聲明 arrT* func(int i) ;// func返回一個指向含有10個整數的數組的指針

    其中 arrT是含有10個整數的數組的別名。因為我們無法返回數組,所以將返回類型定義成數組的指針。因此,func函數接受一個int實參,返回一個指向包含10個整數的數組的指針。

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

    要想在聲明func時不使用類型別名,我們必須牢記被定義的名字后面數組的維度:

    int arr[10] ;// arr是一個含有10個整數的數組 int *p1[10] ;//p1是一個含有10個指針的數組 int (*p2)[10] = &arr;// p2是一個指針,它指向含有10個整數的數組

    和這些聲明一樣,如果我們想定義一個返回數組指針的函數,則數組的維度必須跟在函數名字之后。然而,函數的形參列表也跟在函數名字后面且形參列表應該先于數組的維度。因此,返回數組指針的函數形式如下所示:

    Type (*function(parameter_list))[dimension]

    類似于其他數組的聲明,Type表示元素的類型,dimension表示數組的大小。(*function(parameter_list))兩端的括號必須存在,就像我們定義p2時兩端必須有括號一樣。如果沒有這對括號,函數的返回類型將是指針的數組。

    舉個具體點的例子,下面這個func函數的聲明沒有使用類型別名:

    int (*func(int i))[10];

    可以按照以下的順序來逐層理解該聲明的含義:

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

    • (*func(int i))意味著我們可以對函數調用的結果執行解引用操作。(*不是指針聲明符)

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

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

    使用尾置返回類型

    在C++11新標準中還有一種可以簡化上述func聲明的方法,就是使用尾置返回類型( trailing return type)。任何函數的定義都能使用尾置返回,但是這種形式對于返回類型比較復雜的函數最有效,比如返回類型是數組的指針或者數組的引用。尾置返回類型跟在形參列表后面并以一個->符號開頭。為了表示函數真正的返回類型跟在形參列表之后,我們在本應該出現返回類型的地方放置一個auto:

    //func接受一個int類型的實參,返回一個指針,該指針指向含有10個整數的數組 auto func(int i) -> int (*)[10];

    因為我們把函數的返回類型放在了形參列表之后,所以可以清楚地看到func函數返回的是一個指針,并且該指針指向了含有10個整數的數組。

    使用decltype

    還有一種情況,如果我們知道函數返回的指針將指向哪個數組,就可以使用decltype關鍵字聲明返回類型。例如,下面的函數返回一個指針,該指針根據參數i的不同指向兩個已知數組中的某一個:

    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;//返回一個指向數組的指針 }

    arrPtr使用關鍵字 decltype表示它的返回類型是個指針,并且該指針所指的對象與odd 的類型一致。因為 odd是數組,所以arrPtr返回一個指向含有5個整數的數組的指針。

    有一個地方需要注意:decltype并不負責把數組類型轉換成對應的指針,所以decltype的結果是個數組,要想表示arrPtr返回指針還必須在函數聲明時加一個*符號。

    函數重載

    如果同一作用域內的幾個函數名字相同但形參列表不同,我們稱之為重載(overloaded)函數。例如,上文中我們定義了幾個名為print的函數:

    void print(const char *cp); void print(const int *beg, const int *end); void print(const int ia[], size_t size);

    這些函數接受的形參類型不一樣,但是執行的操作非常類似。當調用這些函數時,編譯器會根據傳遞的實參類型推斷想要的是哪個函數:

    int j[2] = {0,1}; print("Hello World"); // calls print(const char*) print(j, end(j) - begin(j)); // calls print(const int*, size_t) print(begin(j), end(j)); // calls print(const int*, const int*)

    函數的名字僅僅是讓編譯器知道它調用的是哪個函數,而函數重載可以在一定程度上減輕程序員起名字、記名字的負擔。

    Note:main函數不能重載。

    定義重載函數

    有一種典型的數據庫應用,需要創建幾個不同的函數分別根據名字、電話、賬戶號碼等信息查找記錄。函數重載使得我們可以定義一組函數,它們的名字都是lookup,但是查找的依據不同。我們能通過以下形式中的任意一種調用lookup函數:

    Record lookup(const Account&); // find by Account Record lookup(const Phone&); // find by Phone Record lookup(const Name&); // find by Name Account acct; Phone phone; Record r1 = lookup(acct); // call version that takes an Account Record r2 = lookup(phone); // call version that takes a Phone

    其中,雖然我們定義的三個函數各不相同,但它們都有同一個名字。編譯器根據實參的類型確定應該調用哪一個函數。

    對于重載的函數來說,它們應該在形參數量或形參類型上有所不同。在上面的代碼中,雖然每個函數都只接受一個參數,但是參數的類型不同。

    不允許兩個函數除了返回類型外其他所有的要素都相同。假設有兩個函數,它們的形參列表一樣但是返回類型不同,則第二個函數的聲明是錯誤的:

    Record lookup(const Account&); 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 ignoredtypedef Phone Telno; Record lookup(const Phone&); Record lookup(const Telno&); // Telno and Phone are the same type
  • 第一對聲明中,第一個函數給它的形參起了名字,第二個函數沒有。形參的名字僅僅起到幫助記憶的作用,有沒有它并不影響形參列表的內容。

  • 第二對聲明看起來類型不同,但事實上Telno不是一種新類型,它只是 Phone的別名而已。類型別名為已存在的類型提供另外一個名字,它并不是創建新類型。因此,第二對中兩個形參的區別僅在于一個使用類型原來的名字,另一個使用它的別名,從本質上來說它們沒什么不同。

  • 重載和const形參

    前文介紹,頂層const(第2章)不影響傳入函數的對象。一個擁有頂層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不能轉換成其他類型(第4章“其他隱式類型轉換”節內容),所以我們只能把const對象(或指向const的指針)傳遞給const形參。

    相反的,因為非常量可以轉換成const,所以上面的4個函數都能作用于非常量對象或者指向非常量對象的指針。不過,接下來將要介紹的,當我們傳遞一個非常量對象或者指向非常量對象的指針時,編譯器會優先選用非常量版本的函數。

    建議:何時不應該重載函數

    盡管函數重載能在一定程度上減輕我們為函數起名字、記名字的負擔,但是最好只重載那些確實非常相似的操作。有些情況下,給函數起不同的名字能使得程序更易理解。舉個例子,下面是幾個負責移動屏幕光標的函數:

    Screen& moveHome(); screen& moveAbs(int, int); Screen& moveRel(int, int, string direction);

    乍看上去,似平可以把這組函數統一命名為move,從而實現函數的重載:

    Screen& move(); Screen& move(int, int); Screen& move(int, int, string direction);

    其實不然,重載之后這些函數失去了名字中本來擁有的信息。盡管這些函數確實都是在移動光標,但是具體移動的方式卻各不相同。以moveHome為例,它表示的是移動光標的一種特殊實例。

    一般來說,是否重載函數要看哪個更容易理解:

    //哪種形式更容易理解呢? myscreen.moveHome();//我們認為應該是這一個! myscreen.move();

    const_cast和重載

    const_cast為第4章內容。

    const_cast只能改變運算對象的底層const。

    const char *pc; char *p = const_cast<char*>(pc);// 正確:但是通過p 寫值是未定義的行為

    對于將常量對象轉換成非常量對象的行為,我們一般稱其為“去掉const性質(cast away the const)”。一旦我們去掉了某個對象的const性質,編譯器就不再阻止我們對該對象進行寫操作了。

    回憶上文的shorterString函數:

    // 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 的引用。我們可以對兩個非常量的string實參調用這個函數,但返回的結果仍然是const string 的引用。

    因此我們需要一種新的 shorterString函數,當它的實參不是常量時,得到的結果是一個普通的引用,使用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); }

    在這個版本的函數中,首先將它的實參強制轉換成對const 的引用,然后調用了shorterString函數的const版本。const版本返回對const string的引用,這個引用事實上綁定在了某個初始的非常量實參上。因此,我們可以再將其轉換回一個普通的string&,這顯然是安全的。

    (MyNote:因為const不能轉換成其他類型(第4章“其他隱式類型轉換”節內容),如果想修改函數返回的const字符串對象,顯然是不行的。)

    調用重載的函數

    定義了一組重載函數后,我們需要以合理的實參調用它們。函數匹配( function matching)是指一個過程,在這個過程中我們把函數調用與一組重載函數中的某一個關聯起來,函數匹配也叫做重載確定(overload resolution)。編譯器首先將調用的實參與重載集合中每一個函數的形參進行比較,然后根據比較的結果決定到底調用哪個函數。

    在很多(可能是大多數)情況下,程序員很容易判斷某次調用是否合法,以及當調用合法時應該調用哪個函數。通常,重載集中的函數區別明顯,它們要不然是參數的數量不同,要不就是參數類型毫無關系。

    此時,確定調用哪個函數比較容易。但是在另外一些情況下要想選擇函數就比較困難了,比如當兩個重載函數參數數量相同且參數類型可以相互轉換時(第4章“類型轉換”)。我們將在本章“函數匹配”節介紹當函數調用存在類型轉換時編譯器處理的方法。

    現在我們需要掌握的是,當調用重載函數時有三種可能的結果:

    • 編譯器找到一個與實參最佳匹配(best match)的函數,并生成調用該函數的代碼。
    • 找不到任何一個函數與調用的實參匹配,此時編譯器發出無匹配(no match)的錯誤信息。
    • 有多于一個函數可以匹配,但是每一個都不是明顯的最佳選擇。此時也將發生錯誤,稱為二義性調用(ambiguous call)。

    重載與作用域

    WARNING:一般來說,將函數聲明置于局部作用域內不是一個明智的選擇。但是為了說明作用域和重載的相互關系,我們將暫時違反這一原則而使用局部函數聲明。

    對于剛接觸C++的程序員來說,不太容易理清作用域和重載的關系。其實,重載對作用域的一般性質并沒有什么改變:如果我們在內層作用域中聲明名字,它將隱藏外層作用域中聲明的同名實體。在不同的作用域中無法重載函數名

    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 readstring s = read(); // error: read is a bool variable, not a function// bad practice: usually it's a bad idea to declare functions at local scopevoid print(int); // new scope: hides previous instances of printprint("Value: "); // error: print(const string &) is hiddenprint(ival); // ok: print(int) is visibleprint(3.14); // ok: calls print(int); print(double) is hidden }

    大多數讀者都能理解調用read函數會引發錯誤。因為當編譯器處理調用read的請求時,找到的是定義在局部作用域中的read。這個名字是個布爾變量,而我們顯然無法調用一個布爾值,因此該語句非法。

    調用print函數的過程非常相似。在fooBar內聲明的print(int)隱藏了之前兩個print函數,因此只有一個print函數是可用的:該函數以int值作為參數。

    當我們調用print函數時,編譯器首先尋找對該函數名的聲明,找到的是接受 int值的那個局部聲明。一旦在當前作用域中找到了所需的名字,編譯器就會忽略掉外層作用域中的同名實體。剩下的工作就是檢查函數調用是否有效了。

    Note:在C++語言中,名字查找發生在類型檢查之前。

    第一個調用傳入一個字符串字面值,但是當前作用域內print 函數唯一的聲明要求參數是int類型。字符串字面值無法轉換成int類型,所以這個調用是錯誤的。在外層作用域中的print (const string&)函數雖然與本次調用匹配,但是它已經被隱藏掉了,根本不會被考慮。

    當我們為print函數傳入一個double類型的值時,重復上述過程。編譯器在當前作用域內發現了print(int)函數,double類型的實參轉換成int類型,因此調用是合法的。

    假設我們把print(int)和其他print函數聲明放在同一個作用域中,則它將成為另一種重載形式。此時,因為編譯器能看到所有三個函數,上述調用的處理結果將完全不同:

    void print(const string &); void print(double); // overloads the print function void print(int); // another overloaded instance void fooBar2(int ival) {print("Value: "); // calls print(const string &)print(ival); // calls print(int)print(3.14); // calls print(double) }

    特殊用途語言特性

    默認實參

    某些函數有這樣一種形參,在函數的很多次調用中它們都被賦予一個相同的值,此時,我們把這個反復出現的值稱為函數的默認實參(default argument)。調用含有默認實參的函數時,可以包含該實參,也可以省略該實參。

    例如,我們使用string對象表示窗口的內容。一般情況下,我們希望該窗口的高、寬和背景字符都使用默認值。但是同時我們也應該允許用戶為這幾個參數自由指定與默認值不同的數值。為了使得窗口函數既能接納默認值,也能接受用戶指定的值,我們把它定義成如下的形式:

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

    其中我們為每一個形參都提供了默認實參,默認實參作為形參的初始值出現在形參列表中。我們可以為一個或多個形參定義默認值,不過需要注意的是,一旦某個形參被賦予了默認值,它后面的所有形參都必須有默認值。

    使用默認實參調用函數

    如果我們想使用默認實參,只要在調用函數的時候省略該實參就可以了。例如,screen 函數為它的所有形參都提供了默認實參,所以我們可以使用0、1、2或3個實參調用該函數:

    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,'#')

    函數調用時實參按其位置解析,默認實參負責填補函數調用缺少的尾部實參(靠右側位置)。例如,要想覆蓋backgrnd的默認值,必須為ht和wid提供實參:

    window = screen(, , '?'); // error: can omit only trailing arguments window = screen('?'); // calls screen('?',80,' ')

    需要注意,第二個調用傳遞一個字符值,是合法的調用。然而盡管如此,它的實際效果卻與書寫的意圖不符。

    該調用之所以合法是因為’?‘是個char,而函數最左側形參的類型string::size_type是一種無符號整數類型,所以char類型可以轉換成函數最左側形參的類型。當該調用發生時,char類型的實參隱式地轉換成string::size_type,然后作為height的值傳遞給函數。在我們的機器上,’?'對應的十六進制數是0x3F,也就是十進制數的63,所以該調用把值63傳給了形參height。

    當設計含有默認實參的函數時,其中一項任務是合理設置形參的順序,盡量讓不怎么使用默認值的形參出現在前面,而讓那些經常使用默認值的形參出現在后面。

    默認實參聲明

    對于函數的聲明來說,通常的習慣是將其放在頭文件中,并且一個函數只聲明一次,但是多次聲明同一個函數也是合法的。不過有一點需要注意,在給定的作用域中一個形參只能被賦予一次默認實參。換句話說,函數的后續聲明只能為之前那些沒有默認值的形參添加默認實參,而且該形參右側的所有形參必須都有默認值。假如給定

    //表示高度和寬度的形參沒有默認值 string screen(sz, sz, char = ' ');

    我們不能修改一個已經存在的默認值:

    string screen(sz, sz, char = '*"); //錯誤:重復聲明

    但是可以按照如下形式添加默認實參:

    string screen(sz = 24, sz = 80, char); //正確:添加默認實參

    Best Practices:通常,應該在函數聲明中指定默認實參,并將該聲明放在合適的頭文件中。

    默認實參初始值

    局部變量不能作為默認實參。除此之外,只要表達式的類型能轉換成形參所需的類型,該表達式就能作為默認實參:

    // 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 argumentsz wd = 100; // hides the outer definition of wd but does not change the default//這里wd是局部變量,上面的wd是全部變量。window = screen(); // calls screen(ht(), 80, '*') }

    我們在函數f2內部改變了def 的值,所以對screen的調用將會傳遞這個更新過的值。另一方面,雖然我們的函數還聲明了一個局部變量用于隱藏外層的 wd,但是該局部變量與傳遞給screen的默認實參沒有任何關系。

    內聯函數和constexpr函數

    上文我們編寫了一個小函數shorterString,它的功能是比較兩個string 形參的長度并返回長度較小的string的引用。把這種規模較小的操作定義成函數有很多好處,主要包括:

    • 閱讀和理解shorterString函數的調用要比讀懂等價的條件表達式容易得多。
    • 使用函數可以確保行為的統一,每次相關操作都能保證按照同樣的方式進行。
    • 如果我們需要修改計算過程,顯然修改函數要比先找到等價表達式所有出現的地方再逐一修改更容易。
    • 函數可以被其他應用重復利用,省去了程序員重新編寫的代價。

    然而,使用shorterstring 函數也存在一個潛在的缺點:調用函數一般比求等價表達式的值要慢一些。

    在大多數機器上,一次函數調用其實包含著一系列工作:

    • 調用前要先保存寄存器,并在返回時恢復;
    • 可能需要拷貝實參;
    • 程序轉向一個新的位置繼續執行。

    內聯函數可避免函數調用的開銷

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

    假設我們把shorterString函數定義成內聯函數,則如下調用

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

    將在編譯過程中展開成類似于下面的形式

    cout << (s1.size() < s2.size() ? s1 : s2) << endl;

    從而消除了shorterString函數的運行時開銷。

    在shorterString函數的返回類型前面加上關鍵字inline,這樣就可以將它聲明成內聯函數了:

    //內聯版本:尋找兩個string對象中較短的那個 inline const string &shorterstring(const string &s1,const string &s2){return s1.size() <= s2.size() ? s1 : s2; }

    Note:內聯說明只是向編譯器發出的一個請求,編譯器可以選擇忽略這個請求。

    一般來說,內聯機制用于優化規模較小、流程直接、頻繁調用的函數。很多編譯器都不支持內聯遞歸函數,而且一個75行的函數也不大可能在調用點內聯地展開。

    constexpr函數

    constexpr函數(constexpr function)是指能用于常量表達式(第2章內容)的函數。定義 constexpr函數的方法與其他函數類似,不過要遵循幾項約定:函數的返回類型及所有形參的類型都得是字面值類型(第2章內容),而且函數體中必須有且只有一條return語句:

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

    我們把new_sz定義成無參數的constexpr函數。因為編譯器能在程序編譯時驗證new_sz函數返回的是常量表達式,所以可以用new_sz函數初始化constexpr類型的變量foo。

    執行該初始化任務時,編譯器把對constexpr函數的調用替換成其結果值。為了能在編譯過程中隨時展開,constexpr函數被隱式地指定為內聯函數

    constexpr函數體內也可以包含其他語句,只要這些語句在運行時不執行任何操作就行。例如,constexpr函數中可以有空語句、類型別名以及using聲明。

    我們允許constexpr函數的返回值并非一個常量:

    //如果arg是常量表達式,則scale(arg)也是常量表達式 constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }

    當scale的實參是常量表達式時,它的返回值也是常量表達式;反之則不然:

    int arr[scale(2)]; //正確: scale(2)是常量表達式 int i = 2;//i不是常量表達式 ,const int i = 2;才是 int a2[scale(i)] ;//錯誤:scale (i)不是常量表達式

    如上例所示,當我們給scale函數傳入一個形如字面值2的常量表達式時,它的返回類型也是常量表達式。此時,編譯器用相應的結果值替換對scale函數的調用。

    如果我們用一個非常量表達式調用scale函數,比如int類型的對象i,則返回值是一個非常量表達式。當把 scale函數用在需要常量表達式的上下文中時,由編譯器負責檢查函數的結果是否符合要求。如果結果恰好不是常量表達式,編譯器將發出錯誤信息。

    Note:constexpr函數不一定返回常量表達式。

    把內聯函數和constexpr函數放在頭文件內

    和其他函數不一樣,內聯函數和 constexpr函數可以在程序中多次定義。畢竟,編譯器要想展開函數僅有函數聲明是不夠的,還需要函數的定義。不過,對于某個給定的內聯函數或者constexpr函數來說,它的多個定義必須完全一致?;谶@個原因,內聯函數和constexpr函數通常定義在頭文件中。

    調試幫助

    C++程序員有時會用到一種類似于頭文件保護(第2章有相關論述)的技術,以便有選擇地執行調試代碼。基本思想是,程序可以包含一些用于調試的代碼,但是這些代碼只在開發程序時使用。當應用程序編寫完成準備發布時,要先屏蔽掉調試代碼。這種方法用到兩項預處理功能:assert和 NDEBUG。

    assert預處理宏

    assert是一種預處理宏(preprocessor marco)。所謂預處理宏其實是一個預處理變量,它的行為有點類似于內聯函數。assert宏使用一個表達式作為它的條件:

    assert (expr) ;

    首先對expr求值,如果表達式為假(即 0),assert輸出信息并終止程序的執行。如果表達式為真(即非0),assert什么也不做。

    assert宏定義在cassert頭文件中。如我們所知,預處理名字由預處理器而非編譯器管理,因此我們可以直接使用預處理名字而無須提供using聲明。也就是說,我們應該使用assert而不是std::assert,也不需要為assert提供using聲明。

    和預處理變量一樣,宏名字在程序內必須唯一。含有cassert頭文件的程序不能再定義名為assert 的變量、函數或者其他實體。在實際編程過程中,即使我們沒有包含cassert頭文件,也最好不要為了其他目的使用assert。很多頭文件都包含了cassert,這就意味著即使你沒有直接包含 cassert,它也很有可能通過其他途徑包含在你的程序中。

    assert 宏常用于檢查“不能發生”的條件。例如,一個對輸入文本進行操作的程序可能要求所有給定單詞的長度都大于某個閾值。此時,程序可以包含一條如下所示的語句:

    assert(word.size() > threshold);

    NDEBUG預處理變量

    assert的行為依賴于一個名為NDEBUG 的預處理變量的狀態。如果定義了NDEBUG,則assert 什么也不做。默認狀態下沒有定義NDEBUG,此時 assert將執行運行時檢查。

    我們可以使用一個#define語句定義NDEBUG,從而關閉調試狀態。

    同時,很多編譯器都提供了一個命令行選項使我們可以定義預處理變量:

    $ cc -D NDEBUG main.c # use /D with the Microsoft compiler

    這條命令的作用等價于在main.c文件的一開始寫#define NDEBUG。

    定義NDEBUG能避免檢查各種條件所需的運行時開銷,當然此時根本就不會執行運行時檢查。因此,assert應該僅用于驗證那些確實不可能發生的事情。我們可以把assert當成調試程序的一種輔助手段,但是不能用它替代真正的運行時邏輯檢查,也不能替代程序本身應該包含的錯誤檢查

    除了用于assert外,也可以使用NDEBUG編寫自己的條件調試代碼。如果NDEBUG未定義,將執行#ifndef和#endif之間的代碼;如果定義了NDEBUG,這些代碼將被忽略掉:

    void print(const int ia[], size_t size) { #ifndef NDEBUG// _ _func_ _ is a local static defined by the compiler that holds the function's namecerr << __func__ << ": array size is " << size << endl; #endif // ...

    在這段代碼中,我們使用變量__func__輸出當前調試的函數的名字。編譯器為每個函數都定義了__func__,它是const char的一個靜態數組,用于存放函數的名字。

    除了C++編譯器定義的__func__之外,預處理器還定義了另外4個對于程序調試很有用的名字:

    • __FILE__存放文件名的字符串字面值。
    • __LINE__存放當前行號的整型字面值。
    • __TIMEB__存放文件編譯時間的字符串字面值。
    • __DATE__存放文件編譯日期的字符串字面值。

    可以使用這些常量在錯誤消息中提供更多信息:

    if (word.size() < threshold)cerr << "Error: " << _ _FILE_ _<< " : in function " << _ _func_ _<< " at line " << _ _LINE_ _ << endl<< " Compiled on " << _ _DATE_ _<< " at " << _ _TIME_ _ << endl<< " Word read was \"" << word<< "\": Length too short" << endl;

    如果我們給程序提供了一個長度小于threshold的string對象,將得到下面的錯誤消息:

    Error : wdebug.cc : in function main at line 27Compiled on Jul 11 2012 at 20:50:03Word read was "foo" : Length too short

    函數匹配

    例子:調用應該選用哪個重載函數

    在大多數情況下,我們容易確定某次調用應該選用哪個重載函數。然而,當幾個重載函數的形參數量相等以及某些形參的類型可以由其他類型轉換得來時,這項工作就不那么容易了。以下面這組函數及其調用為例:

    void f(); void f (int) ; void f (int, int) ; void f (double, double = 3.14); f (5.6);//調用void f (double, double)

    確定候選函數和可行函數

    函數匹配的第一步是選定本次調用對應的重載函數集,集合中的函數稱為候選函數(candidate function)。候選函數具備兩個特征:

  • 與被調用的函數同名,
  • 其聲明在調用點可見。
  • 在這個例子中,有4個名為f的候選函數。

    第二步考察本次調用提供的實參,然后從候選函數中選出能被這組實參調用的函數,這些新選出的函數稱為可行函數(viable function)??尚泻瘮狄灿袃蓚€特征:

  • 其形參數量與本次調用提供的實參數量相等,
  • 每個實參的類型與對應的形參類型相同,或者能轉換成形參的類型。
  • 我們能根據實參的數量從候選函數中排除掉兩個。不使用形參的函數和使用兩個int形參的函數顯然都不適合本次調用,這是因為我們的調用只提供了一個實參,而它們分別有0個和兩個形參。

    使用一個 int形參的函數和使用兩個double形參的函數是可行的,它們都能用一個實參調用。其中最后那個函數本應該接受兩個double值,但是因為它含有一個默認實參,所以只用一個實參也能調用它。

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

    在使用實參數量初步判別了候選函數后,接下來考察實參的類型是否與形參匹配。和一般的函數調用類似,實參與形參匹配的含義可能是它們具有相同的類型,也可能是實參類型和形參類型滿足轉換規則。在上面的例子中,剩下的兩個函數都是可行的:

    • f(int)是可行的,因為實參類型double 能轉換成形參類型int。

    • f(double,double)是可行的,因為它的第二個形參提供了默認值,而第一個形參的類型正好是 double,與函數使用的實參類型完全一致。

    Note:如果沒找到可行函數,編譯器將報告無匹配函數的錯誤。

    尋找最佳匹配(如果有的話)

    函數匹配的第三步是從可行函數中選擇與本次調用最匹配的函數。在這一過程中,逐一檢查函數調用提供的實參,尋找形參類型與實參類型最匹配的那個可行函數。下一節將介紹“最匹配”的細節,它的基本思想是,實參類型與形參類型越接近,它們匹配得越好。

    在我們的例子中,調用只提供了一個(顯式的)實參,它的類型是double。如果調用f(int) ,實參將不得不從double轉換成int。另一個可行函數f(double,double)則與實參精確匹配。精確匹配比需要類型轉換的匹配更好,因此,編譯器把f(5.6)解析成對含有兩個double形參的函數的調用,并使用默認值填補我們未提供的第二個實參。

    含有多個形參的函數匹配

    當實參的數量有兩個或更多時,函數匹配就比較復雜了。對于前面那些名為f的函數,我們來分析如下的調用會發生什么情況:

    (42,2.56);

    選擇可行函數的方法和只有一個實參時一樣,編譯器選擇那些形參數量滿足要求且實參類型和形參類型能夠匹配的函數。此例中,可行函數包括 f(int,int)和 f(double,double)。

    接下來,編譯器依次檢查每個實參以確定哪個函數是最佳匹配。如果有且只有一個函數滿足下列條件,則匹配成功:

    • 該函數每個實參的匹配都不劣于其他可行函數需要的匹配。The match for each argument is no worse than the match required by any other viable function

    • 至少有一個實參的匹配優于其他可行函數提供的匹配。There is at least one argument for which the match is better than the match provided by any other viable function

    如果在檢查了所有實參之后沒有任何一個函數脫穎而出,則該調用是錯誤的。編譯器將報告二義性調用的信息。

    在上面的調用中,只考慮第一個實參時我們發現函數f(int,int)能精確匹配;要想匹配第二個函數,int類型的實參必須轉換成double類型。顯然需要內置類型轉換的匹配劣于精確匹配,因此僅就第一個實參來說,f(int, int)比 f(double,double)更好。

    接著考慮第二個實參2.56,此時f(double,double)是精確匹配;要想調用f(int,int)必須將2.56從double類型轉換成int類型。因此僅就第二個實參來說,f(double,double)更好。

    (MyNote:公說公有理婆說婆有理。)

    編譯器最終將因為這個調用具有二義性而拒絕其請求:因為每個可行函數各自在一個實參上實現了更好的匹配,從整體上無法判斷孰優孰劣??雌饋砦覀兯坪蹩梢酝ㄟ^強制類型轉換其中的一個實參來實現函數的匹配,但是在設計良好的系統中,不應該對實參進行強制類型轉換。

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

    實參類型轉換

    為了確定最佳匹配,編譯器將實參類型到形參類型的轉換劃分成幾個等級,具體排序如下所示:

  • 精確匹配,包括以下情況:
    • 實參類型和形參類型相同。
    • 實參從數組類型或函數類型轉換成對應的指針類型。
    • 向實參添加頂層const或者從實參中刪除頂層const。
  • 通過const轉換實現的匹配。
  • 通過類型提升實現的匹配。
  • 通過算術類型轉換或指針轉換實現的匹配。
  • 通過類類型轉換實現的匹配(第14章內容)。
  • (2~4項為第4章“類型轉換”內容)

    需要類型提升和算術類型轉換的匹配

    WARNING:內置類型的提升和轉換可能在函數匹配時產生意想不到的結果,但幸運的是,在設計良好的系統中函數很少會含有與下面例子類似的形參。

    分析函數調用前,我們應該知道小整型一般都會提升到int類型或更大的整數類型。

    假設有兩個函數,一個接受int、另一個接受short,則只有當調用提供的是short類型的值時才會選擇short版本的函數。有時候,即使實參是一個很小的整數值,也會直接將它提升成int類型;此時使用short版本反而會導致類型轉換:

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

    所有算術類型轉換的級別都一樣。例如,從int向unsigned int 的轉換并不比從int向double的轉換級別高。舉個具體點的例子,考慮

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

    字面值3.14的類型是double,它既能轉換成long也能轉換成float。因為存在兩種可能的算數類型轉換,所以該調用具有二義性。

    函數匹配和const實參

    如果重載函數的區別在于它們的引用類型的形參是否引用了const,或者指針類型的形參是否指向const,則當調用發生時編譯器通過實參是否是常量來決定選擇哪個函數:

    Record lookup (Account&); //函數的參數是Account的引用 Record lookup(const Account& ); //函數的參數是一個常量引用 const Account a; Account b; lookup(a); //調用lookup (const Account&) lookup(b); //調用lookup(Account&)

    在第一個調用中,我們傳入的是const對象a。因為不能把普通引用綁定到const對象上,所以此例中唯一可行的函數是以常量引用作為形參的那個函數,并且調用該函數與實參a精確匹配。

    在第二個調用中,我們傳入的是非常量對象b。對于這個調用來說,兩個函數都是可行的,因為我們既可以使用b初始化常量引用也可以用它初始化非常量引用。然而,用非常量對象初始化常量引用需要類型轉換,接受非常量形參的版本則與b精確匹配。因此,應該選用非常量版本的函數。

    指針類型的形參也類似。如果兩個函數的唯一區別是它的指針形參指向常量或非常量,則編譯器能通過實參是否是常量決定選用哪個函數:

    • 如果實參是指向常量的指針,調用形參是const*的函數;
    • 如果實參是指向非常量的指針,調用形參是普通指針的函數。

    函數指針

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

    //比較兩個string對象的長度 bool lengthCompare (const string &, const string &);

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

    //pf指向一個函數,該函數的參數是兩個const string的引用,返回值是bool類型 bool (*pf)(const string &, const string &);//未初始化
    • 從我們聲明的名字開始觀察,pf前面有個*,因此pf是指針;
    • 右側是形參列表,表示pf指向的是函數;
    • 再觀察左側,發現函數的返回類型是布爾值。

    因此,pf 就是一個指向函數的指針,其中該函數的參數是兩個const string 的引用,返回值是bool類型。

    Note:*pf兩端的括號必不可少。如果不寫這對括號,則pf是一個返回值為bool指針的函數:

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

    使用函數指針

    當我們把函數名作為一個值使用時,該函數自動地轉換成指針。例如,按照如下形式我們可以將lengthCompare的地址賦給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 ff(int*); void ff(unsigned int) ; void (*pf1)(unsigned int) = ff; // pf1指向ff(unsigned)

    編譯器通過指針類型決定選用哪個函數,指針類型必須與重載函數中的某一個精確匹配

    void (*pf2)(int) = ff;//錯誤:沒有任何一個ff與該形參列表匹配* double (*pf3)(int*) = ff;//錯誤:ff和pf3的返回類型不匹配

    函數指針形參

    和數組類似,雖然不能定義函數類型的形參,但是形參可以是指向函數的指針。此時,形參看起來是函數類型,實際上卻是當成指針使用:

    //第三個形參是函數類型,它會自動地轉換成指向函數的指針 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);

    正如useBigger的聲明語句所示,直接使用函數指針類型顯得冗長而煩瑣。類型別名和 decltype(第2章內容)能讓我們簡化使用了函數指針的代碼:

    // 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;//等價的類型

    我們使用typedef定義自己的類型。Func和Func2是函數類型,而FuncP和 FuncP2是指針類型。需要注意的是,decltype返回函數類型,此時不會將函數類型自動轉換成指針類型。因為decltype的結果是函數類型,所以只有在結果前面加上*才能得到指針??梢允褂萌缦碌男问街匦侣暶鱱seBigger:

    // useBigger的等價聲明,其中使用了類型別名 void useBigger(const string&, const string&, Func); void useBigger(const string&, const string&, FuncP2);

    這兩個聲明語句聲明的是同一個函數,在第一條語句中,編譯器自動地將Func表示的函數類型轉換成指針。

    返回指向函數的指針

    和數組類似,雖然不能返回一個函數,但是能返回指向函數類型的指針。然而,我們必須把返回類型寫成指針形式,編譯器不會自動地將函數返回類型當成對應的指針類型處理。與往常一樣,要想聲明一個返回函數指針的函數,最簡單的辦法是使用類型別名:

    using F = int(int*, int); //F是函數類型,不是指針 using PF = int(*)(int*, int); //PF是指針類型

    其中我們使用類型別名將F定義成函數類型,將PF定義成指向函數類型的指針。必須時刻注意的是,和函數類型的形參不一樣,返回類型不會自動地轉換成指針。我們必須顯式地將返回類型指定為指針:

    PF f1(int); //正確:PF是指向函數的指針,f1返回指向函數的指針 F f1(int); //錯誤:F是函數類型,f1不能返回一個函數 F *f1(int); //正確:顯式地指定返回類型是指向函數的指針

    當然,我們也能用下面的形式直接聲明f1:

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

    按照由內向外的順序閱讀這條聲明語句:

  • 我們看到f1有形參列表,所以f1是個函數;
  • f1前面有*,所以f1返回一個指針;
  • 進一步觀察發現,指針的類型本身也包含形參列表,因此指針指向函數,該函數的返回類型是int。
  • 出于完整性的考慮,有必要提醒讀者我們還可以使用尾置返回類型的方式聲明一個返回函數指針的函數:

    auto f1(int) -> int(*) (int* , int) ;

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

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

    例如假定有兩個函數,它們的返回類型都是string::size_type,并且各有兩個const strings類型的形參,此時我們可以編寫第三個函數,它接受一個string類型的參數,返回一個指針,該指針指向前兩個函數中的一個:

    string::size_type sumLength(const string&, const string&); string::size_type largerLength(const string&, const string&);//根據其形參的取值,getFcn函數返回指向sumLength或者largerLength的指針 decltype(sumLength) *getFcn (const string &);

    聲明getFcn唯一需要注意的地方是,牢記當我們將decltype作用于某個函數時,它返回函數類型而非指針類型。因此,我們顯式地加上*以表明我們需要返回指針,而非函數本身。

    總結

    以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(6 / 19):函数的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。