《C++ Primer》筆記 第6章 函數

  1. 任意兩個形參都不能同名,並且函數最外層做用域中的局部變量也不能使用與函數形參同樣的名字(形參就至關於該函數的局部變量)。
  2. 形參名是可選的,可是因爲咱們沒法使用未命名的形參,因此形參通常都應該有個名字。某類形參一般不命名以表示在函數體內不會使用它。無論怎樣,是否設置未命名的形參並不影響調用時提供的實參數量。即便某個形參不被函數使用,也必須爲它提供一個實參。
  3. 在C++語言中,名字有做用域,對象有生命週期。
    • 名字的做用域是程序文本的一部分,名字在其中可見。
    • 對象的生命週期是程序執行過程當中該對象存在的一段時間。
  4. 一個語句塊構成一個新的做用域。
  5. 形參和函數體內部定義的變量統稱爲局部變量,其僅在該函數的做用域內可見,同時局部變量還會隱藏在外層做用域中同名的其餘全部聲明。
  6. 咱們把只存在於執行期間的對象稱爲自動對象。當塊的執行結束後,塊中建立的自動對象的值就變成未定義的了。
  7. 局部靜態對象在程序的執行路徑第一次通過對象定義語句時初始化,而且直到程序終止才被銷燬,在此期間即便對象所在的函數結束執行也不會對它有影響。
  8. 若是局部靜態變量沒有顯式的初始值,它將執行值初始化,內置類型的局部靜態變量初始化爲0。
  9. 若是函數無需改變引用形參的值,最好將其聲明爲常量引用。
  10. 和其餘初始化過程同樣,當用實參初始化形參時會忽略掉頂層const(從調用的角度來看)。換句話說,形參的頂層const被忽略掉了。當形參有頂層const時,傳給它常量對象或者很是量對象都是能夠的。注意:
    void fcn (const int i) { /* fcn 可以讀取i,可是不能向i寫值 */ }
      void fcn (int i) { /*...*/ } // 錯誤:重複定義了fcn (int)
      // 在C++語言中,容許咱們定義若干具備相同名字的函數,不過前提是不一樣函數的形參列表應該有明顯的區別。由於頂層const被忽略掉了,因此在上面的代碼中傳入兩個fcn函數的參數能夠徹底同樣。所以第二個fcn是錯誤的,儘管形式上有差別,但實際上它的形參和第一個fcn的形參沒什麼不一樣。
  11. 數組形參:
    // 儘管形式不一樣,但這三個print函數是等價的
      // 每一個函數都有一個const int*類型的形參
      void print (const int*);
      void print (const int[]);
      void print (const int[10]); // 這裏的維度沒有用。
  12. 給數組形參指定長度:
    // 1. 使用標記指定數組長度:
      void print (const char *cp) // C風格字符串以'\0'結尾
      {
            if (cp)
                  while (cp)
                        cout << *cp++;
      }
    
      // 2. 使用標準庫規範:
      void print (const int *beg, const int *end) // 傳遞指向數組首元素和尾後元素的指針
      {
            // 輸出beg到end之間(不含end)的全部元素
            while (beg != end)
                  cout << *beg++ << endl; // 輸出當前元素並將指針向前移動一個位置
      }
    
      // 3. 顯式傳遞一個表示數組大小的形參:
      void print (const int ia[], size_t size) // 專門定義一個表示數組大小的形參
      {
            for (size_t i = 0; i != size; ++i)
            {
                  cout << ia[i] << endl;
            }
      }
  13. 當函數不須要對數組元素執行寫操做的時候,數組形參應該是指向const的指針。只有當函數確實要改變元素值的時候,才把形參定義成指向很是量的指針。
  14. 形參也能夠是數組的引用。
    // 正確:形參是數組的引用,維度是類型的一部分
      void print (int (&arr)[10]) // 這裏的維度是必須的 // &arr兩端的括號必不可少
      {
            for (auto elem : arr)
                  cout << elem << endl;
      }
  15. 多維數組的形參:
    // 二者等價
      void print (int (*matrix)[10], int rowSize) { /*...*/ }
      void print (int matrix[][10], int rowSize) { /*...*/ }
  16. main:處理命令行選項 int main (int argc, char *argv[]) { ... }。最後一個指針以後的元素值保證爲0。
  17. 若是函數的實參數量未知可是所有實參的類型都相同,咱們可使用initializer_list類型的形參,initializer_list對象中的元素永遠是常量值,咱們沒法改變initializer_list對象中元素的值:
    void error_msg (ErrCode e, initializer_list<string> il)
      {
            cout << e.msg() << ": ";
            for (const auto &elem : il) // 由於initializer_list包含begin和end成員,因此咱們可使用範圍for循環處理其中的元素。也能夠寫成for (auto beg = il.begin(); beg != il.end(); ++beg)
                  cout << elem << " ";
            cout << endl;
      }
    
      if (expected != actual)
            error_msg (ErrCode(42), {"functionX", expected, actual}); // expected和actual是string對象
      else
            error_msg (ErrCode(0), {"functionX", "okay"});
