第六章 函数

6.1 函数基础

  • 调用运算符:一对圆括号 (),作用于函数指针
  • 函数调用过程:
    • 主调函数(calling function)的执行被中断,使用实参初始化对应的形参
    • 控制权移交给被调函数,被调函数(called function)开始执行。
  • 函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针。

局部对象

  • 生命周期:对象的生命周期是程序执行过程中该对象存在的一段时间。
  • 局部静态对象static类型的局部变量,在程序执行路径第一次经过对象定义语句时进行初始化,直到程序终止才被销毁

函数声明

  • 函数声明不需要形参的名字
  • 函数三要素:返回类型,函数名,形参类型

6.2 参数传递

  • 形参初始化的机理和变量初始化一样
    • 形参的顶层const被忽略:void func(const int i)void func(int i)具有相同的函数签名 ^f06638
      • 原因:引用没有顶层const;如果传值,传递的是实参的副本,不会改变实参的值
      • 但是前面这个函数体中i是const的,连副本也无法修改
  • 两种传参方式:
    • 传值参数(值传递,传值调用)
    • 传引用参数(引用传递,传引用调用)
      • 如果无需改变引用形参的值,最好将其声明为常量引用。
      • 不能将const对象、字面值或需要类型转换的对象传递给普通引用形参,但是可以传递给常量引用形参

