天天看點

【C++Primer筆記】第六章 函數

文章目錄

    • 函數基礎
    • 參數傳遞
      • const形參和實參
      • 數組形參
      • 可變形參
    • 傳回類型
      • 傳回數組指針
    • 函數重載
      • 函數比對
    • 特殊用途語言特性
      • 預設實參
      • 内聯函數
      • constexpr函數
      • 調試幫助
    • 函數指針

函數基礎

  1. 實參傳給形參的規則參見用右值初始化左值。是以,(舉例)如果有一個形參為

    int

    ,則必須傳入一個能轉換成

    int

    的實參(整型、浮點型)。
  2. 在C++中,名字有作用域,對象有生命周期。一旦函數終止,形參就會被銷毀。函數内部的變量也必須被初始化,否則就會被預設初始化為未定義的值。
  3. 局部靜态對象(

    static

    )直到程式終止才會被銷毀。内置類型的局部靜态變量可以被隐式地初始化為0。
  4. 在函數的聲明中可以省略形參的名字。但還是建議把名字寫上,便于讀者了解形參含義。含有函數聲明的頭檔案應該被包括到函數的源檔案中。

參數傳遞

  1. 每次調用函數都會重新建立形參,并用實參初始化形參。當形參是引用類型時,它對應的實參被引用傳遞;當實參的值被拷貝給形參時,實參被值傳遞。
  2. void reset(int *ip)
    {
    	*ip = 0; //改變了ip所指對象的值
    	ip = 0; //并不改變指針ip的值
    }
    reset(&i); //調用
    //如果形參是引用,調用時就隻用寫reset(i)
               
    在C++中,建議使用引用類型的形參代替指針。
  3. 傳引用參數的作用:
    • 使用引用避免拷貝。當某種類型不支援拷貝操作、或需要拷貝的對象可能非常大(如

      string

      類型)時,應使用引用形參。如果無需改變實參内容,應使用

      const

    • 使用引用形參傳回額外資訊。有時函數需要多個傳回值,引用形參為我們一次傳回多個結果提供了有效的途徑(你懂的)。

const形參和實參

形參的初始化方式和變量的初始化方式是一樣的,回顧通用的初始化規則有助于了解本節知識。
  1. 當用實參初始化形參時,會忽略掉頂層

    const

    (作用于對象本身的常量屬性)。是以給形參添加頂層

    const

    屬性并不能構成函數重載(因為傳入這兩個函數的實參可以完全一樣:常量或非常量)。
  2. 可以使用非常量初始化底層

    const

    對象,但反之則不行(引用同理)。
  3. 一個普通的引用必須用同類型的對象初始化。不能把普通引用綁定到字面值上,不過常量引用可以(或者就一個

    string

    型)。
  4. 我們為什麼要把函數不會改變的形參定義為常量引用?除了提醒人們函數内不能改變形參,還有兩個更為隐蔽的原因:
    • 不能把

      const

      對象、字面值或需要類型轉換的對象傳給普通引用形參:
      string::size_type find_char(string &s, char c,
      					string::size_type &occurs);
      find_char("Hello world!", 'o', ctr); //錯誤!
                 
    • 如果其他函數正确地将其形參定義為常量引用,則(如find_char函數)無法在此類函數中正常使用:
      bool is_sentence(const stirng &s)
      {
      	string::size_type ctr = 0;
      	return find_char(s, '.', ctr) == s.size()-1 && ctr == 1; //錯誤!
      }
                 

