第六章 函数

6.1 函数基础

  1. 函数(function) :是一个命名的代码块,我们可以通过调用函数来执行其中的代码。
  2. 函数的定义:① 返回类型(return type) ②函数名字 ③由0个或多个 形式参数(parameter) 组成的列表 ④函数体。
  3. 用户通过 调用运算符(call operator) 来执行函数。调用运算符的形式是一对圆括号 ()它作用于一个表达式,该表达式是函数或者指向函数的指针 。圆括号内是一个用逗号隔开的 实际参数(argument) 列表,用户用实参初始化函数的形参。整个表达式的类型就是函数的返回类型。
  4. 函数的调用完成两项工作:①是用实参初始化函数对应的形参,②是将控制权转移给被调用函数。 主调函数(calling function) 的执行被暂时中断, 被调用函数(called function) 开始执行。
  5. 注意 :尽管实参和形参存在对应关系,但是并 没有规定实参的求值顺序 ,编译器能以任何可行的顺序对实参求值。
  6. 实参的类型必须能隐式转换成形参的类型,且个数相同,也就是实参与对应的形参类型和个数相匹配。
  7. 函数的形参列表可以为空,但是不能省略。 定义一个不带形参的函数,最常用的方法是写一个空的形参列表。有两种形式:
//隐式定义空形参列表
void fcpp(){}
//显式定义空形参列表
void fc(void){}
  1. 每个形参的参数类型都必须写出来(即使形参的类型一样)并使用逗号分隔。任意两个形参不能同名,而且函数最外层作用域中的局部变量也不能与函数形参同名。
//错误
void fcpp_e(int v1, v2){}
//正确
void fcpp1(int v1, int v2){}
//正确
void fcpp2(int v1, int ){}

形参名是可选的,但是无名的形参在函数中无法使用。

  1. 大部分类型都可以用作函数的返回类型,一种特殊的返回类型是 void ,它表示函数无返回值。 函数的返回值不能是数组或函数,但是可以是指向数组或函数的指针。
  2. 函数头 :包括函数名、返回类型、参数列表三个部分。

6.1.1 局部对象

  1. 在 C++ 中,名字有作用域,对象有 生命周期(lifetime),概念如下:
  • 名字的作用域 :程序文本的一部分,名字在其中可见
  • 对象的生命周期 :程序执行过程中该对象存在的一段时间
  1. 函数体是一个块, 一个块构成一个新的作用域 ,我们可以在其中定义变量。
  2. 局部变量(local variable) :形参和函数体内定义的变量。
    它们对于该函数而言是“局部”,仅在该函数的作用域内可见。同时局部变量还会 隐藏(hide) 在外层作用域中同名的其他所有声明中。
  3. 在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束时才会销毁。
  4. 局部变量的生命周期取决于定义的方式。
  5. 自动对象(automatic object) :只存在于块执行期间的对象。
    自动对象的生命周期就是块执行的期间。
    所以当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
  6. 内置类型的未初始化局部变量的值是未定义的。
  7. 局部静态变量(local static object) :类型为 static 的局部变量。
    局部静态变量的生命周期贯穿函数调用及之后的时间。局部静态变量在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。
    例如:
static int sint;    // 定义一个类型为 static 的 int 局部变量 sint
  1. 局部静态变量如果没有显式初始化,它将执行 值初始化内置类型的局部静态变量初始化为0

6.1.2 函数声明

  1. 大部分函数只能被定义一次,但是都可以多次声明。 例外是 虚函数、内联函数和 constexpr 函数 ,如果一个函数永远都不会被用到,那么它可以只有声明没有定义。
  2. 函数的声明可以无需函数体,只需要一个分号替代即可,而且还可以省略形参名。
  3. 函数原型(function prototype) :函数声明的别称。
  4. 函数的三要素(返回类型、函数名、形参类型)描述了函数的接口。
  5. 定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。

6.1.3 分离式编译

  1. C++ 支持 分离式编译(separate compilation) ,分离式编译允许我们把程序分割到几个文件去,每个文件独立编译。
  2. 可执行文件(executable file)
  3. 大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是 .obj(Windows).o(UNIX) 的文件。
    这两个后缀名的含义:该文件包含 对象代码(object code)
  4. 编译的过程:例如
#假设 fact 函数定义位于一个名为 fact.cc 的文件中,它的声明位于 Chapter6.h 的头文件中。 fact.cc 应该包含 Chapter6.h 的头文件。另外在名为 factMain.cc 的文件中创建 main 函数, main 函数中将调用 fact 函数。要生成可执行文件,必须告诉编译起我们用到的代码在哪里,编译过程如下:
$ CC factMain.cc fact.cc # generates factMain.exe or a.out
$ CC factMain.cc fact.cc -o main # generates main or main.exe