操做 解釋
initializer_list lst; 默認初始化:T類型元素的空列表
initializer_list lst{a, b, c...}; lst的元素數量和初始值同樣多;lst的元素是對應初始值的副本;列表中的元素是const
lst2 (lst)或lst2 = lst 拷貝或賦值一個initializer_list對象不會拷貝列表的元素;拷貝後,原始列表和副本共享元素(畢竟是常量值)
lst.size() 列表中的元素數量
lst.begin() 返回指向lst中首元素的指針
lst.end() 返回指向lst中尾元素下一位置的指針
  1. 省略符形參(C語言特性,非C++):大多數類類型的對象在傳遞給省略符形參時都沒法正確拷貝。void foo (parm_list, ...);void foo (...);第一種形式指定了foo函數的部分形參的類型,對應於這些形參的實參將會執行正常的類型檢查。省略符形參所對應的實參無需類型檢查。在第一種形式中,形參聲明後面的逗號是可選的。
  2. 一個返回類型是void的函數也能使用return語句的第二種形式(return expression;),不過此時return語句的expression必須是另外一個返回void的函數。強行令void函數返回其它類型的表達式將產生編譯錯誤。
  3. 在含有return語句的循環後面應該也有一條return語句,若是沒有的話該程序就是錯誤的。控制流可能還沒有返回任何值就結束了函數的執行。不少編譯器都沒法發現此類錯誤。
  4. const string &shorterString (const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; }其中形參和返回類型都是const string的引用,無論是調用函數仍是返回結果都不會真正拷貝string對象。
  5. 不要返回局部對象的引用或指針(返回時局部變量可能已銷燬)。要想確保返回值安全,咱們不妨提問:引用所引的是在函數以前已經存在的哪一個的對象?
  6. 調用一個返回引用的函數獲得左值,其餘返回類型獲得右值。能夠像使用其它左值那樣來使用返回引用的函數的調用,特別是,咱們能爲返回類型是很是量引用的函數的結果賦值。
  7. 列表初始化返回值:若是函數返回的是內置類型,則花括號包圍的列表最多包含一個值,並且該值所佔空間不該該大於目標類型的空間(因此int a = {3.14};是錯的)。若是函數返回的是類類型,由類自己定義初始值如何使用(能夠看做用花括號內容初始化一個待返回的臨時變量。該變量類型就是返回值類型)。
    vector<string> process()
      {
            // ...
            // expected和actual是string對象
            if (expected.empty())
                  return {}; // 返回一個空vector對象
            else if (expected == actual)
                  return {"functionX", "okay"}; // 返回列表初始化的vector對象
            else
                  return {"functionX", expected, actual};
      }
    
      // 猜猜func的返回值會是什麼?
      vector<string> func(void)
      {
            return {10, "hi"};
      }
  8. 若是控制到達了main函數的結尾處並且沒有return語句,編譯器將隱式地插入一條返回0的return語句。
  9. 聲明一個返回數組指針的函數:
    // 1. C風格
      int (*func(int i))[10];
      // 2. 使用尾置返回類型
      auto func(int i) -> int(*)[10]
      // 3. 使用decltype
      int odd[] = {1,3,5,7,9};
      int even[] = {0,2,4,6,8};
      decltype(odd) *arrPtr(int i) // decltype並不負責把數組類型轉換成對應的指針,因此decltype的結果是個數組
      {
            return (i % 2) ? &odd : &even; // 返回一個指向數組的指針
      }
  10. main函數不能重載
  11. 對於重載的函數來講,它們應該在形參數量或形參類型上有所不一樣(省略形參名字或僅僅改變變量名不算)。不容許兩個函數除了返回類型外其餘全部的要素都相同。一個擁有頂層const的形參沒法和另外一個沒有頂層const的形參區分開來(底層const能夠)。
  12. 當咱們傳遞一個很是量對象或者指向很是量對象的指針時,編譯器會優先選用很是量版本的函數。
  13. 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);
      }
  14. 函數匹配是指一個過程,在這個過程當中咱們把函數調用與一組重載函數中的某一個關聯起來,函數匹配也叫作重載肯定
  15. 當調用重載函數時有三種可能的結果:
    • 編譯器找到一個與實參最佳匹配的函數,並生成調用該函數的代碼。
    • 找不到任何一個函數與調用的實參匹配,此時編譯器發出無匹配的錯誤信息。
    • 有多於一個函數能夠匹配,可是每個都不是明顯的最佳選擇。此時也將發生錯誤,稱爲二義性調用
  16. 若是咱們在內層做用域中聲明名字,它將隱藏外層做用域中聲明的同名實體(不管是函數名,仍是變量名)。在不一樣的做用域中沒法重載函數名(內層函數會隱藏同名的外層函數)。
  17. 在C++語言中,名字查找發生在類型檢查以前。
  18. 函數聲明放在同一個做用域中,則它將成爲另外一種重載形式。
  19. 默認實參:一旦某個形參被賦予了默認值,它後面的全部形參都必須有默認值。合理設置形參的順序,儘可能讓不怎麼使用默認值的形參出如今前面,而讓那些常用默認值的形參出如今後面。
    typedef string::size_type sz;
      string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
  20. 若是咱們想使用默認實參,只要在調用函數的時候省略該實參就能夠了。函數調用時實參按其位置解析,默認實參負責填補函數調用缺乏的尾部實參(靠右側位置)。
  21. 屢次聲明同一個函數也是合法的。在給定的做用域中一個形參只能被賦予一次默認實參。函數的後續聲明只能爲以前那些沒有默認值的形參添加默認實參,並且該形參右側的全部形參必須都有默認值。
    typedef string::size_type sz;
      string screen(sz, sz, char = ' ');
      string screen(sz, sz, char = '*'); // 錯誤:重複聲明
      string screen(sz = 24, sz = 80, char); // 正確:添加默認實參 // 默認實參不在形參列表的結尾
      // 第一句和第三句若是調換順序則錯誤
      // 若是第三句再加上char = ' '就是重定義
      // VS2019報錯但能夠運行
  22. 函數定義和函數聲明不能同時指定默認實參。一般,應該在函數聲明中指定默認實參,並將該聲明放在合適的頭文件中。
  23. 局部變量不能做爲默認實參。除此以外,只要表達式的類型能轉換成形參所需的類型,該表達式就能做爲默認實參。
    // wd、def和ht的聲明必須出如今函數以外
      typedef string::size_type sz;
      sz wd = 80;
      char def = ' ';
      sz ht();
      string screen(sz = ht(), sz = wd, char = def);
      string window = screen(); // 調用screen(ht(), 80, ' ')
  24. 用做默認實參的名字在函數聲明所在的做用域內解析,而這些名字的求值過程發生在函數調用時。
    void f2()
      {
            def = '*'; // 改變默認實參的值
            sz wd = 100; // 隱藏了外層定義的wd,可是沒有改變默認值
            window = screen(); // 調用screen(ht(), 80, '*')
      }
  25. 內聯說明(inline)只是向編譯器發出的一個請求,編譯器能夠選擇忽略這個請求。通常來講,內聯機制用於優化規模較小、流程直接、頻繁調用的函數。不少編譯器都不支持內聯遞歸函數,並且一個75行的函數也不大可能在調用點內聯地展開。
  26. constexpr函數定義的幾項約定:函數的返回類型及全部形參的類型都得是字面值類型;並且函數體中必須有且只有一條return語句。
    constexpr int new_sz() { return 42; }
      constexpr int foo = new_sz(); // 正確:foo是一個常量表達式
  27. 爲了能在編譯過程當中隨時展開,constexpr函數被隱式地指定爲內聯函數。
  28. constexpr函數體內也能夠包含其餘語句,只要這些語句在運行時不執行任何操做就行。例如,constexpr函數中能夠有空語句、類型別名以及using聲明。咱們容許constexpr函數的返回值並不是一個常量,當scale的實參是常量表達式時,它的返回值也是常量表達式;反之則否則。(記住constexpr在編譯階段得出計算結果就行)constexpr函數不必定返回常量表達式
    // 若是arg是常量表達式,則scale(arg)也是常量表達式
      constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
      
      int arr[scale(2)]; // 正確:scale(2)是常量表達式
      int i=2; // i不是常量表達式
      int a2[scale(i)]; // 錯誤:scale(i)不是常量表達式
  29. 內聯函數和constexpr函數能夠在程序中屢次定義,但它的多個定義必須徹底一致。基於這個緣由,內聯函數和constexpr函數一般定義在頭文件中。
  30. assert是一種預處理宏assert(expr);。首先對expr求值,若是表達式爲假(即0),assert輸出信息並終止程序的執行。若是表達式爲真(即非0),assert什麼也不作。assert宏定義在cassert頭文件中。
  31. 咱們可使用一個#define語句定義NDEBUG(必定要定義在#include <cassert>以前),從而關閉調試狀態。(命令行:CC -D NDEBUG main.C至關於在main.C文件的一開始寫#define NDEBUG
  32. 調試幫助:
    #ifndef NDEBUG
            // __func__是編譯器定義的一個局部靜態變量,用於存放函數的名字
            cerr << __func__ << ": array size is " << size << endl;
      #endif
