• 第6章 函数


    第6章 函数


     

     


    6.1 函数基础

    1. 一个函数包括以下部分:
      函数结构

    2. 函数在被调用时首先(隐式地)定义并初始化它的形参,其实这个过程就是一个值初始化的过程,所以之前对于 值初始化或 auto初始化的规则一样有效。**这里要注意一点,即 C++并没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值。**所以形如下式的表达式是错误的!
      int a = fun(i, ++i); // 错误:传递进来的实参不能对其它实参有副作用!

    3. 函数的返回类型不能是数组或函数类型,但可以是指向数组或函数的指针。

    4. 局部静态对象在程序执行路径第一次经过对象定义语句时初始化,直到程序终止才被销毁;如果局部静态变量没有显式的初始值,执行值初始化,内置类型被初始化为 0。

    5. 另外,为了确保同一函数在不同使用该函数的地方保持一致,并且将接口和实现分离开来,C++通常会将函数声明放到头文件(.h),实现放到源文件(.cpp)中。这样,使用和修改函数接口都会很方便。

    6. C++支持分离式编译,对每个 源文件(.cpp)独立编译。这样如果我们修改了其中一个源文件,那么只需要重新编译那个改动了的文件。之后编译器会将对象文件(.obj)链接到一起,形成可执行文件(.exe)。整体过程如下
      分离式编译.png
      这样的话,如果在头文件中实现了某个函数,而该函数又被多个源文件使用,那么在编译时正常,而在链接时就会报错,某些函数多次重复定义。这是因为每个源文件都会对自己使用的函数进行编译,编译后的 .obj中已经包括了该函数的定义,而在后续多个 .obj文件链接时,才发现这个函数被多次定义了。解决办法就是在 .h文件中仅包含函数声明,函数实现放到 .cpp文件中去。


    6.2 参数传递

    1. 形参初始化的机理与变量初始化一样。包括引用传递和值传递,其中指针参数也是值传递,进行的是指针的值的拷贝。拷贝之后,两个指针是不同的指针,只是它们都指向都一个对象。

    2. 使用引用传递可以避免拷贝,效率较高;另外,有些类型(IO操作)不支持拷贝,只能通过引用形参访问该类型的对象。

    3. C++中一个函数只能返回一个值,而当函数需要返回多个值时,可以通过引用和指针形参来完成。这样的话,输入参数在函数执行完毕后也会被改变,也就相当于是一个输出参数了。当然,还可以通过自定义一个数据类型或使用 tuple模板来返回多个值。

    4. 与变量初始化一样,参数初始化时,会忽略掉顶层 const。因此对下式传给它常量对象或者非常量对象都是可以的。
      int a = fcn(const int i); // fcn能够读取 i,但是不能修改 i的值
      另外,因为忽略掉顶层 const的缘故,顶层 const并不会引起函数重载,而是函数重定义!

    void fcn(const int i);
    void fcn(int i);  // 错误,函数重定义!
    
    1. 尽量使用常量引用,表示该函数不会改变该形参。因为将函数定义成普通引用有以下缺点:

      • 非常量引用只能接受非常量对象,不能把 const对象、字面值传递给这种形参。
      • 在含有常量引用形参的函数中,无法将常量引用传递给非常量引用的函数,从而限制了后者的适用范围。此时需要使用 const_cast来转换底层 const属性。
      • 给函数的调用者以误导,使用者可能会以为函数可以修改它的实参的值。
    2. 数组不允许拷贝,所以无法以值传递的形式传递数组参数;使用数组时通常会将其转换成指针,所以当为函数传递一个数组参数时,实际传递的是指向数组首元素的指针。数组的大小对函数的调用没有影响。

    // 尽管形式不同,但三个 print函数是等价的,每个形参都是 const int*类型
    void print(const int *);
    void print(const int[]);    // 此函数的意图是作用于一个数组
    void print(const int[10]);  // 这个维度表示我们期望的输入数组有多少个元素,实际并不一定!
    int i = 0, j[2] = {0, 1};
    print(&i);                  // 正确,即使参数只是一个单独的 int类型
    print(j);                   // 正确
    
    1. 对于数组引用形参,因为维度是数组类型的一部分,所以声明数组引用形参时必须指定数组的维度,也只能将函数应用于指定大小的数组。
    // 形参是数组的常量引用,维度是类型的一部分
    void print(const int (&arr) [10]);
    int i = 0, j[2] = {0, 1};
    print(&i);                  // 错误,实参不是含有 10个整数的数组
    print(j);                   // 错误,实参不是含有 10个整数的数组
    
    1. 使用 main函数处理命令行选项时,通常会写成下列两种形式:
    int main(int argc, char *argv[]) {...}
    int main(int argc, char **argv ) {...}
    

      在上面两个表达式中,argv是一个数组,它的元素是指向 C风格字符串的指针,而 argv又可以看成是指向首元素的指针,因此 argv就是一个二级指针,所以也就有了第二个表达式的写法。
    在使用 argv的实参时,可选的实参从 argv[1]开始;argv[0]保存的是程序的名字,而非用户输入。
     9. 为了编写处理不同数量实参的函数,C++11新标准提供了两种方法:所有实参类型相同,使用 initializer_list;实参类型不同, 使用可变参数模板,然后实例化即可。另外,对于与C函数交互的接口程序,省略符形参(...)。可变参数符号与其它特定参数一起出现时,必须在最右边。

     10.initializer_list提供了对一系列相同类型元素的轻量级存储和访问的能力,值初始化后列表中的元素永远是常量值。在拷贝或赋值时,执行的也是“类指针拷贝”,原始列表和副本共享元素。


    6.3 返回类型和 return语句

    1. 含有 return语句的循环后面应该也有一条 return语句,对于该错误,编译器可能检测不到该错误(在我的 VS2015中,会警告,但不报错),则运行时该程序的行为将是未定义的!
    2. 返回一个值的方式和初始化一个变量完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
    int a = fcn(5);
    // 上式等价于
    int tmp = fcn(5);    // 在调用点定义并初始化一个临时量
    int a = tmp;         // 执行 int类型的拷贝构造函数
    
    1. 不要返回局部对象的引用或指针,因为函数完成后,内部的局部变量也会被释放掉。
    const string &manip()
    {
        string ret;
        // 以某种方式改变一下 ret
        if (!ret.empty())
            return ret;        // 错误:返回局部对象的引用
        else
            return "Empty";    // 错误:字符串字面值还是会被转换成一个临时 string对象
    }
    
    1. 调用一个返回引用的函数得到左值,其他返回类型得到右值。可以为返回类型是非常量引用的函数的结果赋值。
    char &get_val(string &str, string::size_type ix)
    {
        return str[ix];
    }
    string s("value");
    get_val(s, 0) = 'V';  // 将 s改成 "Value"
    
    1. C++11新规定,可以返回花括号包围的值的列表,用此列表对表示函数返回的调用点的临时量进行初始化。列表为空,临时量执行值初始化。另外,如果返回类型是内置类型,则花括号列表最多包含一个值,且该值所占内存空间不应大于目标类型的空间(比如,double——>int就会报错)

    2. main函数,返回 0表示执行成功,返回其他值表示执行失败,其中非 0值的具体含义依机器而定。

    3. 数组不能被拷贝,所以函数不能返回数组,但是可以返回数组的指针或引用。形式如下
      int (*func(int i))[10] // func函数返回指向 10个 int组成的数组的指针
      可以按照由内到外的顺序来理解该声明的含义:

    • func(int i)表示一个函数,形参为 int类型。
    • (*func(int i))表示可以对函数调用的结果执行解引用操作,则函数的返回值是指针类型。
    • int (*func(int i))[10]表示该指针指向 10个 int组成的数组
      使用类型别名的话可以大大简化上述表达式,且其可读性也更好。
    using arrT = int[10];
    arrT* func(int i);
    

      C++11新标准中还可以使用尾置返回类型来简化上述函数声明。下式就可以很清楚地看到 func函数返回的是一个指针,且该指针指向了含有 10个整数的数组。
      auto func(int i) -> int(*)[10];
    另外,如果已经有返回值类型的数组存在,可以使用 decltype关键字声明返回类型。

    int arr[10] = {1,2};
    decltype(arr) *func(int i);
    

      不过,需要注意,decltype的返回类型是数组类型,要想表示返回类型为指向数组的指针,必须加上一个 *****符号。


    6.4 函数重载

    1. 重载,几个函数名字相同但形参列表不同,在判断是否重载时,返回类型不予考虑。另外,因为在编译时会为函数在进行重命名,而在重命名时是只考虑函数名和形参的,所以不允许两个函数除了返回类型外其它所有的要素都相同。
    int func(int i);
    double func(int i);  // 错误,无法重载仅按返回类型重载的函数
    
    1. 顶层 const形参不构成重载,而底层 const形参是可以构成重载的。
    Record lookup(Account &);        // 实参为非常量对象时,优先调用此版本
    Record lookup(const Account &);  // 实参为常量对象时,只能调用此版本
    

      对于第二个表达式,实参为常量/非常量对象,都是可以的。但是如果两种表达式都存在,且实参为非常量对象时,会优先调用第一个非常量版本。因为第一个表达式为精确匹配,而第二个表达式则需要将非常量类型转化为常量类型。
    3. 在 C++语言中,名字查找发生在类型检查之前。在内层作用域中声明的名字将会隐藏外层作用域中的同名实体。

    string read();
    void print(const string &s);
    void main()
    {
        bool read = false;
        string s = read();  // 错误:read是一个 bool类型,而非函数
        void print(int);
        print("value");     // 错误:print(const string &s)被隐藏掉了
    }
    

    6.5 特殊用途语言特性

    1. 默认实参,应尽量让有默认值的形参出现在参数列表的后面。
    2. 局部变量不能作为默认实参。
    // wd、def和 ht的声明必须出现在函数之外
    sz wd = 80;
    char def = ' ';
    sz ht();
    string screen(sz = ht(), sz = wd, char = def);
    string window = screen();  // 调用 screen(ht(), 80, ' ')
    

    另外,用作函数默认实参的名字在函数声明所在的作用域内解析,及确定默认实参到底是用哪些名字,而这些名字的求值过程则发生在函数调用时。可以看下面这个例子。

    void f2()
    {
        def = '*';          // 改变了默认实参的值
        sz wd = 100;        // 隐藏了外层作用域的 wd,但是默认实参使用的
                            // 是外层作用域中的 wd,所以对于函数调用没有影响!
        window = screen();  // 调用 screen(ht(), 80, '*')
    }
    
    1. constexpr函数,当所有形参在编译期就已全部知道,其返回值也是常量表达式,即也是在编译期就已知的。而只要有一个形参在编译期是未知的,它就会表现为一个正常函数,在运行期计算它的值。这样做,可以大大扩展一个函数的适用范围,对于需要使用在编译期就能知道的常量表达式的场景(如数组大小的说明,整形模板参数(包括std::array对象的长度),枚举成员的值等),该函数也可以使用了。

    2. C++11中规定,函数的返回类型及所有形参都得是字面值类型,而且函数体中必须有且只有一条return语句(不过可以通过条件表达式 “?:”和迭代绕过这些限制)。更详细的内容见 Item 15: 只要有可能,就使用constexpr,这是《effective modern c++》Item 15的翻译,关于 constexpr,讲得非常透彻!

    3. 另外,内联函数和 constexpr函数可以在程序中多次定义,不过对于某个给定的内联函数或 constexpr函数,多个定义必须完全一致。基于这个原因,内联函数和 constexpr函数通常定义在头文件中。也因为它们可以多次定义,所以即使定义在头文件中,链接时也不会出现多次定义的错误,而普通函数这样做就会出错。

    4. assert预处理宏,assert(expr),语义为保证表达式为真,如果表达式为假,assert输出信息并终止程序。这种技术一般用于调试代码,只在开发程序时使用。真正在发布程序时,需要屏蔽掉调试代码。这时可以使用 NDEBUG,定义了 NDEBUG后,assert什么也不做。


    6.6 函数匹配

    1. 函数匹配的过程:
      1. 确定候选函数:与被调用函数同名,且在调用点可见。

      2. 确定可行函数:参数数量相同,参数类型相同或能转换。

      3. 寻找最佳匹配。为了确定最佳匹配,将实参类型转换划分成几个等级,由上到下优先级逐渐降低。

        1. 精确匹配,包括以下情况:
          • 实参类型和形参类型相同。
          • 实参从数组或函数类型转换成指针。
          • 添加或删除顶层 const属性。
        2. 需要进行 const转换(const_cast)。
        3. 需要进行类型提升(short--->int)。
        4. 需要进行算术类型(int-->double)或指针转换。
        5. 需要进行类类型转换。
      4. 编译器依次检查每个实参以确定哪个函数是最佳匹配,如果有且只有一个函数满足下列条件,则匹配成功;否则,编译器将报二义性错误。

      • 该函数每个实参的匹配都不劣于其他可行函数。
      • 至少已有一个实参的匹配优先于其他可行函数。

    6.7 函数指针

    1. 函数指针,指向某种特定函数类型。而函数类型由返回类型和形参类型共同决定,与函数名无关。例如:
      bool compare(int i, int j);
      其函数类型是 bool(int, int)。则该函数类型的指针可声明为
      bool (*pf)(int i, int j);
      但是此时只是声明了一个该类型的函数指针变量,并没有进行初始化!还需要使用函数名或函数指针进行初始化。而把函数名当做一个值使用时,函数可以自动转换成指针。
      pf = compare; 等价于 pf = &compare;
      此外,在使用函数指针调用函数时,无须提前解引用指针。
      bool b1 = pf(1, 2); 等价于 bool b2 = (*pf)(1, 2);

    2. 不能定义函数类型的形参,但形参可以是指向函数的指针。与 void print(const int[10])类似,函数看起来是函数(数组)类型,但实际上却是当成指针使用。所以下面两个表达式都是可以的。

    void useBigger(int i, int j, bool compare(int i, int j)); // 形参是函数类型,但会自动地转换成相应的函数指针
    void useBigger(int i, int j, bool (*pf)(int i, int j));   // 显式地将形参定义成函数指针
    

      注意,对于上面两个表达式,在其之前是否已经声明了 compare和 pf,不会对其产生任何影响。因为作为形参, compare或 pf只是形参的名字,与之前已经声明的同名名字没有关系。另外,作为形参表达式,整体的意义是一个类型。所以使用类型别名可以简化代码,增强可读性。

    // Func和 Func2是函数类型
    typedef bool Func(int i, int j);
    typedef decltype(compare) Func2;
    // FuncP和 FuncP2是函数指针类型
    typedef bool (*FuncP)(int i, int j);
    typedef decltype(compare) *FuncP2;
    // useBigger的等价声明
    void useBigger(int i, int j, Func);    // 形参是函数类型,但会自动地转换成相应的函数指针
    void useBigger(int i, int j, FuncP2);
    
    1. 返回函数指针。不能返回一个函数,但可以返回函数指针。与返回数组指针一样,也还是这四种返回方式。
      • 直接声明。 int (*f1(int)) (int*, int);
      • 类型别名。 using PE = int(*)(int*, int); PE f1(int);
      • 尾置返回。 auto f1(int) ->int(*)(int*, int);
      • decltype。 int f(int*, int); decltype(f) *f1(int);
  • 相关阅读:
    初拾Java(问题一:404错误,页面找不到)
    新年新气象--总结过去,展望未来
    接口测试[整理]
    [转]SVN-版本控制软件
    浅谈黑盒测试和白盒测试
    Bug管理工具的使用介绍(Bugger 2016)
    P2805/BZOJ1565 [NOI2009]植物大战僵尸
    近期学习目标
    P3643/BZOJ4584 [APIO2016]划艇
    P5344 【XR-1】逛森林
  • 原文地址:https://www.cnblogs.com/taqikema/p/8176635.html
Copyright © 2020-2023  润新知