其中的 CC 是编译器的名字, $ 是系统提示符, # 后面是命令行下的注释语句。

#修改了其中一个源文件,只需要重新编译改动的文件
$ CC -c fact.cc
$ CC -c factMain.cc
$ CC factMain.o fact.o
$ CC factMain.o fact.o -o main

6.2 参数传递

  1. 每次调用函数时都会重新创建它的形参,并使用实参对形参进行初始化。
  2. 如果形参是引用类型,它将绑定对应的实参;否则将实参的值拷贝后赋给形参。
  3. 当形参是引用类型时,我们说它对应的实参被 引用传递(passed by reference) 或者函数被 传引用调用(called by reference) 。同样地,引用形参也是对应实参的别名。

6.2.1 传值参数

  1. 传值参数的机理:函数对形参做的所有操作都不会影响实参。而程序员可以通过指针的值访问外部对象,或是通过引用形参来改变外部东西。
  2. 在 C 中,程序员常常使用指针类型的参数和解引用运算符来访问外部的对象。在 C++ 中,建议使用引用类型的形参替代指针。

6.2.2 传引用参数

  1. 通过使用引用形参,允许函数改变一个或多个实参的值。
  2. 拷贝大的类类型对象或者容器对象比较低效,甚至 有的类( class )类型(包括 IO 类型在内)根本不支持拷贝类型当某种类型不支持拷贝类型时,函数只能通过引用形参访问该类型的对象。
  3. 一个函数只能返回一个值,引用形参为我们一次返回多个结果提供了有效的途径——给函数传入额外的引用实参用于存储其他返回结果。
  4. 如果函数无需改变引用形参的值,最好将其声明为常量引用。

6.2.3 const 形参和实参

  1. 若引用形参是 const 时,则 const 作用于被引用的对象本身。
  2. 与其他初始化一样,使用实参初始化时会忽略掉顶层 const ,所以当形参有顶层 const 时,传给它常量对象或者非常量对象都是可以的。
  3. 在 C++ 中,允许我们定义若干相同函数名的函数,前提是参数列表有明显的区别。因为 top-level-const 形参中的 const 会被忽略掉,所以以下两个函数头的参数列表是一样的。
void fcn(const int i);
void fcn(int i);
  1. 形参的初始化方式和变量的初始化方式一样 ,所以引用参数的初始化必须要用一个相匹配类型的对象。而我们可以使用一个非常量初始化一个 low-level-const 形参,反之不行。

6.2.4 数组形参

1.数组的两个特殊性质:
①不允许将数组以数组名拷贝赋值的方式赋值给别的数组,因为数组;
②使用数组名时(通常)会将其转换成指针。

  1. 不能以值传递的方式传递数组,也就无法定义数组类型的形参,但是 可以把形参声明写成类似数组声明的形式,但是实际上最后得到的还是指针形参
三个都是等价的,他们的形参都是 const int* ,而不是数组类型的形参

void print(const int*);

//函数的意图是作用于一个数组
void print(const int[]);
//表示我们期望数组含有多少元素,实际
//不一定
void print(const int[10]);
  1. 因为数组名是以指针传入函数的,所以函数并不知道数组的元素个数,调用者应该提供一些信息。
  2. 管理指针实参有三种常用的技术:
    ①数组本身包括一个结束标记,比如 C 风格字符串的结尾为空字符;
    ②传递指向数组首元素和尾后元素的指针(指向尾元素的下一个地址的指针);
    ③专门定义一个表示数组元素大小的形参。
  3. 可以通过定义 low-level-const 的形参来防止对数组内元素进行改写。
  4. C++ 允许将变量定义成数组的引用,所以形参可以是对数组的引用 ,此时形参绑定了实参,也就是数组。
// arr 是一个对具有10个元素的 int 数组的引用
void print(int (&arr)[10])

注意: 因为数组的大小也是数组类型的一部分 ,所以在这里只能将一个具有10个元素的 int 数组作为实参传入。

int i = 0 ,m[10]{};
int n[2] {};
//错误
print(&i);
print(n);
//正确
print(m);
  1. 传递多维数组时,函数的引用形参类型必须与数组的维度个数和大小相同。
  2. 使用数组的语法定义函数时,编译器也会忽略掉第一个维度。