名稱 解釋
__func__ 輸出當前調試的函數的名字
__FILE__ 存放文件名的字符串字面值
__LINE__ 存放當前行號的整型字面值
__TIME__ 存放文件編譯時間的字符串字面值
__DATE__ 存放文件編譯日期的字符串字面值
  1. 函數匹配的第一步是選定本次調用對應的重載函數集,集合中的函數稱爲候選函數。候選函數具有兩個特徵:一是與被調用的函數同名,二是其聲明在調用點可見。
  2. 第二步考察本次調用提供的實參,而後從候選函數中選出能被這組實參調用的函數,這些新選出的函數稱爲可行函數。可行函數也有兩個特徵:一是其形參數量與本次調用提供的實參數量相等,二是每一個實參的類型與對應的形參類型相同,或者能轉換成形參的類型。(若是函數含有默認實參,則咱們在調用該函數時傳入的實參數量可能少於它實際使用的實參數量。)
  3. 接下來,編譯器依次檢查每一個實參以肯定哪一個函數是最佳匹配。若是有且只有一個函數知足如下列條件,則匹配成功:
    • 該函數每一個實參的匹配都不劣於其餘可行函數須要的匹配。
    • 至少有一個實參的匹配優於其餘可行函數提供的匹配。
  4. 調用重載函數時應儘可能避免強制類型轉換。若是在實際應用中確實須要強制類型轉換,則說明咱們設計的形參集合不合理。
  5. 實參類型轉換:
    • 精確匹配
      • 實參類型和形參類型相同
      • 實參從數組類型或函數類型轉換成對應的指針類型
      • 向實參添加頂層const或者從實參中刪除頂層const
    • 經過const轉換實現的匹配
    • 經過類型提高實現的匹配
    • 經過算術類型轉換或指針轉換實現的匹配
    • 經過類類型轉換實現的匹配。
  6. 分析函數調用前,咱們應該知道小整型通常都會提高到int類型或更大的整數類型。有時候,即便實參是一個很小的整數值,也會直接將它提高成int類型。
    void ff(int);
      void ff(short);
      ff('a'); // char提高成int;調用ff(int);
  7. 全部算術類型轉換的級別都同樣。例如:從int向unsigned int的轉換並不比從int向double的轉換級別高。例如:
    void manip(long);
      void manip(float);
      manip(3.14); // 錯誤:二義性調用
      // 字面值3.14的類型是double,它既能轉換成long也能轉換成float。由於存在兩種可能的算術類型轉換,因此該調用具備二義性。
  8. 函數的類型由它的返回類型形參類型共同決定,與函數名無關。
  9. 當咱們把函數名做爲一個值使用時,該函數自動地轉換成指針。此外,咱們還能直接使用指向函數的指針調用該函數,無須提早解引用指針。注意,在指向不一樣函數類型的指針間不存在轉換規則。可是和往常同樣,咱們能夠爲函數指針賦一個nullptr或者值爲0的整型常量表達式,表示該指針沒有指向任何一個函數。
    // pf指向一個函數,該函數的參數是兩個const string的引用,返回值是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; // 錯誤:返回類型不匹配
  10. 編譯器經過指針類型決定選用哪一個函數,指針類型必須與重載函數中的某一個精確匹配
  11. 和數組相似,雖然不能定義函數類型的形參,可是形參能夠是指向函數的指針。此時,形參看起來是函數類型,實際上確是當成指針使用。咱們能夠直接把函數做爲實參使用,此時它會自動轉換成指針。
    // 第三個形參是函數類型,它會自動的轉換成指向函數的指針
      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);
  12. 類型別名和decltype能讓咱們簡化使用了函數指針的代碼。decltype返回函數類型,此時不會將函數類型自動轉換成指針類型。
    // 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; // 等價的類型
    
      // useBigger的等價聲明,其中使用了類型別名。這兩個聲明語句聲明的是同一個函數。
      void useBigger(const string&, const string&, Func); // 編譯器自動地將Func表示的函數類型轉換成指針
      void useBigger(const string&, const string&, FuncP2);
  13. 和數組相似,雖然不能返回一個函數,可是能返回指向函數類型的指針。然而,咱們必須把返回類型寫成指針形式,編譯器不會自動地將函數返回類型當成對應的指針類型處理。
    using F = int(int*, int); // F是函數類型,不是指針
      using PF = int(*)(int*, int); // PF是指針類型
      
      // 1. 使用類型別名聲明f1
      PF f1(int); // 正確:PF是指向函數的指針,f1返回指向函數的指針
      F f1(int); // 錯誤:F是函數類型,f1不能返回一個函數
      F *f1(int); // 正確:顯示地指定返回類型是指向函數的指針
    
      // 2. 直接聲明f1
      int (*f1(int))(int*, int); 
    
      // 3. 使用尾置返回類型的方式聲明f1
      auto f1(int) -> int (*)(int*, int); 
    
      // 4. 使用auto和decltype用於函數指針類型
      string::size_type sumLength(const string&, const string&);
      string::size_type largerLength(const string&, const string&);
      // 牢記當咱們將decltype做用於某個函數時,它返回函數類型而非指針類型。
      decltype(sumLength) *getFcn(const string &); // 使用decltype(sumLength)和decltype(largerLength)同樣
  14. 實參(argument),形參(parameter)
  15. 隱藏名字:某個做用域內聲明的名字會隱藏掉外層做用域中聲明的同名實體(不只限於變量的名字)。
  16. 內聯函數:請求編譯器在可能的狀況下在調用點展開函數。內聯函數能夠避免常見的函數調用開銷。
相關文章
相關標籤/搜索