C++ 學習筆記之(6)-函數、重載和指針

C++ 學習筆記之(6)-函數、重載和指針

函數基礎

函數定義包括以下幾個部分

返回類型、函數名字、由0個或多個形參組成的列表以及函數體

局部對象

C++語言中,名字有作用域,對象有生命週期

  • 名字的作用域是程序文本的一部分,名字在其中可見
  • 對象的生命週期是程序執行過程中該對象存在的一段時間

自動對象

存在於塊執行期間的對象成爲自動對象,即執行到變量定義時創建對象,快末尾銷燬它。比如形參等。

局部靜態對象

在程序的執行路徑第一次經過對象定義語句時初始化,並且知道程序終止才被銷燬,即使所在的函數執行結束。

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

函數聲明

  • 函數原型:即函數聲明,包含返回類型,函數名和形參類型。

  • 函數的聲明和函數的定義類似,區別是聲明無需函數體,用一個分號替代即可

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

參數傳遞

  • 形參的初始化方式和變量的初始化方式一樣

  • 引用傳遞:形參是引用類型,也被叫做傳引用調用,即引用形參是對應實參的別名

  • 值傳遞:當實參的值拷貝給形參是,形參和實參是兩個獨立對象,也被稱爲傳值調用

傳值參數

  • 指針形參:指針與非引用類型一樣,執行拷貝操作時,拷貝的是指針的值。兩個指針是不同的指針,但是由於指針可以間接訪問所指對象,所以指向的是統一內容

    void reset(int *ip)
    {
      *ip = 0; // 改變指針 ip 所指對象的值
        ip = 0;  // 只改變了 ip 的局部拷貝,實參未被改變
    }

 傳引用參數

  • 對於引用的操作實際上是作用在引用所引的對象上

    void reset(int &i)
    {
      i = 0;  // 改變了 i 所引對象的值
    }
  • 使用引用避免拷貝

  • 如果函數無需改變引用形參的值,最好將其聲明爲常量引用

const 形參和實參

  • 實參初始化形參時會忽略掉頂層const,即形參的頂層const被忽略

    void fcn(const int i) { /* fcn 能夠讀取 i, 但是不能向 i 寫值 */ }
    void fcn(int i) {/* ... */}  // 錯誤:重複定義 fcn(int)
  • C++語言允許函數名字相同,但前提是形參列表應該有明顯區別,由於頂層const被忽略,故上述代碼中傳入兩個函數的參數可以完全一樣,故爲重定義

  • 非常量能夠初始化底層const對象,反之不行

  • 字面值能夠初始化常量引用

  • 函數形參儘量使用常量引用

    • 常量:避免函數內修改值,同時可以傳入普通參數和常量參數
    • 引用:避免函數對參數進行拷貝,提高效率

知識點

數組形參

數組的兩個性質

  • 不允許拷貝數組, 故無法值傳遞
  • 使用數組時通常會將其轉換成指針,故實際上傳遞給函數的是指向數組首元素的指針
// 下面三個函數等價,每個函數形參都是 const int*
void print(const int*);
void print(const int[]);  // 可以看出,函數的意圖是作用於一個數組
void print(const int[10]);  // 維度表示期望數組含有多少元素,但實際不一定

含有可變形參的函數

C++11新標準提供了兩個方法處理不同數量實參的函數

  • 若所有實參類型相同,可以傳遞一個名爲initializer_list的標準庫類型
  • 若實參不同,則可以拜年可變參數模板(16.4節介紹)

initializer_list

若函數的實參數量未知但全部實參類型相同,則可以使用initializer_list類型的形參, initializer_list是一種標準庫類型,用於表示某種特定類型的值的數組。其定義在同名頭文件中

initializer_list_operations_methods

  • 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;
    }
    // 調用語句
    // expected, actual 是 string 對象
    if (expected != actual)
        error_msg({"functionX", expected, actual});
    else
        error_msg({"functionX", "okay"});

省略符形參

省略符形參是爲了便於C++程序訪問某些特殊的C代碼設置的,這些代碼使用了varargs的C標準庫功能。

  • 省略符形參只能出現在形參列表的最後一個位置,形式有兩種

    // 第一種指定了部分形參的類型,對於這部分參數將進行正常的類型檢查,省略符形參所對應的實參無需類型檢查
    void foo(parm_list, ...);  
    void foo(...);
進階

返回類型和return語句

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

return;
return expression;

