第六章_函数_PrimerC++


1. 函数基础

函数是一个命名了的代码块,我们通过调用函数执行相应的代码。

  • 函数定义包含以下几个部分:返回类型、函数名字、由0个或多个形参组成的列表以及函数体
  • 我们通过调用运算符执行函数,调用运算符的形式是一对圆括号,它作用域一个表达式,表达式时函数或指向函数的指针;圆括号内为用逗号分割的实参,实参用于初始化函数的形参

函数的编写和调用

1.1 局部对象

形参和函数体内部定义的变量统称为局部变量,局部变量的生命周期依赖于创建方式。

在所有函数体之外定义的对象存在于程序的整个执行过程中,此类对象在程序启动时被创建,直到程序阶数才会销毁。

自动对象

  • 函数的控制路径经过变量定义语句时创建该对象,到达定义所在的块末尾时销毁它。

  • 这样只存在于块执行期间的对象称为自动对象

  • 块结束后,块中创建的自动对象的值就变成未定义的了

  • 形参是自动对象

    • 通过实参初始化形参

    • 如果变量定义有初始值,就用初始值进行初始化;否则,执行默认初始化

    • 形参初始化示例

      #include <iostream>
      using namespace std;
      
      void f1(int a = 20){
          cout << a << endl;
      }
      
      void f2(int a){
          cout << a << endl;
      }
      
      int main(){
          f1();
          f1(10);
          // f2(); // error: too few arguments to function 'void f2(int)'
          f2(10);
          return 0;
      }
      

      输出:

      20
      10
      10
      
  • 内置类型的未初始化局部变量将有未定义的值

局部静态对象

  • 有时候有必要另局部变量的生命周期贯穿调用及之后的时间(后续调用会用到相同的变量,要求其保留状态的情况)

  • 将局部变量定义为static可以获得这样的对象

  • 局部静态变量在程序的执行路径第一次经过对象定义时初始化,直到程序终止才销毁

    在此器件即使对象所在的函数结束执行也不会对它有影响

  • 例子:

    #include <iostream>
    using namespace std;
    
    void f(){
        static int a = 0;
        cout << a++ << endl;
    }
    
    int main(){
        f();
        f();
        return 0;
    }
    

    输出:

    0
    1
    
1.2 函数声明
  • 函数必须在使用之前声明

  • 只能定义一次,但可以声明多次

  • 如果一个函数不会被我们用到,可以只声明(15.3节)

  • 函数声明也称为函数原型

  • 例子:

    void print(vector<int>::const_iterator beg, vector<int>::const_iterator end);
    
  • 建议尽量在头文件中进行声明,在源文件中进行定义。

1.3 分离式编译

2. 参数传递

形参的类型决定了形参和实参的交互方式

  • 形参为引用:绑定到实参上(引用调用)
  • 形参为值:实参的值拷贝后赋值给形参(传值调用)
2.1 传值参数

非引用类型

  • 初始值被拷贝给形参
  • 对变量的改动不会影响初始值

指针形参

  • 指针的行为与非引用类型一样
  • 指针拷贝时拷贝的是指针的值
  • 拷贝后两个指针为不同指针,但是指向的对象相同
2.2 传引用参数
  • 引用形参可以通过形参改变原来的实参

  • 使用引用形参可以避免拷贝

  • 使用形参可以返回额外信息

    由于一个函数只能返回一个值,有时候函数需要返回多个值,可以通过引用形参来返回值

2.3 const形参和实参
  • 实参初始化形参时会忽略掉顶层const,即常量对象或非常量对象都可以初始化具有顶层const的形参
  • 可以用非常量初始化一个底层const对象,但是不可以用底层const对象初始化一个非常量,适用于形参中。
  • 尽量使用常引用
2.4 数组形参