数组形参

  • 数组有两个特殊性质:不允许拷贝数组、使用数组时通常会将其转换为指针
  • 数组形参:
    1
    2
    3
    4
    
    // 以下几种方式形参等价,编译器只会检查传参类型是否为const int*
    void func(const int*)
    void func(const int[]) // 可以看出函数意图是传递数组
    void func(const int[10] // 可以提示数组长度
    
    • 当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针,因此也要传入数组长度
  • 数组引用形参:形参是数组引用,实参要传递相同类型及大小的数组
    1
    2
    3
    
    void func(int (&a)[5]){}
    int a[5] = {1,2,3,4};
    func(a);
    
  • 传递多维数组:数组第二维(及更多维)的大小都是数组类型的一部分,不能省略
    1
    2
    
    void func(int (*matrix)[10], int size){} // matrix是一个指针,指向int[10]类型
    void func(int matrix[][10], int size){} // 等价定义
    

main处理命令行选项

  • int main(int argc, char *argv[]) {}
    • argc代表参数的个数;argv是一个数组,数组元素是char*(或者说char数组),第一个元素是程序的名字或一个空字符串,

可变形参

  • 处理不同数量实参的函数
    • 如果所有实参类型相同,可以传递一个initializer_list标准库类型
    • 如果实参类型不同,可以定义可变参数模板
    • 省略符形参:void func(param_list, ...)
      • 一般只用于与C函数交互的接口程序,便于CPP访问某些C代码
      • 大多数类类型的对象在传递给省略符形参时都无法正确拷贝
      • 省略符形参对应的实参无须类型检查
  • initializer_list:定义在同名头文件中的模板类型
    • initializer_list与vector类似,但是它元素永远是常量
    • initializer_list只能使用列表初始化
    • 含有initializer_list形参的函数也可以有其他形参
    • 其他容器使用列表初始化本质上都是采用了initializer_list形参的构造函数进行初始化的
    • initializer_list 提供的操作
      1
      2
      3
      4
      5
      6
      7
      
      initializer_list<int> lst1, lst2;//默认初始化:空列表 
      initializer_list<int> initlst{1,2,3,4};//initlast 的元素数量与初始值一样多
      lst1(initlst); // 直接初始化,lst1与initlst共享元素(不会复制)
      lst2(initlst); // 赋值初始化,lst2与initlst共享元素(不会复制)
      
      void func(int x, initializer_list<char> lst);
      func(1, {'a', 'b'});
      

6.3 返回类型和return语句

  • 不要返回局部对象的引用或指针
  • 引用返回左值:调用一个返回引用的函数得到左值,其他返回类型得到右值。
  • 列表初始化返回值:函数可以返回花括号包围的值的列表,并对函数返回的临时量进行初始化。
    • vector<int> func() { return {"a", "b", "c"}; }
    • 这样可以减少一次拷贝?
      • 相关:RVO
      • 参考:《程序员的自我修养》P305(声明狼藉的C++返回对象)
  • main的返回值:cstdlib头文件定义了两种预处理变量来表示成功(EXIT_FAILURE)与失败(EXIT_SUCCESS

返回数组指针

  • 取别名比较方便
  • 可以使用尾置返回类型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <cstdio>
#include <iostream>
using namespace std;

// int ( *p )[10]; // 数组指针:指向数组的指针
// int ( *func(params) )[10]; // 返回数组指针:函数参数是params,返回指向int[10]的数组指针

// 形式: Type (*function (parameter_list))[dimension]
int ( *func1(int (*arr)[5]) )[5]{ // 传入数组指针,返回数组指针, int (*arr)[5]是传入的数组指针
    for(int *it = begin(*arr), *ed = end(*arr); it != ed; ++it)
        (*it) += 1;
    return arr;
}

// 使用类型别名进行简化
using arrT = int[5];
// typedef int arrT[10]; // 感觉没有using直观
arrT* func2(arrT* arr, int n){ // 使用别名,传入数组指针,返回数组指针
    for(int i = 0; i < n; i++)
        (*arr)[i] += 1; // 数组指针解引用得到数组
    return arr;
}

// 使用尾置返回类型,简化函数的声明和定义(尤其当返回值比较复杂时)
// 在形参列表后面跟一个->,表示真正的返回值类型跟在形参列表之后,开头返回值用auto代替
auto func3( int (*arr)[5] ) -> int(*)[5]{ 
    for(int *it = begin(*arr), *ed = end(*arr); it != ed; ++it)
        (*it) += 1;
    return arr;
}

// 还可以使用 `decltype`,见P206

int main(){
    int arr[5] = {1,2,3,4,5}; 
    int (*p)[5] = &arr; // p是指向数组的指针, p的内容是数组首地址
    arrT* pp = &arr; // pp同样是数组的指针
    
    int (*a)[5] = func1(p);
    for(int *it = begin(*a), *ed = end(*a); it != ed; ++it) cout<<*it<<" ";
    cout<<endl;

    int (*aa)[5] = func2(pp, 5);
    for(int *it = begin(*aa), *ed = end(*aa); it != ed; ++it) cout<<*it<<" ";
    cout<<endl;

    int (*a3)[5] = func3(p);
    for(int *it = begin(*a3), *ed = end(*a3); it != ed; ++it) cout<<*it<<" ";
    cout<<endl;
}

6.4 函数重载

  • 语法相关:
    • 不允许两个函数除了返回类型以外的其他所有要素都相同,或者说返回值与重载无关
    • 因为[[ch06-函数#^f06638|形参的顶层const被忽略]],所以在重载函数中,一个有顶层const形参,另一个重载函数相应参数是普通形参,相当于重复声明,但是可以区分底层const。例子
  • 使用:
    • 一个函数的形参可能有常量引用和非常量引用两种版本,可以使用const_cast进行类型的转换
      1
      2
      3
      4
      5
      
      const string& func(const string &s) {/* do something*/} // 底层const
      string func(string s) {
          const string& r = func(const_cast<const string&>(s));
          return const_cast<string&>(r);
      }
      
    • 重载和作用域:因为C++中名字查找发生在类型检查之间,所以编译器一旦在当前作用域内找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
      • 因此,不同的重载版本要定义在同一作用域中,一般是全局作用域

6.5 特殊用途语言特性

默认实参

  • 形参顺序:普通形参,不怎么使用默认值的形参,经常使用默认值的形参
    • 设置默认值的形参必须放在没有默认值的形参之后
    • 一旦某个形参被赋予(或使用)默认值,那么它之后的形参都必须要有默认值
  • 虽然多次声明同一个函数是合法的,但是在给定的作用域中一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
    1
    2
    
    void screen(int w, int h, char c=' '); // 第一次声明
    void screen(int w, int h = 80, char c); // 第二次声明,添加了默认实参
    
  • 默认实参只能出现在函数声明和定义其中一处,通常应该在头文件中的函数声明中指定默认实参。
  • 局部变量不能作为函数的默认实参,全局变量和字面值都可以

内联(inline)函数

  • 在函数声明和定义中都能使用关键字inline,但是建议只在函数定义时使用。
  • 一般来说,内联机制适用于优化规模较小、流程直接、调用频繁的函数。内联函数中不允许有循环语句和switch语句,否则函数会被编译为普通函数。

constexpr 函数

不是很理解,个人理解是在编译器就能确定返回值的函数

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

调试帮助

调试帮助:用类似头文件保护的方式,有选择的执行调试代码。即在开发过程中,程序可以包含一些用于调试的代码,当程序发布时,需要先屏蔽掉调试代码。调试帮助通常包含两种预处理功能:assertDNEBUG

  • assert是一种预处理宏(preprocessor macro):assert(expr);
    • 当表达式为假时,assert输出信息并终止程序;如果真,assert什么都不做
    • 常用来检查不能发生的条件
  • NDEBUG预处理变量:关闭调试状态
    • 可以使用#define NDEBUG来定义NDEBUG,但很多编译器都提供了命令行选项-D NDEBUG
    • 如果定义了NDEBUG,则assert什么都不做;默认情况下没有定义NDEBUG
    • 几个用于调试的变量名称:
变量名称内容
__func__当前函数名称
__FILE__当前文件名称
__LINE__当前行号
__TIME__文件编译时间
__DATE__文件编译日期
1
2
3
4
5
6
void print(){
    #ifndef NDEBUG // 默认情况下没有定义NDEBUG,可以在这里编写自己的调试代码
    // 如果定义了NDEBUG,则跳过
    cerr << __func__ << "..." << endl;
    #endif
}

6.6 函数匹配

函数匹配(或称为重载确定)

  • 重载函数匹配的三个步骤:
    • 找候选函数:同名函数
    • 选可行函数:形参实参数量相等,类型匹配或者能进行转换
    • 寻找最佳匹配:实参类型和形参类型越接近,它们匹配越好
      • 精确匹配、从数组类型或函数类型转换为对应的指针类型、添加/删除顶层const
      • const转换
      • 类型提升
      • 算数类型转换、指针转换
      • 类类型转换
    • 如果有若干个匹配,但没有一个最佳匹配时,编译器可能报告二义性调用的信息

6.7 函数指针

  • 对于重载函数,函数指针类型必须与重载函数中某一个精确匹配
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
      using Func = int(int, int); // 函数类型
      typedef int Func2(int, int); // 函数类型,等价于Func
      using FuncP =  int(*)(int, int); // 函数指针类型
      typedef int (*FuncP2)(int, int); // 函数指针类型,等价于FuncP
    
      int add(int a, int b) {return a + b;}
      double add(double a, double b) {return a + b;}
    
      int op1(int a, int b, int (*f)(int, int) ) {return (*f)(a, b);}
      int op2(int a, int b, Func* f) {return (*f)(a, b);} // 函数不能做形参
      int op3(int a, int b, FuncP fp) {return (*fp)(a, b);} 
    
      int test() {return 1;}
      typedef decltype(test) FuncT; // 函数类型
      typedef decltype(test) (*FuncTP); // 函数指针类型,decltype返回函数类型,需要在别名类型前加上*表示函数指针
    
      int main(){
          Func *f = &add;
          FuncP g = &add;
          cout<<op1(1,2,add)<<" "<<op2(1,2,f)<<" "<<op3(1,2,g);  
      }
    
  • 函数指针可以作为形参
    • 形参类型可能为函数类型,传入的也可能是函数名,但是最终都是转换为函数指针
  • 函数指针可以作为返回值,函数类型不可以作为返回值
1
2
3
4
5
6
7
using Func = int(int, int); // 函数类型
using FuncP =  int(*)(int, int); // 函数指针类型

int (*ret_func1(params)) (int, int); // 一个名为ret_func1的函数,其参数为params,返回一个int(*)(int, int)类型的函数指针
auto ret_func2(params) -> int(*)(int, int); // 尾置返回类型
Func* ret_func3(params); // 使用别名,返回指向函数类型的指针,不能返回Func
FuncP ret_func4(params); // 使用别名,返回函数指针
  • 复杂例子:假设函数指针类型是int(*)(int, int),数组指针类型是int (*)[5]
    1
    2
    
    using FuncP = int(*)(int, int);
    using Arr = int[5];
    
    • 返回值是函数指针,函数形参是数组指针
      1
      2
      3
      
      int ( *f( int(*arr)(int,int) ) ) (int (*)[5]);
      auto f( int(*arr)[5] ) -> int(*)(int, int);
      FuncP f(Arr* arr);
      
    • 返回值是函数指针,函数形参是函数指针
      1
      2
      3
      
      int (*f( int(*fp)(int, int) )) (int, int);
      auto f( int(*fp)(int, int) ) -> int(*)(int, int);
      FuncP f(FuncP fp);
      
    • 返回值是数组指针,函数形参是函数指针
      1
      2
      3
      
      int ( *f( int(*fp)(int, int) ) )[5];
      auto f( int(*fp)(int, int) ) -> int(*)[5];
      Arr* f(FuncP fp);
      
    • 返回值是数组指针,函数形参是数组指针
      1
      2
      3
      
      int (*f( int(*arr)[5] ))[5];
      auto f( int(*arr)[5] ) -> int(*)[5];
      Arr* f(Arr* arr);