無返回值函數

無返回值的return語句用在返回類型是void的函數中,此類函數最後會隱式執行return

有返回值函數

return語句返回值的類型必須與函數的返回類型相同,或能隱式轉換成函數的額返回類型

  • 返回值的方式和初始化一個變量或形參的方式完全一樣

  • 不要反悔局部對象的引用或指針:因爲函數完成後,其存儲空間被釋放,局部變量的引用指向的內存無效

  • 調用返回引用的函數得到左值,其他返回類型得到右值

  • 列表初始化返回值

    vector<string> process() { return {"return", "example"}};

返回數組指針

函數不能返回數組,所以可以返回數組指針或引用

  • Type (*function (parameter_list)) [dimension]

    // 例子,使用類型別名
    typedef int arrT[10];  // arrT爲類型別名,表示類型是含有 10 個整數的數組
    using arrT = int[10];  // arrT的等價聲明
    arrT* func(int i); // func 返回一個指向含有 10 個整數的數組的指針
    
    // 例子,沒有使用類型別名
    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 類型
  • 使用尾置返回類型

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

    int odd[] = {1, 3, 5, 7, 9};
    decltype(odd) *arrPtr(int i);  /* 返回一個指向含有 5 個整數的數組的指針,注意 decltype 的結果是數組,不會把數組類型轉換成對應的指針,故需要加 * 符號 */

函數重載

若同一作用域內的幾個函數名字相同但形參列表不同,即爲重載函數

  • 頂層const形參等價於無頂層const形參

    int lookup(int i);
    int lookup(const int i);  // 重複聲明 int lookup(int); 頂層const
    
    int lookup(int *pi);
    int lookup(int * const pi);  // 重複聲明 int lookup(int *); 頂層const
  • 底層const可以實現函數重載

    int lookup(int &);  // 函數作用域 int 引用
    int lookup(const int&);  // 新函數,作用域常量引用
    
    int lookup(int *);  // 新函數,作用於指向 int 的指針
    int lookup(const int *);  // 新函數,作用於指向常量的指針
  • const_cast和重載

    // 參數和返回類型都是 const string 的引用
    const string &shorterString(const string &s1, const string &s2)
    {
      return s1.size() <= s2.size() ? s1 : s2;
    }
    // 返回類型是普通引用, 參數爲普通變量
    string &shorterString(string &s1, string &s2)
    {
        // 將普通引用轉換成對對const的引用,然後調用const版本,返回對const的引用,如果不轉換成const,則會根據非常量參數調用非const版本,直至報錯
      auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
        // 將const引用轉成普通引用返回
        return const_cast<string&>(r);
    }

特殊用途語言特性

默認實參

  • 某個形參被賦予默認值,則其後面的所有形參都必須有默認值

  • 在給定作用域中,一個形參只能被賦予一次默認實參

    typedef string::size_type sz;
    string screen(sz, sz, char = ' ');
    string screen(sz, sz, char = '*'); // 錯誤:重複聲明
    string screen(sz = 24, sz = 80, char); // 正確:添加默認實參
  • 通常,應該在函數聲明中指定默認實參,並將該聲明放在合適的頭文件中。

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

內聯函數就和constexpr函數

  • 內聯函數inline, 即函數在調用點 內聯地展開

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

  • 很多編譯器不支持內聯遞歸函數

  • constexpr函數:能用於常量表達式的函數。 函數的返回類型及所有形參的類型都是字面值類型,函數體中必須有且只有一條 return 語句

    constexpr int new_sz() { return 42; }  // new_sz() 爲無參數的 constexpr 函數
  • constexpr函數被隱式地指定爲內聯函數

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

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

    // 若參數 arg 爲常量表達式,則 scale(arg) 也是常量表達式
    constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
  • 內聯函數和constexpr函數可以在程序中多次定義,爲了多個定義保持完全一致,通常定義在頭文件內

調試幫助

  • assert預處理宏:一個預處理變量,類似於內聯函數,由預處理器而非編譯器管理

    assert(expr);  // 對 exp r求值,若表達式爲假(即 0 ),assert輸出信息並終止運行;若爲真,則什麼也不做
  • NDEBUG:預處理變量,assert的行爲依賴於次變量的狀態, 若定義了NDEBUG, 則assert什麼也不錯,相當於關閉調試。默認沒有定義,此時assert會執行運行時檢查

    void print(const int ia[], size_t size)
    {
    
    # ifndef NDEBUG
    
        cerr << __func__ << endl  // 當前調試函數名
            << __FILE__ << endl   // 存放文件名的字符串字面值
            << __LINE__ << endl   // 存放當前行號的整型字面值
            << __TIME__ << endl   // 存放文件編譯時間的字符串字面值
            << __DATE__ << endl;  // 存放文件編譯時期的字符串字面值
    }