数组的特殊性质对作用在数组上的函数有影响:不允许拷贝数组以及使用数组时通常会将其转换为指针

  • 传递数组实际上是传递指针

  • 形参依然可以写为类似数组的形式

    void print(const int *);
    void print(const int []);
    void print(const int [10]);
    

    扩展:传递二维数组

    // 方式一
    void f(const int (*arr)[3]){
        ...
    }
    // 方式二
    void f(const int arr[][3]){
    
    }
    

常用的传递方式

  • 使用标记指定数组长度

    C语言风格,读到空指针就停下

  • 使用标准库规范

    传递指向数组首元素和尾后元素的指针

  • 显式传递一个表示数组大小的形参

  • 数组引用形参

    void f(int (&ref)[10]){ // 需要指定大小
        ...
    }
    
2.5 main:处理命令行选项

int main(int argc, char *argv[])

main函数可以接收两个参数,第一个参数是数组argv中字符串的个数,第二个参数是指向C风格字符串数组的指针

例子:

prog -d -o ofile data0
argc = 5
argv[0] = "prog"
argv[1] = "-d"
argv[2] = "-o"
argv[3] = "ofile"
argv[4] = "data0"
argv[5] = "prog0
2.6 可变参数

有时候无法预知有多少个实参会被传递,这是通过C++11标准提供的方法:

  • 传递的实参类型相同时,可以传递initializer_list的标准库类型
  • 如果实参类型不同,可以编写特殊的可变参数模板(16.4节)
  • 特殊形参:省略符

initializer_list形参

  • 用法:略
  • 含有initializer_list形参的函数也可以拥有其他形参

省略符形参

  • 用于形参列表的最后一个位置
  • 为了便于C++程序访问某些特殊的C代码而设置的

3. 返回类型和return语句

3.1 无返回值函数
  • 无返回值的return语句只能用在返回类型为void的函数中
  • 这样的函数中的return语句也可以省略
3.2 有返回值函数
  • 有返回值的函数返回值的类型必须与返回类型一直

  • 值是如何被返回的?

    返回一个值的方式和初始化一个变量或者形参的方式一样:返回地值用于初始化调用点的一个临时量,该临时量就是函数调用的结果

  • 不要返回局部对象的引用或指针

3.3 返回数组指针

可以通过以下集中方式返回一个数组指针

  • 声明一个返回数组指针的函数

    形式:Type (*function(parameter_list)) [dimension]

    例子:int (*func(int i))[10];

    • 拆解过程

      func(int i)表示func函数接受一个int型实参

      (*func(int i))表示func返回值为指针

      (*func (int i))[10]表示func返回值为指向大小为10数组的指针

      int (*func(int i))[10]表示这个指针直线的数组为int型数组

  • 尾置返回值

    C++11新标准,任何函数定义都可以使用尾置返回

    但是这种形式对于返回类型比较复杂的函数最有效(例如返回类型为数组指针/引用)

    尾置返回类型跟在形参列表后并以一个->符号开头

    为了表示函数真正的返回类型在形参列表之后,我们在原来的返回类型的地方放置一个auto

    • auto func(int i) -> int(*)[10];
  • 使用decltype

    例子:

    int odd[] = {1, 3, 5, 7, 9};
    int even[] = {0, 2, 5, 6, 8};
    decltype(odd) *arrPtr(int i){
        return (i % 2)? &odd : &even;	// 返回一个指向数组的指针
    }
    

    注意的是decltype并不负责把数组类型转换成对应的指针,所以后面还需要加上*符号。


4. 函数重载

重载函数:在同一作用域内的几个函数名字相同但形参不同

定义重载函数

  • 重载函数的形参数量或形参类型上必须有所不同
  • 不允许两个函数除了返回类型外其他所有的要素都相同