數組形參

  1. void print(const int *);
    void print(const int[]);
    void print(const int[10]);
               
    上述三個函數聲明是等價的。編譯器隻檢查傳入的參數是否是

    const int*

    類型,而不關心數組的次元。數組的大小對函數的調用沒有影響!
  2. 確定數組不越界的三種技術:
    • **使用标記指定數組長度。**适用于C風格字元串(在最後一個字元後面跟着一個空字元)。例如:循環條件為指針不為空。
    • **使用标準庫規範。**傳遞指向數組首元素和尾元素的指針:
      void print(const int *beg, const int *end)
      {
      	while (beg != end)
      		cout << *beg++ << endl; //經典寫法
      }
      int j[2] = {0,1};
      print(begin(j), end(j)); //調用
                 
    • (回顧C程式的寫法)專門定義一個表示數組大小的形參。
    當函數不需要對數組元素執行寫操作時,數組形參應該是指向const的指針。
  3. 數組引用作為形參時,因為次元是類型的一部分,是以必須寫上。
    void print(int (&arr)[10]);
               
    這一用法限制了print函數的可用性:隻能傳遞次元為10的數組。
  4. 傳遞多元數組時,數組第二維(以及後面所有次元)的大小都是數組類型的一部分,不能省略。
    void print(int (*matrix)[10], int rowSize);
    void print(int matrix[][10], int rowSize);
    //matrix都是指向含有10個整數的數組的指針
               
  5. main

    函數是了解C++程式如何向函數傳遞數組的好例子:
    int main(int argc, char *argv[]) {...}
               
    argv[0]為空字元串或程式名字,非使用者輸入;最後一個指針之後的元素值保證為0(舉例:argv[5] = 0; 此時argc等于5)。

可變形參

  1. 如果函數的實參數量未知,但全部實參的數量都相同,可以使用

    initializer_list

    類型的形參(定義在同名頭檔案中,常用于輸出程式産生的錯誤資訊):
    void error_msg(initializer_list<string> i1)
    {
    	for (const auto &elem : i1) //用引用避免拷貝
    		cout << elem << endl;
    }
    //expected和actual是string對象
    if (expected != actual)
        error_msg({"functionX", expected, actual});
    //要有花括号來初始化initializer_list<string>
    else
        error_msg({"functionX", "okay"});
    /* i1.size()
     * i1.begin()
     * i1.end() */
               
  2. vector

    不一樣的是,

    initializer_list

    對象中的元素永遠是常量,我們無法改變它們。
  3. 省略符形參

    ...

    因該僅僅用于C和C++通用的類型。而且,大多數類型的對象在傳遞給省略符形參時,都無法正确拷貝(是以基本不要用)。

傳回類型

  1. 傳回

    void

    的函數不要求非得有

    return

    語句,因為在這類函數的最後一行會隐式地執行

    return

    。當然,

    void

    函數如果想在中間位置提前退出,可以使用

    return

  2. 對于有傳回值的函數,在含有

    return

    語句的循環後面應該也有一條

    return

    語句,沒有的話就是錯誤的;因為循環可能不被執行。
  3. **不要傳回局部對象的引用或指針!!!**要確定引用所引的是在函數之前就存在的對象。一旦函數完成,局部對象就會被釋放。
  4. 調用運算符的優先級和點、箭頭運算符相同,且都滿足左結合律。是以,形如下面的語句是合法的:
    auto sz = shorterString(s1, s2).size();
               
  5. 函數的傳回類型決定函數調用是否為左值。我們能為傳回類型是非常量引用的函數的結果指派。
  6. C++新标準允許清單初始化傳回值:
    vector<string> process()
    {
    	...
    	return {"stage1", "prepared"};
    }
               
    如果函數傳回内置類型,則花括号包圍的清單最多包含一個值。
  7. 如果控制到達了

    main

    函數的結尾且沒有

    return

    語句,編譯器将隐式地插入一條傳回0的

    return

    語句。
  8. 在遞歸調用中,一定有某條路徑是不包含遞歸調用的;否則,函數将不斷地調用它自身直到程式棧空間耗盡為止。

    main

    函數不能調用它自己喔。