函數匹配

  • 候選函數:調用對應的重載函數集,有兩個特徵
    • 與被調用函數同名
    • 其聲明在調用點可見
  • 可行函數:通過考察調用提供的實參,從候選函數中選出能被這組實參調用的函數,也有兩個特徵
    • 其形參數量與本次調用提供的實參數量相等
    • 每個實參類型與對應形參類型相同,或能轉換成形參類型
  • 實參類型與形參類型能夠越接近,匹配的越好
  • 若有多個形參匹配,編譯器會一次檢查每個實參以確定最佳匹配函數,若未找到,則會報告二義性錯誤
    • 該函數每個實參的匹配都不劣於其他可行函數需要的匹配
    • 至少有一個實參的匹配由於其他可行函數提供的匹配

實參類型轉換

爲了確定最佳匹配,編譯器將實參類型到形參類型的轉換分成等級,如下所示

function_real_arguments_cast

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

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

    void ff(int);
    void ff(short);
    ff('a');  // char 提升成 int; 調用 f(int)
  • 所有算數類型轉換的級別都一樣

    void manip(long);
    void manip(float);
    manip(3.14);  // 錯誤:二義性調用,因爲double可以轉換爲long 和 float

函數匹配和const形參

若重載函數的區別在於用用類型的形參是否引用了const,或指針類型的形參是否執行const,則編譯器通過實參是否爲常量決定函數選擇

int lookup(int &);  // 參數爲 int 引用
int lookup(const int &);  // 參數爲常量引用
const int a;
int b;
lookup(a);  // 調用 lookup(const int &)
lookup(b);  // 調用 lookup(int &)

函數指針

函數指針即指向函數的指針,函數指針也是指向某種特定類型。函數類型由其返回類型和形參類型決定,與函數名無關。

// 函數類型是 bool (const string&, const string &)
bool lengthCompare(const string &, const string &); 
// 聲明指向該函數的指針,只需要用指針替換函數名即可
// pf 指向一個函數,該函數的參數爲兩個 const string引用,返回值是 bool 類型
bool (*pf)(const string &, const string &);  // 未初始化
  • 函數名作爲值使用時,自動轉換爲指針

    pf = lengthCompare;  // 等價於 pf = &lengthCompare; 取地址符可選
  • 可直接使用函數指針調用函數,無需解引用

    bool b1 = pf("hello", "goodbye");  // 調用 lengthCompare 函數
    bool b2 = (*pf)("hello", "goodbye");  // 等價於上個調用語句
  • 函數指針不存在類型轉換,可爲函數指針賦值nullptr0,表示函數指針不指向任何函數

  • 函數指針可做爲形參,可以使用類型別名簡化形參

    // 第三個形參是函數類型,它會自動轉換成指向函數的指針
    void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
    // 等價聲明,顯示地將形參定義成指向函數的指針
    void useBigger(const string &s1, const string &s2, book (*pf)(const string &, const string &));
    // 自動將函數 lengthCompare 轉換成指向該函數的指針
    useBigger(s1, s2, lengthCompare);
  • 函數能返回函數指針,與形參不同,編譯器不會自動將函數返回類型轉換成對應的指針類型。

    using F = int(int *, int);  // F 是函數類型,不是指針
    using PF = int(*)(int *, int);  // PF 是指針類型
    PF f1(int);  // 正確:PF爲函數指針,f1 返回函數指針
    F f1(int);  // 錯誤:F是函數類型,f1不能返回一個函數
    int (*f1(int))(int *, int);  // 等價於PF f1(int);
    auto f1(int) -> int(*)(int *, int);  // 使用尾置返回類型方式聲明返回函數指針的函數

結語

  • 函數是命名了的計算單元,每個函數都包括返回類型、名字、(可能爲空的)形參列表以及函數體
  • C++中,函數可以被重載:同一個敏子可用於定義多個函數,只要形參數量或形參類型不同即可。

函數相關基礎知識已經瞭解,以後要深入學習函數相關知識。

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