重载和const形参

  • 顶层const不影响传入参数

    故相同顶层const参数的两个函数将无法重载

    例子:

    #include <iostream>
    using namespace std;
    
    void f(const int arr){	// 给成int const也不行
        cout << 1 << endl;
    }
    
    void f(int *arr){
        cout << 2 << endl;
    }
    
    int main(){
        int s[] = {1,2,3,4,5,6,7,8,9};
        f(s);
        return 0;
    }
    

    一个拥有顶层const无法和没有顶层const的形参区分开来

  • 对于底层const,函数可以区分它指向的是常量对象还是非常量对象区分开来

    此时的const是底层的

    例子:

    #include <iostream>
    using namespace std;
    
    void f(const int *arr){
        cout << 1 << endl;
    }
    
    void f(int *arr){	// g++9.2.0版本下默认会执行这个函数
        cout << 2 << endl;
    }
    
    int main(){
        int s[] = {1,2,3,4,5,6,7,8,9};
        f(s);
        return 0;
    }
    
  • 重载函数的原则:重载的几个函数实现的功能应该相同,即与它们的名字相符合

const_cast和重载

  • 通过const_cast强制类型转换可以在不同重载函数之间相互调用

    注意: const_cast 中的类型必须是指针、引用或指向对象类型成员的指针

    const string& shorterString(const string& s1, const string& s2) {
        return s1.size() <= s2.size() ? s1 : s2;
    }
    string& shorterString(string& s1, string& s2) {
        const string& (r) = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
        return const_cast<string&> (r);
    }
    
  • 这样的意义是什么?

调用重载的函数

  • 函数匹配:将函数调用与重载函数中的某一个关联起来(也称函数确定)

    编译器将调用的实参与重载函数的形参列表进行比较,根据比较确定函数

重载与作用域

  • 不同的作用域中无法重载函数

    书上例子(6.4.1节)


5. 特殊语言用途

5.1 默认实参

函数调用的过程中,形参都被赋予了一个重复的值,这个反复出现的参数可以作为默认实参,调用含有默认实参的函数时,可以包含该参数,也可以不包含该参数。

默认实参声明:

  • 函数通常只声明一次,但是声明多次也是合法的。

  • 一个形参被赋予了默认值,则后面的形参也需要被赋予默认值。

  • 声明多次包含默认实参的函数,只能为没有默认值的形参添加默认值。

    例如:

    string screen(sz, sz, char = '*');
    string screen(sz, sz, char = '?');	// 错误:重复声明
    string screen(sz, sz = 80, char);	// 正确:添加默认实参表
    

默认实参初始值:

  • 局部变量不能作为默认实参
5.2 内联函数

使用内联函数可以减少函数调用的开销


6. 函数匹配

确定候选函数和可行函数

  • 集合中的函数称为候选函数,满足:
    1. 与被调用函数同名
    2. 声明在调用点可见
  • 考察参数选出可行函数
    1. 形参数量与提供实参数量相等
    2. 实参类型与对应形参类型相同或者可以转换为形参类型

寻找最佳匹配(可能存在的)

  • 逐一检查实参,寻找与形参最匹配的
6.1 实参类型转换

为了确定最佳匹配,将形参类型优先级进行划分:

  1. 精确匹配

    类型相同

    数组可以转换为指针

    向实参添加顶层const(即视为常量)或向实参删除顶层const

  2. 通过const转换实现的匹配

  3. 通过类型提升实现的匹配

  4. 通过算术类型转换或指针转换实现的匹配

  5. 通过类类型转换实现的匹配


7. 函数指针

函数类型由返回类型和形参类型共同确定,而与函数名无关。

使用函数指针

  • 函数名作为值使用时,自动转换为函数指针

    即函数名相当于函数指针

    例子:

    bool lengthCompare(const string &, const string &);
    bool (*pf)(const string &, const string &);
    
    // 以下两种声明都可以
    pf = lengthCompare;
    pf = &lengthCompare;
    // 以下两种使用都可以
    bool b1 = pf(s1, s2);
    bool b2 = (*pf)(s1, s2);
    
    

重载函数的指针

  • 要求重载函数与定义的函数指针精确匹配

函数指针形参

  • 和数组类似,不可定义函数类型的形参,但可以定义指向函数的指针,此时指针可以当做函数来用
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值