//等价定义
void print(int (*matrix)[10]);
void print(int matrix [] [10]);
//实际上形参是一个指向含有10个 int 元素的数组

6.2.5 main:处理命令行选项

  1. main 函数有时需要用户传递实参,一种常见的情况是用户设置一组选项来确定函数所要执行的操作。
    比如当 main 函数位于可执行文件 prog 之内,我们可以通过命令行向程序传递下面选项: prog -d -o ofile data0
    这些命令行选项通过两个(可选)形参(argc 和 argv)传递给 main 函数:
//下面两个是等价的
int main(int argc, char *argv[]){return 0;}
//因为 argv 实际上是数组,所以 *argv[] 可以写成 **argv 
int mian(int argc, char **argv){return 0;}

当实参传给 main 函数时, main 形参的值如下:
argv 的首元素指向程序的名字或一个空字符串
接下来的元素依次传递命令行提供的素材
尾后元素的值保证为0
以上面的命令行为例, argc 为5, argv 的元素如下:

// argv[0] 可以是一个空字符串
argv[0] = "prog";  
argv[1] = "-b";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
  1. 注意: 当使用 argv 时,一定要记得可选的形参从 argv[1] 开始; argv[0] 保存程序的名字,要不就是0,不是用户的输入。

6.2.6 含有可变形参的函数

  1. C++11 为了用户能编写处理未知数量实参的函数,提供了两种主要的方法:
    ①如果所有的 实参类型相同 ,可以传递一个名为 initializer_list标准库类型
    ②如果 实参类型不同 ,我们可以编写一种特殊的函数,也就是所谓的 可变参数模板
  2. C++ 还有一种特殊的 形参类型(即省略符) ,可以用它传递可变数量的实参。
    注意:这种功能一般只用于与 C 函数交互的接口程序。
  3. initializer_list 类型是一种 标准库类型用于表示某种特定类型的值的数组 ,类似 vector 容器类型,它也是一种 模板类 ,它定义在同名的头文件中。
    它提供了一些 initializer_list 的操作如下:
默认初始化:初始化 lst 为 T 类型的空列表
initializer_list lst;

 lst 的元素个数和初始值一样多;
 lst 的元素是对应初始值的副本;列表中的元素是 const 
initializer_list lst{a, b, c . . . };

//拷贝或赋值一个 initializer_list 对象不会拷贝列表中的元素,而是拷贝对象的地址;
//拷贝后,原始列表和副本共享元素——就是原始列表和副本同时指向同一个临时内存
//(两个对象指向同一个,就是存放初始化 lst 时所用常量副本的一个临时内存地址)
lst2 = lst
lst2(lst)

//对象中元素数量
lst.size();

//返回指向 lst 首元素的指针
lst.begin()

//返回指向 lst 尾后元素的指针
lst.end()

注意:
①定义 initializer_list 对象时,必须说明<>中所含元素的类型。
②与 vector 不同的是,由于 initializer_list 对象与初始化它的常量副本共享元素, initializer_list 对象中的元素永远是常量值 ,无法通过任何方法修改。

用户可以使用如下形式编写输出错误信息的函数,使其用于未知数量的实参:

void error_msg(initializer_list il)
{
      for (auto beg = il.begin; beg != il.end(); ++beg)
            cout << *beg << " ";
      cout << endl;
}

调用如下:

// exp 和 act 是 string 对象
//下列函数调用传入了三个实参
error_msg("functionX", exp, act);

含有 initializer_list 形参的函数还可以声明别的形参

void error_ggg(int a, initializer_list il);

注意 initializer_list 形参的初始化需要使用列表 ,两种调用如下:

// exp 和 act 是 string 对象
error_ggg(12, {"functionX", exp, act});
// sstrlst 是 initializer_list 对象
error_ggg(12, strlst);
  1. 省略符形参 是为了便于 C++ 程序访问某些特殊的 C 代码 而设置的,这些代码使用了名为 varargs 的 C 标准库功能。
    注意:
    大多数类型的对象在传递给省略符形参时都无法正确拷贝。
    省略符形参只能出现在形参列表的最后一个位置 ,它的形式为以下两种:
/parm_list 是参数列表
void foo(parm_list, ...);
void foo(...);

第一种形式制定了 foo 函数的部分形参类型,对应这些形参的实参会执行正确的类型检查。省略符形参对应的实参无需类型检查。第一种形式中,形参声明后面的逗号是可选。


6.3 返回类型和 return 语句

  1. return 语句 终止当前正在执行的函数将控制权返回到调用该函数的地方
  2. return 语句有两种形式:
return ;
return expression; // expression 是表达式
  1. 一个函数内, return 语句可以有多个。

6.3.1 无返回值函数

  1. 没有返回值的 return 语句只能用在返回值类型为 void 的函数中。
  2. 返回值类型为 void 的函数不一定要有 return 语句 ,因为在返回值类型为 void 的函数执行完最后一句语句后,编译器会隐式执行没有返回值的 return 语句。
  3. 如果 void 函数想在它的中间位置提前退出,可以使用无返回值的 return 语句,类似于使用 break 语句提前退出循环一样。
  4. 一个返回类型是 void 的函数也能使用 return 语句的第二种形式,不过 expression 必须是另一个返回 void 的函数 ,否则强行令 void 函数返回其他类型的表达式将出现编译错误。

6.3.2 有返回值函数

  1. return 语句第二种形式的表达式 expression 的类型必须 与函数的返回类型相同 或者 能隐式转换成函数的返回类型
  2. 只要函数返回类型不是 void ,则除了 main 函数以外,函数内每条 return 语句都必须显式返回一个值。
  3. C++ 编译器能保证每个 return 语句的返回值类型正确。 若函数的 return 语句返回了与当前函数声明的返回值类型不同类型的值,编译将会失败。
  4. 注意:编译器有时不能检测到在含有 return 语句的循环语句之后应该含有一条 return 语句的错误。例如:
int text(int n){
  while(n != 10){
    if(n < 10)
      return n; //若 n 小于10则返回 n 的值
    n--;
  }
  //若 n 跳出循环则不返回任何值就结束了函数的运行
  //编译器可能检查不出这一错误
}
  1. C++ 如何实现函数值的返回:将函数的 return 语句的返回值用于初始化被调用函数的调用位置的一个临时量 (既可以是临时变量,也可以是临时对象,由编译器自动生成),该临时量就是该函数调用的结果。换句话说就是将返回值拷贝到这个临时量中。
    注意:
    ①若函数的返回值类型是引用类型,则编译器并不会生成一个临时量用于拷贝返回值,而是返回返回值的一个引用,换句话说就是别名
    ②函数的返回值类型不是引用类型,则编译器会生成一个临时量用于拷贝返回值
  2. 函数结束后,它所占的存储空间将会被释放掉(将函数栈帧删除),因此函数的局部变量将会被释放,其引用将不再指向有效的内存空间。所以 不要返回局部变量的引用或指针
  3. 调用运算符 () 的优先级与点运算符和箭头运算符相同,符合左结合律,函数名为其左侧运算对象。
  4. 可以通过点运算符和箭头运算符来访问函数的返回值中的成员 ,例如:
//通过点运算符访问 shorterString 函数返回值,一个 string 对象的成员方法 size 
auto sz = shorterString(s1, s2).size();
  1. 函数的返回类型决定函数调用是否为左值。调用一个返回类型为引用类型的函数得到左值,调用其他返回类型的函数得到右值。
  2. 当然如果返回类型是常量引用,我们不能给函数调用的结果赋值。
  3. C++11 规定:函数可以返回花括号包围的值的列表
    ①如果函数返回类型是 C++ 的内置类型,则花括号包围的列表最多包含一个值,而且该值的大小不应该大于函数返回类型大小。
    ②如果函数返回类型是 C++ 的类类型,则由类本身定义初始值如何使用。
    例如:
vector process(string &expected,string &actual){
  if(expected.empty()){
    return {};  //用空列表初始化一个 vector 临时量
  }
  else if(expected == actual){
    return {"function", "okay"};
  }
  else return {"function", expected, actual};
}
  1. 若 main 函数没有 return 语句直接结束,则编译器将隐式插入一条 return 0;
  2. 在 cstdlib 头文件里定义了两个预处理变量用于判断 main 函数是否执行成功: EXIT_FAILURE 和 EXIT_SUCCESS 。
  3. 递归函数(recursive function) :一个函数直接或间接地调用了它本身。递归函数将会一直执行直到执行 return 语句或者栈空间耗尽为止。

6.3.3 返回数组指针

  1. 可以定义数组类型的别名,如 typedef int arrT[10];// arrT 是类型别名using arrT = int[10];// C++11 中的别名声明arrT* fun(); 是返回一个指向长度为10的一维数组的首指针, 注意是指向指定维度数组
  2. 注意:
arrT* funtion1() {
    int a[10] = {};
    return a; //这是错误的, a 的类型是 int[10] ,而编译器无法隐式将 int[10] 的值转换成 int(*)[10]  
}
arrT* funtion2() {
    int b[11] = {};
    return &b; //这是错误的, b 的类型是 int(*)[11] 
}
arrT* funtion3() {
    int c[10] = {};
    return &c; //这是正确的
}
  1. 在不使用类型别名的情况下,如果想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。
    函数声明格式如下 type (*function(parameter_list))[dimension]; , type 是数组元素类型, function 是函数名, parameter_list 是参数列表, dimension 是数组大小。
    例如 int (*func(int i))[10]; 和 第1条的 arrT* fun(); 等价,其中 arrT 是 int[10] 的类型别名。
  2. C++11 新标准增添了一种可以简化定义一个返回数组指针的函数的方法,就是使用 尾置返回类型(trailing return type)任何函数的定义都可以使用尾置返回类型 ,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或数组的引用。
    函数声明形式如下: auto func(parameter_list) -> type ,其中 auto 关键字放在函数名前, func 是函数名, parameter_list 是参数列表, type 是函数的返回类型。
    例如 auto func(int i) -> int(*)[10];
  3. 如果知道函数返回的指针指向哪个数组,则 可以使用 decltype 关键字声明返回类型
    例如:

6.4 函数重载

  1. 重载函数(overloaded function) :同一作用域内的几个函数的函数名相同,但 参数列表不同 。参数列表不同指的是参数类型和数量不全相同。 如果只是返回类型不同,这么定义重载函数是错误的 ,因为重载函数是根据形参来匹配相应重载函数。
  2. main 函数不能重载。
  3. 以下的情况的形参类型相同:
1. 使用类型别名
2. 在函数声明中省略形参名
3. 使用顶层 const ( top-level-const )

1. 
typedef  int typeint;
void hh1(typeint a);
void hh1(int a); //一样的,使用了类型别名
2. 
void hh2(typeint a);
void hh2(typeint); //一样的,忽略了形参名
3. 
void hh3(typeint* const a);
void hh3(typeint* a); //一样的,使用了顶层 const 
void hh3(const int a);
void hh3(int a); //一样的,使用了顶层 const 
  1. 对于形参是某种类型的引用或者指针时,则通过区分其指向的是常量对象还是非常量对象以实现函数重载。这里的 const 是底层 const 。
//以下4个函数的形参类型不同
void look(const int* a);
void look (int* a);

void look(const int & a);
void look(int & a);
  1. 可以通过C++的强制类型转换的 const_cast 来实现一些函数的重载,以达到 DRY 编程原则(简单说就是不写重复的代码片段用来复用)和 pass-by-reference-to-const (通过 const 引用传送参数)。
    例如
const string &shorterString(const string &s1,const string &s2)
{
  return s1.size()<=s2.size()?s1:s2;
}
//重载函数
string &shorterString(string &s1,string &s2)
{
  //这里就实现了 DRY 编程原则
  auto &r = shorterString(const_cast(s1),const_cast(s2));
  return const_cast(r);
}
  1. 函数匹配(function matching) :是指一个把函数调用与一组重载函数中的某一个关联起来的过程,也叫 重载确定(overload resolution) ,编译器首先将调用的实参与一组重载函数中每一个函数的参数列表进行比较,再根据比较的结果选择并调用最佳函数。
  2. 在某些特殊情况下,函数匹配将会比较困难,比如函数参数的数量相同且类型可以互相转换。
  3. 调用重载函数会有三种可能的结果:
  • 编译器找到一个与实参 最佳匹配(best match) 的函数,并生成调用该函数的代码。
  • 找不到任何一个函数与调用的实参匹配,此时编译器发出 无匹配(no match) 的错误信息。
  • 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生名为 二义性调用(ambiguous call) 的错误。

6.4.1 重载与作用域

  1. 在不同作用域无法重载函数名,内层作用域声明的标识符会覆盖掉外层作用域的标识符(包括外层定义的函数或变量)。例如定义一个函数 fool ,在 main 函数定义一个名为 fool 的 int 型变量,则在 main 函数内使用 fool 代表一个 int 型变量。
  2. C++的名字查找发生在类型检查之前 。 C++ 先寻找同名标识符,若找到就忽略掉外层作用域的同名实体。

6.5 特殊用途语言特性

6.5.1 默认实参

  1. 默认形参(default argument) :在函数的定义时,就给形参进行了赋值,此时这个形参就叫做默认形参。
  2. 默认实参 :用于给默认形参初始化的值。
  3. 注意:在 C++ 中,一旦设置了一个默认形参,那么它后面(右边)的形参也都必须是默认形参。 例如这是错误的 void defa(int a, int b = 1, int c);//错误的, c 也必须是默认形参
  4. 如何使用默认形参:在调用函数时,省略掉该实参。