傳回數組指針

  1. 因為數組不能被拷貝,是以函數不能傳回數組,但是函數能傳回數組的指針或引用。最直接的方法是使用類型别名:
    typedef int arr[10];
    //using arr = int[10]; 等價聲明
    arr* func(int i);
               
  2. 要想不使用類型别名,必須加上傳回類型的次元:
    int (*p)[10] = &arr; //arr是int[10]型
    int (*func(int i))[10];
    //(*func(int i))意味着我們可以對函數調用的結果解引用
    //(*func(int i))[10]表示解引用的結果是一個大小為10的數組
               
  3. C++新标準允許使用尾置傳回類型(适用于傳回類型較複雜的函數,如傳回類型是數組的指針或引用):
    //在本應出現傳回類型的地方放一個auto
    auto func(int i) -> int(*)[10];
               
  4. 如果我們知道函數傳回的指針指向哪個數組,可以使用

    decltype

    聲明傳回類型:
    int odd[] = {1,3,5,7,9};
    int even[] = {0,2,4,6,8};
    decltype(odd) *func(int i)
    {
    	return (i%2) ? &odd : &even;
    } //decltype(odd)是數組類型而不是數組指針,是以還要加上*
               
    當數組被用作

    decltype

    關鍵字的參數,或者作為取位址符

    &

    sizzeof

    等運算符的對象時,數組不會被隐式地轉換成指針。

函數重載

  1. 僅有傳回類型不同,不構成重載。

    main

    函數不能重載。
  2. 如const形參和實參一節所說,頂層

    const

    不影響傳入函數的對象,是以參數的頂層

    const

    屬性不能構成重載。另一方面,如果形參是某種類型的指針或引用,則通過區分其指向的是常量對象還是非常量對象可以實作重載。此時的

    const

    是底層的。
    Record lookup(Phone*);
    Record lookup(Phone* const); //不構成重載
    
    Record lookup(Phone*);
    Record lookup(const Phone*); //構成重載
               
    我們隻能把

    const

    對象或指向

    const

    的指針傳給

    const

    形參。而當我們傳遞一個非常量對象時,編譯器會優先使用非常量版本的函數。
  3. const_cast

    在函數重載中最有用:
    const string &shorertString(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);
    } //非常量版本函數中,可以調用常量版本,精簡代碼
               

    const

    版本傳回對

    const string

    的引用,但這個引用事實上綁定在了非常量實參上,是以我們可以再将其轉換為一個普通的

    string&

    ,這顯然是安全的。
  4. **在不同的作用域中無法重載函數名。**通常來說,在局部作用域中聲明函數不是一個好的選擇。

函數比對

  1. 候選函數:與被調用函數同名、其聲明在調用點可見。可行函數:形參數量等于實參且類型相同(或能轉換)。
  2. 尋找最佳比對:如果有一個函數,每個實參的比對都不劣于其他可行函數需要的比對,且至少有一個參數優于其他可行函數提供的比對,則比對成功;否則調用具有二義性,是錯誤的。
  3. 所有類型轉換的級别都一樣。隻有當調用提供

    short

    類型的值,才會選擇

    short

    版本的函數(否則選用

    int

    版本)。

特殊用途語言特性

預設實參

  1. 一旦某個形參被賦予了預設值,它後面的所有形參都必須有預設值。預設實參負責填補函數調用缺少的尾部實參。
  2. 當設計含有預設實參的函數時,要合理設定形參順序,盡量讓不怎麼使用預設值的形參出現在前面,而讓經常使用預設值的形參出現在後面。
  3. 通常,應該在函數聲明中指定預設形參,并将該聲明放在合适的預設形參中。函數的後續聲明隻能為之前沒有預設值的形參添加預設實參。
  4. 局部變量不能作為預設實參。除此之外,隻要表達式的類型能轉換成形參所需要的類型,該表達式就能作為預設形參:
int wd = 80;
string screen(int sz = wd);
void f2()
{
	wd = 100;
} 
//函數f2内改變了wd,是以對screen的調用會傳遞這個更新過的值
           

内聯函數

  1. 把規模較小的操作定義成函數,有利于確定行為的統一、便于修改、能夠被重複利用。
  2. 内聯函數(在傳回類型前加上

    inline

    )能夠在調用點上内聯地展開,避免函數調用的開銷。
  3. 内聯機制用于優化規模較小、流程直接、頻繁調用的函數(往往不支援遞歸)。内聯函數通常定義在頭檔案中。

