第 6 章:函数

  1. 一些情况下函数有个别形参不会被用到,此类形参通常不命名以表示在函数体内不会使用它。但是即便是这样,调用此函数的时候,该未命名的形参也需要提供一个实参,以契合参数表的参数个数和类型。

  2. 只存在于块执行期间的对象被称为自动对象,当块的执行结束后,块中创建的自动对象的值就变成未定义的了。所以形参是一种自动对象,函数开始的时候给形参申请空间,并用实参来初始化,在函数终止时形参也被销毁。

  3. 局部静态变量是指在块内用 static 关键字修饰的局部变量。它们在第一次初始化之后生效,并且在块结束之后依旧生效,直到程序运行结束才被销毁。它们的初始化语句只被执行一次,在下次函数被调用的时候该语句失效。局部静态变量一般要求显式初始化,当没有初始值的时候会被值初始化:int 就会初始化为 0,其他的类型是类自己决定的,

  4. 传入实参给函数的时候,函数会把接收到的实参拷贝一份赋给形参,然后在函数体内使用形参,最后销毁形参。如果实参是复杂类型或体积较大的时候,则最好将形参声明为引用,因为这样可以避免拷贝动作。如果在函数内不会修改实参的值,那还可以进一步声明为常量引用:

    bool isShorter(const string &s1, const string &s2)
    {
        return s1.size() < s2.size();// string 有可能会很长,所以用常量引用
    }
    
  5. 可以通过向函数传入指针形参的方式来在函数内通过解引用指针来修改被指对象,其效果与把对象的引用传入函数一样,但是在 C++ 中推荐使用引用形参来代替指针形参。

  6. 巧妙使用引用形参可以使函数“返回”不止一个值:把需要额外返回的数据通过引用来记录。这在很多内置函数中都用到了。

    string::size_type findFirstOccurrence(const string &s, char c, 
                                           string::size_type &occurs)
    {
        auto ret = s.size();
        occurs = 0;
        for (decltype(ret) i = 0; i != s.size(); ++i) {
            if (s[i] == c) {
                if (ret == s.size()) ret = i;
                ++occurs; // occurs记录出现次数,相当于隐式的多返回了一个变量
            }
        }
        return ret; // 如果出现过,那么返回第一次的位置;否则返回尾后位置
    }
    string s("ultrasound cleaner");
    string::size_type occurTimes = 0;
    auto firstOccurrence = findFirstOccurrence(s, 'a', occurTimes);
    // firstOccurrence = 4, occurTimes = 2
    
  7. 顶层 const 不影响传入函数的对象,因此当形参是顶层 const 的时候,你可以传常量的实参也可以传非常量的。因此,如果你重载了一个非顶层 const 形参版本的函数,是不行的,因为这两种本质上是同一种:

    void fcn(const int i)
    {
        cout << i << endl;
        i = i + 1;        //❌在函数内修改了常量
    }
    void fcn(int i)       //❌重复定义了 fcn()
    {
        cout << i+1 << endl;
    }
    const int cp = 3;
    int p = 1;
    fcn(p);    //👌
    fcn(cp);   //👌
    

    但是底层 const 就不同了。底层 const 形参可以用来区别传入的实参是否是常量:

    void fn(int &i)
    {
        cout << i << endl;
        i = i + 1;
    }
    void fn(const int &i) //这是两个函数,可以重载
    {
        cout << i << endl;
        //由于是常量引用,那么形参是底层 const,那么实参也一定是底层,所以不能修改 i 的值
    }
    const int cp = 3;
    int p = 1;
    fn(p);    //👌输出 1,p=2
    fn(cp);   //👌输出 3
    

    常量不可以转为非常量,但是非常量是可以转为常量的。当传入的实参是非常量的对象,或是非常量对象的指针的时候,编译器会优先选择哪个版本的函数呢?答案是非常量版本的函数。

  8. 可以使用 initializer_list<T> 来代替形参列表的一部分,以达到传入形参数量可变的目的,但是 initializer_list<T> 中的形参都是同一个 T 类型,而且都是常量。可以把 initializer_list<T> 看作一个 vector<T>

    void error_msg(ErrCode e, initializer_list<string> l)
    {
        cout << e.msg() << ": ";
        for (const auto &elem : l)
            cout << elem << " ";
        cout << endl;
    }
    string expected, actual;
    //...
    if (expected != actual)
        error_msg(ErrCode(42), {"functionX", expected, actual});
    else
        error_msg(ErrCode(0), {"functionX", "OK"});
    

    在使用 initializer_list<T> 传参的时候用大括号把要穿的同一类型的实参包括起来。

  9. C++11 标准新规定,函数可以返回花括号包围的值的列表。例如函数的返回类型是 vector<int>,那么可以 return {}return {0, 1, 3}

  10. cstdlib 头文件定义了两个预处理变量 EXIT_FAILUREEXIT_SUCCESS,可以用于返回 main 函数。

  11. 返回数组指针的函数的声明方式有四种:返回类型别名、使用规定的语法、使用尾置返回类型和使用 decltype。具体看书 205 页。

  12. 在调用函数的时候,编译器的第一个工作是在当前作用域下找对该函数名的声明,如果找到的话就会把外层作用域中的所有同名实体都忽略掉,如果找不到就去外层找。第二个工作才是检查函数的形参类型和实参类型是否匹配。注意:先查找名字,后检查类型。

  13. 函数匹配的第一步是选定本次调用对应的重载函数集。集合中的函数称为候选函数。候选函数具备两个特征:一是于被调用的函数同名,二是其声明在调用点可见。选定重载函数集,就对应着上一条所说的编译器的第一个工作。

  14. 第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。可行函数也有两个特征:以是器形参数量和本次调用提供的实参数量相同,二是每个实参的类型与对应的形参类型相同,或是实参类型能够转换为相应的形参类型。如果没找到可行函数,编译器将报告无匹配函数的错误。如果可行函数不止一个,那么接下来在可行函数中选择与本次调用最匹配的函数。在这一工程中,注意检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数,参数类型越接近,匹配得越好。这些对应着上一条所说的编译器的第二个工作。

  15. 为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级:

    1. 精确匹配,包括以下情况:
      1.1 实参类型与形参类型完全相同
      1.2 实参从数组类型或函数类型,转换成对应的指针类型
      1.3 向实参添加顶层 const 或者删除实参的顶层 const
    2. 通过将非常量类型的指针、引用转换成常量类型实现的匹配
    3. 通过类型提升实现的匹配 (详见书 P142 和第四章的总结)
    4. 通过算数类型转换或指针转换实现的匹配
    5. 通过类类型转换实现的匹配
      越往上越优先匹配,如果匹配到两个同一级别的函数,那么就会产生二义性问题。
  16. 所有算数类型转换的级别都一样高。所以从 double 转为 float 和从 double 转为 long 的级别是一样高的。当有两个函数,一个接受 float,一个接受 long,但是你传入 double 的时候,就会产生二义性问题。

  17. 函数可以有默认实参,在声明的时候可以给相应的形参提供默认实参。但是注意,提供了默认实参的形参后面的所有形参都必须有默认实参。在声明了一个带有默认实参的函数之后,如果要重载另一个版本的同样带有其他默认实参的函数,那么要注意,之前被赋予了默认实参的形参在之后就不能再重复被赋予新的默认实参了,只能给这个形参前面的其他形参赋予默认值,同时省略之后的所有默认实参。
    调用的时候,需要使用默认值的形参不提供实参,同时该形参之后的所有形参均不提供实参。所以在设计有默认实参的函数的时候,要注意设计一下形参列表的顺序,使得所有有默认实参的形参均分布在列表的末尾,而且满足调用的逻辑。

  18. 通过在函数返回类型之前加上 inline 关键字,可以将函数指定为内联函数,这样在函数调用语句的部分,在编译过程中会自动替换成具体的函数语句:

    inline const string &shorterString(const string &s1, const string &s2)
    {
        return s1.size() < s2.size() ? s1 : s2;
    }
    //调用的时候
    cout << shorterString(s1, s2) << endl;
    //等同于
    cout << (s1.size() < s2.size() ? s1 : s2) << endl;
    

    这么写可以节省一些调用函数时的开销。但是注意,这种声明方式仅适用于简短、直接、被频繁调用的函数,而且编译器有权忽略这个函数的内联属性。

  19. 之前讲到的 constexpr 函数就被隐式地指定为内联函数,因为它的内容足够简单能够在编译时展开。之前说 constexpr 函数内部只能有一条语句,其实是只能有一条实际执行操作的语句。可以有空语句、类型别名以及 using 声明,但是谁没事干往 constexpr 函数里头塞这写东西呢?constexpr 函数也被用作返回常量表达式,但是不是说它只能返回常量表达式:

    constexpr int new_sz()
    {
        return 42;
    }
    constexpr size_t scale(size_t cnt)
    {
        return new_sz() * cnt;
    }
    //调用的时候
    int arr[scale(2)]; //👌返回常量表达式
    int i = 2;
    int a2[scale(i)];  //❌返回一个非常量表达式
    

    内联函数和 constexpr 函数通常定义在头文件中。

  20. assert 是一种预处理宏,在 <cassert> 头文件中。在由于被预处理器管理,所以和 NULL (<cstdlib>) 一样不需要加 std:: 或者 using 声明。 assert(expr) 接受一个 expr 表达式,当表达式的运算结果是 false 的时候 assert 输出信息并种植程序的执行。否则,assert 什么都不做。assert 常用于在重要执行语句之前检查绝对不能发生的情况。和预处理变量一样,宏名字在程序内必须唯一。所以正确的做法是把 assert 当作一个关键字,不要在自己的程序中声明其他名为 assert 的变量或函数。

  21. assert 的行为依赖一个名为 NDEBUG 的预处理变量的状态。如果定义了 NDEBUGassert 什么都不做。默认状态下 NDEBUG 不被定义,所以此时 assert 会执行运行时检查。所以我们可以搭配 #define#ifdef#ifndef#endif 这些技巧来控制我们自己的程序流。

  22. __func__ 用于存放当前作用域内的函数的名字,__FILE__ 用于存放文件名的字符串字面值,__TIME__ 用于存放当前行号的整形字面值,__DATE__ 用于存放文件编译时期的字符串字面值。可以用这些编译器定义的局部静态变量来在错误处理中提供很多有用的信息。

  23. 函数指针是个指针,它指向一个函数。使用方法如下:

    int power(int base, int p)
    {
        int ret = 1;
        for (; p>0; p--) ret *= base;
        return ret;
    }
    
    void nothing(int base, int p)
    {
        cout << base << endl;
        cout << p << endl;
    }
    
    int fact(int base)
    {
        if (base < 0) return -1;
        if (base == 0 || base == 1) return 1;
        for (int b = base; b > 0; b--) base *= b;
        return base;
    }
    
    int (*pf)(int, int);//声明,pf两侧括号必不可少
    pf = power;//定义
    pf = &power;//这样也行
    void (*pn)(int, int) = nothing;//声明同时定义
    
    //下面这三个调用等价
    int res1 = pf(3,4);
    int res2 = (*pf)(3,4);
    int res3 = power(3,4);
    
    pf = fact;//❌形参列表不匹配
    pf = nothing;//❌返回类型不匹配
    

    函数指针明确了函数的形参类型和返回类型,缺一不可。

  24. 可以把函数指针当作形参,以达到在函数内部调用其他函数的目的:

    void nothing(int base, int p)
    {
        cout << base << endl;
        cout << p << endl;
    }
    
    int power(int base, int p, void output(int, int))//传入一个函数指针
    {
        output(base, p);
        int ret = 1;
        for (; p>0; p--) ret *= base;
        return ret;
    }
    
    int fact(int base, void (*output)(int, int));//另一种传函数指针的方法
    
    int res = power(3,4, nothing);
    
  25. 可以使用类型别名来简化函数指针的使用:

    int power(int base, int p)
    {
        cout << "in power" << endl;
        int ret = 1;
        for (; p>0; p--) ret *= base;
        return ret;
    }
    
    typedef int (*pPower)(int, int);
    typedef decltype(power) *pPower;//和上一句等价,但是推荐这种写法
    
    void anoPower(int base, int p, void output(int, int, pPower))//传入一个函数指针
    {
        cout << "in anoPower" << endl;
        output(base, p, power);
    }
    
    int fact(int base)
    {
        cout << "in fact" << endl;
        if (base < 0) return -1;
        if (base == 0 || base == 1) return 1;
        for (int b = base; b > 0; b--) base *= b;
        return base;
    }
    
    typedef decltype(fact) *pFact;
    
    void nothing(int base, int p, Power ptr)
    {
        cout << "in nothing" << endl;
        cout << base << endl;
        cout << p << endl;
        cout << ptr(base, p) << endl;
    }
    
    int main()
    {
        nothing(3, 4, power);
        cout << "$$$$$$$$" << endl;
        pFact ptr = fact;
        cout << ptr(5) << endl;
        cout << "$$$$$$$$" << endl;
        anoPower(5,3,nothing);
        return 0;
    }
    

    程序的输出是:
    in nothing
    3
    4
    in power
    81
    $$$$$$$$
    in fact
    600
    $$$$$$$$
    in anoPower
    in nothing
    5
    3
    in power
    125

    通过看函数的输出就能明白调用顺序。

  26. 一个函数还可以返回一个函数指针:

    using PF = int(*)(int*, int);//PF是一个函数指针类型
    PF func(int); //声明一个函数func,它接受一个int作为形参,返回一个PF类型的函数指针
    
    //fun和 func 一样,但是你会发现返回的函数指针类型的形参列表放在了fun的列表之后
    int (*fun(int)) (int*, int);//这个语法看起来挺晦涩的,还是推荐上面那种类型别名的方法
    
    auto fu(int) -> int (*)(int*, int);//也可以使用尾置返回类型的方式,这种看起来舒服一些
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • #1.函数基础1.1 局部对象1.2 函数声明1.3 分离式编译 #2.参数传递2.1 传值参数2.2 传引用参数...
    MrDecoder阅读 655评论 0 1
  • 6.1 函数基础 6.1.1 局部对象 函数参数:实参是函数中形参的初始值,存在对应关系,但并没有规定实参的求值顺...
    咸鱼翻身ing阅读 302评论 0 0
  • https://blog.csdn.net/u011185231/article/details/51591571...
    燕京博士阅读 696评论 0 0
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,618评论 1 51
  • 题目类型 a.C++与C差异(1-18) 1.C和C++中struct有什么区别? C没有Protection行为...
    阿面a阅读 7,873评论 0 10

友情链接更多精彩内容