void screen(int = 0, int = 0, string = "red");//符合函数声明形参名可以被省略的规定
int main(){
  screen();  //调用全部的默认形参
  screen(66);  //相等于 screen(66, 0, "red"); 
  screen(66, 66);  //相等于 screen(66, 66, "red"); 
  screen(66, 66, "white");
}

注意:函数调用时 默认形参负责填补函数调用缺少的尾部实参(靠右位置)。所以一般设计含有默认形参的函数时,要将经常使用默认形参的参数位置提前(左移)。

screen(, , 66);//这是错误
  1. 函数的声明一般是放在头文件中,并且一个函数只声明一次,但是多次声明一个函数也是合法的。
  2. 不能重复定义同一个函数中的默认形参。
void screen(int a, int b = 0, int flag = 0);
void screen(int a, int b = 0, int flag = 1);//这是错误的

可以通过声明函数的形式给函数 添加默认形参 ,但是其它的形参不能赋值。

void screen(int a =1, int b, int flag);
//void screen(int a =1, int b = 0, int flag = 1); 这是错误的
//添加默认形参 a 后,相当于void screen(int a = 1, int b = 0, int flag = 0);
  1. 局部变量不能作为默认实参。
  2. 只要表达式的类型能转换成形参的类型,该表达式就能作为默认实参。
  3. 用作默认实参的表达式如果改变,则默认实参的值也会改变。
    比如:
//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,但是没有改变默认值
    window = screen();          //调用screen(ht(),80,'*')
}

6.5.2 内联函数和 constexpr 函数

  1. 调用函数会导致一些时间和空间上的开销:保存调用者保存寄存器和被调用者保存寄存器,并在函数返回时恢复;将参数拷贝进栈中;栈顶指针寄存器和基址指针寄存器的存取等操作会消耗时间和空间。
  2. 内联函数(inline function) :在每个调用点上“内联地”展开的函数,不会产生常规函数时间和空间上的开销,与宏类似。
  3. 内联函数的声明:在函数声明前面加上关键字 inline
    例如:
inline const string & shorterString(const string &s1, const string & s2)
{
  return s1.size() <= size() ? s1 : s2;
}
  1. 内联函数适用于优化规模较小、流程直接、频繁调用的函数。
  2. 很多编译器都不支持内联递归函数。
  3. constexpr 函数(constexpr function) :是指能用于常量表达式的函数。
  4. 定义 constexpr 函数的约定:

①函数的返回类型和所有形参的类型都必须是字面值类型,并且返回类型前还要加上关键字 constexpr ;
②函数体中有且只有一条 return 语句;
③函数体中的语句除了 return 语句外,其它语句必须是在运行时不执行任何操作的,例如空语句、声明类型别名、 using 声明等语句;

  1. constexpr 函数被隐式地指定为内联函数 ,以方便在编译过程中展开。
  2. 允许 constexpr 函数不返回常量,若有参数,且实参是常量表达式,则函数返回常量表达式。
//如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt){ return new_sz() * cnt; }

//当scale的实参是常量表达式时,它的表达式也是常量表达式,反之则不然
int arr[scale(2)];  //正确:scale(2)是常量表达式
int i = 2;          //i不是常量表达式
int a2[scale(i)];   //错误:scale(i)不是常量表达式
  1. 与其他函数不同,内联函数和 constexpr 函数可以定义多次,但是它的多个定义必须完全一致。 所以内联函数和 constexpr 函数一般都定义在头文件中。

6.5.3 调试帮助

  1. C++ 程序员可以通过一种技术,以便有选择地调式代码。基本思想是程序包含一些用于调试的代码,但是这些代码只会在开发程序时使用。当应用程序编写完准备发布时,要先屏蔽掉调试代码才能发布。这种方法用到两项预处理功能: assert 和 NDEBUG
  2. 断言( assert )预处理宏 : assert 是一个定义在头文件 assert.h ( C++ 还有一个 cassert )中的预处理宏。 assert(expr); 通过一个表达式 expr 作为它的条件,如果表达式为假,assert 输出信息并终止程序执行;如果表达式为真,则 assert 什么都不做。
  3. 预处理宏(preprocessor marco) :其实就是一个预处理变量,预处理器会在编译器进行编译前对所有的预处理宏进行处理替换。
    宏的名字在同一个程序内必须唯一,不能再定义一个同名的变量、对象、函数或其他用处的标识符。
  4. assert 的行为依赖于一个名为 NDEBUG 的预处理变量的状态。如果定义了宏 NDEBUG ,从而关闭了调试状态,则无论表达式为什么, assert 什么都不做。
    注意: NDEBUG 宏要在头文件 assert.h 或 cassert 前定义