constexpr函數

  1. constexpr

    函數(被隐式地指定為内聯函數)是指能用于常量表達式的函數:
    constexpr int new_sz() {return 42;}
    constexpr int foo = new_sz;
               
    規定:函數的傳回值類型、所有的形參類型必須是字面值類型;函數體中隻有一條

    return

    語句。
  2. 允許

    constexpr

    函數的傳回值并非一個常量。
    constexpr size_t scale(size_t cnt)
    {
    	return new_sz() * cnt;
    } //如果arg是常量表達式,scale(arg)也是常量表達式
    int arr[scale(2)]; //正确:scale(2)是常量表達式
    int i = 2;
    int arr[scale(i)]; //錯誤:scale(i)不是常量表達式
               

調試幫助

  1. assert

    是一種預處理宏。定義在

    cassert

    頭檔案中,無需為其提供

    using

    聲明。
    assert(expr);
    //如果表達式為假(即0),assert輸出資訊并終止程式運作
    //如果表達式為真,assert什麼也不做
               

    assert

    宏常用于檢查“不能發生”的條件。
  2. 如果在

    main.c

    檔案的一開始寫

    #define NDEBUG

    ,可以關閉調試狀态;此時

    assert

    将不執行運作時檢查。此外,

    NDEBUG

    也可用于編寫自己的調試代碼:
    void print()
    {
    #ifdef NDEBUG
    	cerr << __func__ << endl;
    #endif
    }
               

    __func__

    是編譯器定義的一個局部靜态變量,用于存放函數的名字。除此之外,預處理器還定義了4個對程式調試很有幫助的名字:
    • __FILE__

      存放檔案名的字元串字面值
    • __LINE__

      存放目前行号的整形字面值
    • __TIME__

      存放檔案編譯時間的字元串字面值
    • __DATE__

      存放檔案編譯日期的字元串字面值
    if (word.size() < threshold)
    	cerr << "Error:" << __FILE__
    	     << "in function " << __func__
    	     << "at line " << __LINE__ << endl
    	     << "Compiled on " << __DATE__
    	     << "at " << __TIME__ << endl;
               

函數指針

  1. 函數指針指向的是函數而非對象。函數指針指向某種特定類型。函數的類型由它的傳回類型和形參的類型共同決定,與函數名無關。
    bool (*pf)(const string &, const string &);
    //聲明函數指針,隻需要将原本函數名的地方換成指針
               
  2. 當我們把函數名當一個值使用時,該函數自動地轉換為指針。此外,我們還能直接使用指向函數的指針調用函數,而無需解引用。
    pf = lengthCompare;
    bool b3 = pf("hello", "goodbye");
               
    在指向不同函數類型的指針之間,不能互相轉換。不過,我們還是能為函數指針賦

    nullptr

  3. 當我們使用重載函數時,編譯器通過函數指針類型判斷選用哪個函數(如:初始化函數指針時)。
  4. 形參可以是指向函數的指針。雖然看起來是函數類型,實際上卻當成指針使用。
    //直接寫
    void useBigger(const string &s1, const string &s2,
    			   bool pf(const string &, 
    			           const string &));
    //使用類型别名
    type bool Func(const string &, const string &);
    void useBigger(const string &s1, const string &s2,
    			   Func);
               
  5. 和數組類似,雖然不能傳回一個函數,但是能傳回指向函數的指針。最簡單的辦法依舊是使用類型别名:
    using func = int(int *, int); //func是函數類型
    using pfunc = int(*)(int *, int); //pfunc是指針類型
    func *f1(int);
    pfunc f1(int);
    decltype(sumLength) *getFcn(const string &);
    //sumaLength是一個函數的名字
               
    和函數形參不一樣,函數傳回值不會自動轉換為指針,我們需要顯式地将傳回類型定為指針。當然,還可以使用尾置傳回類型的方式。

繼續閱讀