第 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);//也可以使用尾置返回类型的方式,这种看起来舒服一些
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,809评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,189评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,290评论 0 359
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,399评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,425评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,116评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,710评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,629评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,155评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,261评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,399评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,068评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,758评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,252评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,381评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,747评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,402评论 2 358

推荐阅读更多精彩内容

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