#include 
#define NDEBUG
#include 

或是通过一个命令行选项 $ CC -D NDEBUG main.C # use /D with the Microsoft compiler ,这两个方法是等价的。

  1. 一般 debug 版本的 NDEBUG 不会自动定义的(但会自动定义一个名为 _DEBUG 的宏),但是 release 版本会自动定义 NDEBUG 。
  2. 除了使用 assert 外,还可以使用 NDEBUG 编写自己的条件调试代码,如果定义了宏 NDEBUG ,则忽略掉 #ifndef 和 #endif 之间的代码。
#ifdef NDEBUG
  //如果没有定义了 NDEBUG ,则执行 #ifndef 和 #endif 之间的代码
  //通过通过 __func__ 宏可以输出当前调试的函数名
  // cerr :输出到标准错误的一个 ostream 对象,常用于程序错误信息;
  cerr << __func__ <<  endl;
#endif 
  1. 除了 C++ 编译器定义的局部静态 char 数组 __func__ 外,预处理器还定义了另外4个对程序调试有用的宏:
作用
__FILE__ 存放文件名的字符串字面值
__LINE__ 存放当前行号的整型字面值
__TIME__ 存放文件编译时间的字符串字面值
__DATE__ 存放文件编译日期的字符串字面值

6.6 函数匹配

  1. 函数匹配的步骤:

①选定本次调用对应的重载函数集(候选函数);
②根据本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数(可行函数);
③根据本次调用提供的实参,从可行函数中选择与本次调用最匹配的函数(实参类型与形参类型越接近越好),如果有且只有一个函数满足以下条件则匹配成功。

  1. 该函数的 每个 实参的匹配 都不劣于 其他可行函数需要的匹配;
  2. 满足第一个条件下,至少有一个实参的匹配优于其他可行函数提供的匹配。
重载函数 f 有以下声明
void f(int, int);
void f(double, double = 3.14);//符合形参名可以被省略的规定
void f(int);

有以下调用(有对有错)
f(5.6);//该调用只有一个 double 实参,符合条件1和2,所以是正确调用
f(5.6, 1);//该调用只符合条件2,不符合条件1,所以产生二义性错误
  1. 候选函数(candidate function) :函数匹配时调选的重载函数集合中的函数。
    候选函数具有两个特征:

①与被调用的函数同名;
②其声明在调用点可见。

  1. 可行函数(viable function) :从候选函数中选出,能被这组实参调用的函数。
    可行函数具有两个特征:

①形参数量与本次调用提供的实参数量相同;
②每个对应的实参类型与形参类型相同,或是能(隐式)转换成形参类型。

  1. 如果函数含有默认形参,则我们在调用该函数时传入的实参数量可能少于它实际使用的实参数量。
  2. 如果没有找到可行函数,编译器则报告无匹配函数的错误。
  3. 如果没有找到最佳函数,编译器则报告二义性调用的错误。
  4. 调用重载函数时,应该尽量避免强制类型转换。如果使用了强制类型转换,就说明重载函数设计不合理。

6.6.1 实参类型转换

  1. 寻找函数匹配的最佳函数时, 如何判断每一个实参匹配的优劣 (1>2>3>4>5):
  1. 精确匹配,包括以下情况:
  • 实参类型和形参类型相同
  • 实参从 数组类型或函数类型转换成对应的指针类型
  • 向实参 添加 top-level-const (非常量实参传给 top-level const 形参)或者从实参中 删除 top-level const (常量实参传给非常量形参);(在函数重载中,顶层 const 形参与非顶层 const 实参是一样的)
  1. 通过 底层 const 转换 实现的匹配(非常量对象传给 low-level const 的引用或指向非常量对象的指针传给 low-level const 的指针);
  2. 通过 类型提升 实现的匹配(整型提升);
  3. 通过 算术类型转换指针转换 (4.11.2)实现的匹配;
  4. 通过 类类型转换 (14.9)实现的匹配。
  1. 通过类型提升实现的匹配与通过算术类型转换实现的匹配的区别:
void ff(int);
void ff(short);
int main(){
    ff(‘2’);
//此时会调用形参类型为 int 的函数
//因为将 char 类型提升为 int 类型的函数匹配比将 char 类型转换成 short 类型的匹配优先级要高
//第一种是类型提升的匹配,第二种是算术类型转换的匹配
}

