Loading

《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. 内联函数:请求编译器在可能的情况下在调用点展开函数。内联函数可以避免常见的函数调用开销。
posted @ 2021-01-20 22:13  橘崽崽啊  阅读(137)  评论(0编辑  收藏  举报