因为所有通过算术类型转换实现的匹配优先级都是一样的,所以下面的函数调用会产生二义性错误:

void nn(double);
void nn(long);
int main(){
    nn(2);//会产生二义性错误,因为从 int 转换到 double 和 long 的匹配优先级是一样的
}

6.7 函数指针

  1. 函数指针指向的是某种特定类型的函数,而非对象。
  2. 函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
    例如:
//该函数的类型为 bool (const string&, const string&)
bool length(const string&, const string&);

如何声明一个可以指向该函数的指针,只需要用 *指针名 替换指向的函数声明中的函数名:

//该函数指针为 pf 
//如果不用 () ,那么 pf 就是一个返回类型为 bool* ,参数列表为 (const string&, const string&) 的函数
bool (*pf)(const string&, const string&);
  1. 当我们把函数名作为值使用时,函数名会被隐式转换成函数指针。 在函数名前加 & 表示该表达式的类型是一个指向该函数的指针。
//两个表达式都是一样的
pf = length;
pf = &length;

上面两个赋值表达式是等价的,但是 length&length 的类型是不相同的,只是 length 在这里隐式转换成了一个指向 length 函数的函数指针,也就是 &length

  1. 可以通过使用函数指针来调用函数。
    例如:
length("ss", "ss");
// pf 是指向 length 的函数指针
(*pf)("ss", "ss");//推荐第一种
pf("ss", "ss");//这两种方式是等价的
  1. 在指向不同函数类型的指针间不存在转换规则。但是可以用 nullptr 或者值为 0 的整型常量表达式给所有类型的函数指针赋值,表示该指针没有指向任何一个函数。
    例如:
pf = 0;
pf = nullptr;
  1. 当我们使用重载函数时,上下文必须清晰地区分到底选用哪个重载函数。 编译器通过 指针类型 决定选用哪个重载函数,指针类型必须与重载函数中的某一个 精确匹配 (必须是精确匹配)。
  2. 和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。
//隐式声明, s3 是函数类型,它会自动转换成指向函数的指针
void useBigger(const string &s1, const string &s2, 
               bool s3(const string &, const string &) );
//等价的声明:显示地将形参 s3 定义为指向函数的指针
void useBigger(const string &s1, const string &s2, 
               bool (*s3)(const string &, const string &) );

调用方法如下,可以直接使用函数名,因为函数名会自动转换成指向函数类型的指针。

// lengthCompare 是对应类型的函数
useBigger(s1, s2, lengthCompare);
  1. 可以使用类型别名简化使用函数指针的代码,同时加深理解上文。
// Func 和 Func2 是函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; //等价的类型
// FuncP 和 FuncP2 是指向函数的指针
typedef bool (*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2; //等价的类型

decltype 关键字作用于函数名则返回函数类型,此时不会将函数类型自动转换成指针类型。
可以使用以下形式重新声明 useBigger 函数:

// useBigger 的两个等价声明,都使用了类型别名
//第一个函数中编译器自动将 Func2 形参类型转换成 Func2
void useBigger(const string &s1, const string &s2, Func2);
void useBigger(const string &s1, const string &s2, FuncP2);
  1. 和数组类似,虽然不能返回数组类型的值,但是可以返回指向函数类型的指针。 在不使用类型别名的情况下,如果想定义一个返回函数指针的函数,则函数头格式如下: type (*function(parameter_list1)) (parameter_list2); 。其中 type 是指针所指向的函数的返回类型, function 是函数名, parameter_list1 是该函数的参数列表, parameter_list2 是指针所指向的函数的参数列表。例如
int (* f1(int)) (int*, int);

函数的声明是从外到内理解:函数名所在括号外有返回类型和参数列表,再加上函数名前有 * 符号证明该函数返回一个函数指针。
也可以使用类型别名简化并理清函数:

using F = int (int*, int); // F 是函数类型
using PF = int (int*, int); // PF 是指针类型
//等价声明
F* f1(int);
PF f1(int);

当然我们还可以 使用尾置返回类型的方式声明一个返回函数指针的函数:

//等价声明
auto f1(int) -> int (*)(int *, int);
  1. 当已知函数返回指针类型指向哪一个函数,则可以使用 decltype 关键字来作用该函数名,但是注意, decltype 取到的是函数类型而非指针类型,所以要在函数名前加上 * 。例如
decltype(useBigger) *f1(int);

你可能感兴趣的:(第六章 函数)