Loading

C++Primer 第6章

第6章 函数

本章首先介绍函数的定义和声明,包括参数如何传入函数以及函数如何返回结果。在C++语言中允许重载函数,也就是几个不同的函数可以使用同一个名字。所以接下来我们介绍重载函数的方法,以及编译器如何从函数的若于重载形式中选取一个与调用匹配的版本。最后,我们将介绍一些关于函数指针的知识。

函数是一个命名了的代码块,我们通过调用函数执行相应的代码。函数可以有0个或多个参数,而且(通常)会产生一个结果。可以重载函数,也就是说,同一个名字可以对应几个不同的函数。

6.1 函数基础

一个典型的函数(function)定义包括以下部分:返回类型(return type)、函数字由0个或多个形参(parameter)组成的列表以及函数体。其中,形参以逗号隔开,形参的列表位于一对圆括号之内。函数执行的操作在语句块(参见5.1节)中说明该语句块称为函数体(function body)
我们通过调用运算符(calloperator)来执行函数。调用运算符的形式是一对圆括号它作用于一个表达式,该表达式是函数或者指向函数的指针:圆括号之内是一个用逗号隔开的实参 (argument)列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。

编写函数

举个例子,我们准备编写一个求数的阶乘的程序。

int fact(int val)
{
    int ret = 1;	//局部变量,用于保存计算结果
    while(ret > 1)
        ret *= val--;
    return ret;		//返回结果
}

函数的名字是fact,它作用于一个整型参数,返回一个整数值。在while循环内部,在每次迭代时用后置递减运算符将val的值减1。return语句负责结束fact并返回ret的值。

调用函数

要调用fact函数,必须提供一个整数值,调用得到的结果也是一个整数:

int main()
{
    int j = fact(5);
    cout << "5! is " << j << endl;
    return 0;
}

函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。

执行函数的第一步是(隐式地)定义并初始化它的形参。因此,当调用fact函数时首先创建一个名为val的int变量,然后将它初始化为调用时所用的实参5。
当遇到一条return语句时函数结束执行过程。和函数调用一样,return语句也完成两项工作:一是返回 return 语句中的值(如果有的话),二是将控制权从被调函数转移回主调函数。函数的返回值用于初始化调用表达式的结果,之后继续完成调用所在的表达式的剩余部分。

形参和实参

实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推。尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序(参见4.1.3节)。编译器能以任意可行的顺序对实参求值。
实参的类型必须与对应的形参类型匹配,这一点与之前的规则是一致的,我们知道在初始化过程中初始值的类型也必须与初始化对象的类型匹配。函数有几个形参,我们就必须提供相同数量的实参。因为函数的调用规定实参数量应与形参数量一致,所以形参一定会被初始化。

函数的形参列表

函数的形参列表可以为空,但是不能省略。要想定义一个不带形参的函数,最常用的办法是书写一个空的形参列表。不过为了与C语言兼容,也可以使用关键字 void表示函数没有形参:

void f1() { /* ... */}			//隐式地定义空形参列表
void f2(void) { /* ... */}		//显式地定义空形参列表s

形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明。即使,两个形参的类型一样,也必须把两个类型都写出来:

int f3(int v1, v2) {/* ... */}		//错误
int f4(int v1, int v2) {/* ... */}	//正确

任意两个形参都不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。
形参名是可选的,但是由于我们无法使用未命名的形参,所以形参一般都应该有个名字。偶尔,函数确实有个别形参不会被用到,则此类形参通常不命名以表示在函数体内不会使用它。不管怎样,是否设置未命名的形参并不影响调用时提供的实参数量。即使某个形参不被函数使用,也必须为它提供一个实参。

函数返回类型

大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何值。函数的返回类型不能是数组(参见3.5节)类型或函数类型,但可以是指向数组或函数的指针。我们将在6.3.3节绍如何定义一种特殊的函数,它的返回值是数组的指针(或引用),在6.7节将介绍如何返回指向函数的指针。

6.1.1 局部对象

在C++语言中,名字有作用域(参见2.2.4节),对象有生命周期(lifetime)。

  • 名字的作用域是程序文本的一部分,名字在其中可见。
  • 对象的声明周期是程序执行过程中该对象存在的一段时间。

如我们所知,函数体是一个语句块。块构成一个新的作用域,我们可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量(localvariable)。它们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏 (hide)在外层作用域中同名的其他所有声明中。
在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁。局部变量的生命周期依赖于定义的方式。

自动对象

对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象(automatic object)。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
我们用传递给函数的实参初始化形参对应的自动对象。对于局部变量对应的自动对象来说,则分为两种情况:如果变量定义本身含有初始值,就用这个初始值进行初始化;否则,如果变量定义本身不含初始值,执行默认初始化(参见2.2.1 节)。这意味着内置类型的未初始化局部变量将产生未定义的值。

局部静态对象

某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象(localstatic object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
举个例子,下面的函数统计它自己被调用了多少次,这样的函数也许没什么实际意义但是足够说明问题:

size_t count_calls()
{
    static size_t ctr = 0;	//调用结束后,这个值仍然有效
    return ++ctr;
}

int main()
{
    for(size_t i = 0; i != 10; ++i)
        cout << count_calls() << endl;
    return 0;
}

这段程序将输出从1到10(包括10在内)的数字。
如果局部静态变量没有显式的初始值,它将执行值初始化(参见3.3.1节),内置类型的局部静态变量初始化为0。

6.1.2 函数声明

和其他名字一样,函数的名字也必须在使用之前声明。类似于变量(参见 2.2.2 节),函数只能定义一次,但可以声明多次。唯一的例外是如 15.3 节将要介绍的,如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。
因为函数的声明不包含函数体,所以也就无须形参的名字。事实上,在函数的声明中经常省略形参的名字。尽管如此,写上形参的名字还是有用处的,它可以帮助使用者更好地理解函数的功能。

函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型(function prototype)

在头文件中进行函数声明

回忆之前所学的知识,我们建议变量在头文件(参见2.6.3节)中声明,在源文件中定义。与之类似,函数也应该在头文件中声明而在源文件中定义。
看起来把函数的声明直接放在使用该函数的源文件中是合法的,也比较容易被人接受;但是这么做可能会很烦琐而且容易出错。相反,如果把函数声明放在头文件中,就能确保同一函数的所有声明保持一致。而且一旦我们想改变函数的接口,只需改变一条声明即可。
定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。

6.1.3 分离式编译

随着程序越来越复杂,我们希望把程序的各个部分分别存储在不同文件中。例如,可以把 6.1 节练习的函数存在一个文件里,把使用这些函数的代码存在其他源文件中。为了允许编写程序时按照逻辑关系将其划分开来,C++语言支持所谓的分离式编译(separate compilation)。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

编译和链接多个源文件

举个例子,假设 fact 函数的定义位于一个名为 fact.cc 的文件中,它的声明位于名为Chapter6.h 的头文件中。显然与其他所有用到 fact 函数的文件一样,fact.cc应该包含chapter6.h头文件。另外,我们在名为 factMain.cc 的文件中创建main函数,main函数将调用 fact 函数。要生成可执行文件 (executable file),必须告诉编译器我们用到的代码在哪里。对于上述几个文件来说,编译的过程如下所示:

$ CC factMain.cc fact.cc # generates factMain.exe or a.out
$ CC factMain.cc fact.cc -o main # generates main or main.exe

其中,CC是编译器的名字,S是系统提示符,#后面是命令行下的注释语句。接下来运行可执行文件,就会执行我们定义的main函数。
如果我们修改了其中一个源文件,那么只需重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(Windows)或.o(UNIX)的文件,后缀名的含义是该文件包含对象代码 (object code)。
接下来编译器负责把对象文件链接在一起形成可执行文件。在我们的系统中,编译的过程如下所示:

$ CC -c factMain.cc # generates factMain.o
$ CC -c fact.cc # generates fact.o
$ CC factMain.o fact.o # generates factMain.exe or a.out
$ CC factMain.o fact.o -o main # generates main or main.exe

6.2 参数传递

如前所述,每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。

形参初始化的机理与变量初始化一样。

和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型(参见2.3.1节),它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。

当形参是引用类型时,我们说它对应的实参被引用传递(passed byreference)或者函数被传引用调用(called byreference)。和其他引用一样,引用形参也是它绑定的对象的别名:也就是说,引用形参是它对应的实参的别名。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用(called by value)。

6.2.1 传值参数

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。

传值参数的机理完全一样,函数对形参做的所有操作都不会影响实参。

指针形参

指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。

熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参替代指针。

6.2.2 传引用参数

回忆过去的知识,我们知道对于引用的操作实际上是作用在引用所引的对象上(参见2.3.1节),引用形参的行为与之类似。通过使用引用形参,允许函数改变一个或多个实参的值。

使用引用避免拷贝

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
举个例子,我们准备编写一个函数比较两个 string 对象的长度。因为 string 对象可能会非常长,所以应该尽量避免直接拷贝它们,这时使用引用形参是比较明智的选择。又因为比较长度无须改变 string 对象的内容,所以把形参定义成对常量的引用(参见2.4.1节):

//比较两个string对象的长度
bool isShorter(const strinq &s1,const string &s2)
	return sl.size() < s2.size();

如6.2.3节将要介绍的,当函数无需修改引用形参的值时最好使用常量引用。

使用引用形参返回额外信息

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。举个例子,我们定义一个名为 find_char 的函数,它返回在string对象中某个指定字符第一次出现的位置。同时,我们也希望函数能返回该字符出现的总次数。
该如何定义函数使得它能够既返回位置也返回出现次数呢?一种方法是定义一个新的数据类型,让它包含位置和数量两个成员。还有另一种更简单的方法,我们可以给函数传入一个额外的引用实参,令其保存字符出现的次数:

//返回s中c第一次出现的位置索引
//引用形参occurs负责统计c出现的总次数
string::size type find char(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;	//记录c第一次出现的位置
            ++occurs;		//将出现的次数加 1
        }
    }
    return ret;			//出现次数通过occurs隐式地返回
}

当我们调用 find_char函数时,必须传入三个实参:作为查找范围的一个string 对象要找的字符以及一个用于保存字符出现次数的 size type(参见3.2.2 节)对象。假设s是一个string对象,ctr 是一个size type对象,则我们通过如下形式调用findchar函数:

auto index = find_char(s,'o',ctr);

调用完成后,如果 string 对象中确实存在o,那么ctr 的值就是o出现的次数,index指向o第一次出现的位置;否则如果 string 对象中没有o,index 等于 s.size()而ctr等于0。

6.2.3 const形参和实参

当形参是const时,必须要注意2.4.3节关于顶层const的讨论。如前所述,顶层const作用于对象本身

const int ci = 42;	//不能改变ci,const是顶层的
int i = ci;			//正确:当拷贝ci时,忽略了它的顶层const
int * const p = &i;	//const是顶层的,不能给p赋值
*p = 0;				//正确:通过p改变对象的内容是允许的,现在i变成了0

和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的:

void fcn(const int i) { /*fcn能够读取i,但是不能向i写值*/}

调用fcn函数时,既可以传入const int也可以传入int。忽略掉形参的顶层const可能产生意想不到的结果:

void fcn(const int i) { /*fcn能够读取i,但是不能向i写值*/}
void fcn(int i){/* ... */}	//错误:重复定义了fcn(int)

在C++语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为顶层const被忽略掉了,所以在上面的代码中传入两个fcn函数的参数可以完全一样。因此第二个fcn是错误的,尽管形式上有差异,但实际上它的形参和第一个fcn的形参没什么不同。

指针或引用形参与const

形参的初始化方式和变量的初始化方式是一样的,所以回顾通用的初始化规则有助于理解本节知识。我们可以使用非常量初始化一个底层 const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。

inti=42;
const int *cp = &i;	//正确:但是cp不能改变i(参见2.4.2节)
const int &r = i; 	//正确:但是r不能改变i(参见2.4.1节)
const int &r2 = 42;	//正确:(参见2.4.1节)
int *p = cp;		//错误:p的类型和cp的类型不匹配(参见2.4.2节)
int &r3 = r;		//错误:r3的类型和r的类型不匹配(参见2.4.1节)
int &r4 = 42;		//错误:不能用字面值初始化一个非常量引用(参见2.3.1节)

将同样的初始化规则应用到参数传递上可得如下形式:

int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i);		//调用形参类型是int*的reset函数
reset(&ci);		//错误:不能用指向const int对象的指针初始化int*
reset(i);		//调用形参类型是int&的reset函数
reset(ci);		//错误:不能把普通引用绑定到const对象ci上
reset(42);		//错误:不能把普通应用绑定到字面值上
reset(ctr);		//错误:类型不匹配,ctr是无符号类型
//正确:find_char的第一个形参是对常量的引用
find_char("Hello World!", 'o', ctr);

要想调用引用版本的reset(参见6.2.2节),只能使用int类型的对象而不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型的对象。类似的,要想调用指针版本的 reset (参见6.2.1节)只能使用 int*。
另一方面,我们能传递一个字符串字面值作为 find_char(参见6.2.2节)的第一个实参,这是因为该函数的引用形参是常量引用,而 C++允许我们用字面值初始化常量引用。

尽量使用常量引用

把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。就像刚刚看到的,我们不能把 const 对象、字面值或者需要类型转换的对象传递给普通的引用形参。
这种错误绝不像看起来那么简单,它可能造成出人意料的后果。以 6.2.2 节的find_char函数为例,那个函数(正确地)将它的string类型的形参定义成常量引用。假如我们把它定义成普通的string&:

//不良设计:第一个形参的类型应该是const string&
string::size_type find_char(string &s, char c, string::size_type &occurs);

则只能将find_char函数作用于string对象。类似下面这样的调用

find_char("Hello Wolrd", 'o', ctr);

将在编译时发生错误。

还有一个更难察觉的问题,假如其他函数(正确地)将它们的形参定义成常量引用,那么第二个版本的 find_char 无法在此类函数中正常使用。举个例子,我们希望在一个判断string对象是否是句子的函数中使用find_char:

bool is_sentence(const string &s)
{
    //如果在s的末尾有且只有一个句号,则s是一个句子
    string::size_type ctr = 0;
    return find_char(s,'.',ctr) == s.size() - 1 && ctr == 1;
}

如果find_char的第一个形参类型是string&,那么上面这条调用 find_char 的语句将在编译时发生错误。原因在于s是常量引用,但 find_char被(不正确地)定义成只能接受普通引用。
解决该问题的一种思路是修改 is_sentence 的形参类型,但是这么做只不过转移了错误而已,结果是is_sentence函数的调用者只能接受非常量 string 对象了。
正确的修改思路是改正 find_char 函数的形参。如果实在不能修改 find_char,就在 is_sentence内部定义一个 string 类型的变量,令其为 s 的副本,然后把这个string对象传递给find_char。

6.2.4 数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组(参见 3.5.1节)以及使用数组时(通常)会将其转换成指针(参见3.5.3节)。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:

void print(const int*);
void print(const int[]);	//可以看出来,函数的意图是作用于一个数组
void print(const int[10]);	//这里的维度表示我们期望数组含有多少元素,实际不一定

尽管表现形式不同,但上面的三个函数是等价的:每个函数的唯一形参都是 const int*类型的。当编译器处理对 print 函数的调用时,只检查传入的参数是否是 const int*类型。

和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。

因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。

使用标记指定数组长度

管理数组实参的第一种方法是要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串(参见3.5.4节)。C风格符存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数在处理C 风格字符串时遇到空字符停止:

void print(const char *cp)
{
    if(cp)
        while(*cp)
            cout << *cp++;
}

这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况,但是对于像int这样所有取值都是合法值的数据就不太有效了。

使用标准库规范

管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发,关于其细节将在第I部分详细介绍。使用该方法,我们可以按照如下形式输出元素内容:

void print(const int *beg, const int *end)
{
    //输出beg到end之间(不含end)的所有元素
    while(beg != end)
        cout << *beg++ << endl;
}

为了调用这个函数,我们需要传入两个指针:一个指向要输出的首元素,另一个指向尾元素的下一位置:

int j[2] = {0,1};
//j转换成指向它首元素的指针
//第二个实参是指向j的尾后元素的指针
print(begin(j), end(j));

只要调用者能正确地计算指针所指的位置,那么上述代码就是安全的。在这里,我们使用标准库begin和end函数(参见3.5.3节)提供所需的指针。

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

第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。

void print(const int ia[], size_t size)
{
    for(size_t i = 0; i != size; ++i){
        cout << ia[i] << endl;
    }
}

这个版本的程序通过形参size的值确定要输出多少个元素,调用print函数时必须传入这个表示数组大小的值:

int j[] = {0,1};
print(j, end(j) - begin(j));

只要传递给函数的size值不超过数组实际的大小,函数就是安全的。

数组形参和const

我们的三个print函数都把数组形参定义成了指向const的指针,6.2.3节关于引用的讨论同样适用于指针。当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针(参见2.4.2节)。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。

数组引用形参

C++语言允许将变量定义成数组的引用(参见3.5.1节),基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上:

void print(int (&arr)[10])
{
    for(auto elem : arr)
        cout << elem << endl;
}

&arr两端的括号必不可少(参见3.5.1节)

f(int &arr[10])		//错误:将arr声明成了引用的数组
f(int (&arr)[10])	//正确:arr是具有10个整数的整型数组的引用

因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用数组。但是,这一用法也无形中限制了print 函数的可用性,我们只能将函数作用于大小为10的数组。

16.1.1节将要介绍我们应该如何编写这个函数,使其可以给引用类型的形参传递任意大小的数组。

传递多维数组

我们曾经介绍过,在C++语言中实际上没有真正的多维数组(参见3.6节),所谓多维数组其实是数组的数组。
和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针(参见3.6节)。因为我们处理的是数组的数组,所以首元素本身就是一个数组指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的部分,不能省略:

//matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize) { /*...*/}

再一次强调,*matrix两端的括号必不可少:

int *matrix[10];		//10个指针构成的数组
int (*matrix)[10];		//指向含有10个整数的数组的指针

我们也可以用使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表内:

void print(int matrix[][10], int rowSize)	{/*...*/}

matrix的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针。

6.2.5 main:处理命令行选项

main函数是演示C++程序如何向函数传递数组的好例子。到目前为止,我们定义的main函数都只有空形参列表:

int main() {...}

然而,有时我们确实需要给 main 传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。例如,假定 main 函数位于可执行文件 prog 之内,我们可以向程序传递下面的选项:

prog -d -o ofile data0

这些命令行选项通过两个(可选的)形参传递给main函数:

int main(int argc, char *argv[]) {...}

第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:

int main(int argc, char **argv) {...}

其中argv指向char*。

当实参传给main函数之后,argv的第一个元素指向程序的名字或一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。

以上面提供的命令行为例,argc应该等于5,argv应该包含以下的C风格字符串:

argv[0] = "prog";		//或者argv[0]也可以指向一个空字符串
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;

当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。

6.2.6 含有可变形参的函数

有时我们无法提前预知应该向函数传递几个实参。例如,我们想要编写代码输出程序产生的错误信息,此时最好用同一个函数实现该项功能,以便对所有错误的处理能够整齐划一。然而,错误信息的种类不同,所以调用错误输出函数时传递的实参也各不相同。

为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板,关于它的细节将在16.4节介绍。
C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。本节将简要介绍省略符形参,不过需要注意的是,这种功能一般只用于与C函数交互的接口程序。

initializer_list 形参

如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组(参见3.5节)。initializer_list类型定义在同名的头文件中,它提供的操作如表6.1所示。

image-20221205171016104

和vector一样,initializer_list也是一种模板类型(参见3.3节)。定义initializer_list对象时,必须说明列表中所含元素的类型:

initializer_list<string> ls;	//initializer_list的元素类型是string
initializer_list<int> li;		//initializer_list的元素类型是int

和vector不一样的是,initiazlier_list对象中的元素永远是常量值,我们无法改变initiazlier_list对象中元素的值。

我们使用如下的形式编写输出错误信息的函数,使其可以作用于可变数量的实参:

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

作用于initializer_list对象的begin和end操作类似于vector对应的成员(参见3.4.1节)。begin()成员提供一个指向列表首元素的指针,end()成员提供一个指向列表尾后元素的指针。

如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:

//expected和actual是string对象
if(expected != actual)
    error_msg({"functionX", expected, actual});
else
    error_msg({"functionX", "okay"});

在上面的代码中我们调用了同一个函数error_msg,但是两次调用传递的参数数量不同;第一次调用传入了三个值,第二次调用只传入了两个。

含有initializer_list形参的函数也可以同时拥有其他形参。

void error_msg(ErrCode e, initializer_list<string> il)
{
    cout << e.msg() << ": ";
    for(const auto &elem : il)
        cout << elem << " ";
    cout << endl;
}

为了调用这个版本的error_msg函数,需要额外传递一个ErrCode实参:

if(expected != actual)
    error_msg(ErrCode(42),{"functionX", expected, actual});
else
    error_msg(ErrCode(0),{"functionX", "okay"});

省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。你的C编译器文档会描述如何使用varargs。

省略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。

省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:

void foo(parm_list, ...);
void foo(...);

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

6.3 返回类型和return语句

return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。return语句有两种形式:

return;
return expression;

6.3.1 无返回值函数

没有返回值的return语句只能用在返回类型是void的函数中。返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式地执行return。
通常情况下,void函数如果想在它的中间位置提前退出,可以使用 return 语句。return的这种用法有点类似于用break语句(参见5.5.1节)退出循环例如,可以编写一个 swap 函数,使其在参与交换的值相等时什么也不做直接退出:

void swap(int &vl,int &v2)
{
    // 如果两个值是相等的,则不需要交换,直接退出
    if (vl==v2)
    	return;//如果程序执行到了这里,说明还需要继续完成某些功能
    int tmp=v2;
    v2=vl;
    vl=tmp;
    //此处无须显式的return语句
}

一个返回类型是void的函数也能使用return语句的第二种形式,不过此时return语句的expression必须是另一个返回 void 的函数。强行令 void 函数返回其他类型的表达式将产生编译错误。

6.3.2 有返回值的函数

return 语句的第二种形式提供了函数的结果。只要函数的返回类型不是 void,则该函数内的每条return语句必须返回一个值。return 语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成(参见4.11节)函数的返回类型。
尽管C++无法确保结果的正确性,但是可以保证每个return语句的结果类型正确。也许无法顾及所有情况,但是编译器仍然尽量确保具有返回值的函数只能通过一条有效的return语句退出。例如:

// 因为含有不正确的返回值,所以这段代码无法通过编译
bool str_subrange(const string &strl,const string &str2)
{
    // 大小相同:此时用普通的相等性判断结果作为返回值
    if (strl.size() == str2.size())
        return str1 == str2;//正确:==运算符返回布尔值
    //得到较短string对象的大小,条件运算符参见第4.7节(134页)
    auto size=(strl.size() < str2.size()) ? 
        strl.size() :str2.size();// 检查两个string 对象的对应字符是否相等,以较短的字符串长度为限
    for (decltype(size) i=0;i!= size;++i)
    {
        if (strl[i] != str2[i])
        return;//错误 #1:没有返回值,编译器将报告这一错误
    }
    //错误 #2:控制流可能尚未返回任何值就结束了函数的执行
    //编译器可能检查不出这一错误
}

在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。很多编译器都无法发现此类错误。

值是如何被返回的

返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
必须注意当函数返回局部变量时的初始化规则。例如我们书写一个函数,给定计数值、单词和结束符之后,判断计数值是否大于1:如果是,返回单词的复数形式;如果不是,返回单词原形:

//如果ctr的值大于1,返回word的复数形式
string make plural(size t ctr, const string &word,const string &ending)
	return (ctr > 1) ? word + ending : word;

该函数的返回类型是string,意味着返回值将被拷贝到调用点。因此,该函数将返回word的副本或者一个未命名的临时 string 对象,该对象的内容是 word 和 ending 的和。
同其他引用类型一样,如果函数返回引用,则该引用仅是它所引对象的一个别名。举个例子来说明,假定某函数挑出两个 string 形参中较短的那个并返回其引用:

//挑出两个string 对象中较短的那个,返回其引用
const string &shorterString(const string &sl,const string &s2)
	return sl.size() <= s2.size() ? s :s2;

其中形参和返回类型都是 const string 的引用,不管是调用函数还是返回结果都不会真正拷贝string对象。

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

函数完成后,它所占用的存储空间也随之被释放掉(参见 6.1.1 节)。因此函数终止意味着局部变量的引用将指向不再有效的内存区域:

//严重错误:这个函数试图返回局部对象的引用
const string &manip()
{
    string ret;
    //以某种方式改变一下ret
    if (!ret.empty())
        return ret; //错误:返回局部对象的引用
    else
    	return "Empty";//错误:"Empty"是一个局部临时量
}

要想确保返回值安全,我们不妨提问,引用所引的是在函数之前已经存在的哪个对象?

返回类类型的函数和调用运算符

和其他运算符一样,调用运算符也有优先级和结合律(参见4.1.2节)。调用运算符的优先级与点运算符和箭头运算符(参见4.6节)相同,并且也符合左结合律。因此,如果函数返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员。
例如,我们可以通过如下形式得到较短 string 对象的长度:

//调用string对象的size成员,该string对象是由shorterString函数返回的
auto sz = shorterString(sl,s2).size();

因为上面提到的运算符都满足左结合律,所以 shorterString 的结果是点运算符的左侧运算对象,点运算符可以得到该string 对象的size成员,size又是第二个调用运算符的左侧运算对象。

引用返回左值

函数的返回类型决定函数调用是否是左值(参见4.1.1 节)。调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值:

char &get_val(string &str,string::size_type ix)
	return str[ix];//get_val假定索引值是有效的

int main()
{
    string s("a value");
    cout << s << endl;//输出a value
    get_val(s,0)='A';//将s[0]的值改为A
    cout << s << endl;//输出A value
	return 0;
}

把函数调用放在赋值语句的左侧可能看起来有点奇怪,但其实这没什么特别的。返回值是引用,因此调用是个左值,和其他左值一样它也能出现在赋值运算符的左侧。
如果返回类型是常量引用,我们不能给调用的结果赋值,这一点和我们熟悉的情况是一样的:

shorterString("hi","bye") = "X";//错误:返回值是个常量

列表初始化返回值

C++11新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化(参见3.3.1节);否则,返回的值由函数的返回类型决定。
举个例子,回忆6.2.6节的error_msg 函数,该函数的输入是一组可变数量的string实参,输出由这些string对象组成的错误信息。在下面的函数中,我们返回一个vector对象,用它存放表示错误信息的string对象:

vector<string> process()
{
    //...
	//expected 和 actual 是string对象
	if (expected.empty())
        return {};						// 返回一个空 vector 对象
    else if (expected == actual)
        return {"functionx","okay"};	//返回列表初始化的 vector 对象
	else
		return {"functionx",expected,actual};
}

如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间(参见 2.2.1 节)。如果函数返回的是类类型,由类本身定义初始值如何使用 (参见3.3.1节)。

主函数main的返回值

之前介绍过,如果函数的返回类型不是 void,那么它必须返回一个值。但是这条规则有个例外:我们允许main函数没有return语句直接结束。如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。
如1.1节介绍的,main 函数的返回值可以看做是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量 (参见2.3.2节),我们可以使用这两个变量分别表示成功与失败:

int main()
{
    if(some_failure)
        return EXIT_FAILURE;
    else
        return EXIT_SUCCESS;
}

因为它们是预处理变量,所以既不能在前面加上std::,也不能在using 声明中出现。

递归

如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。举个例子,我们可以使用递归函数重新实现求阶乘的功能:

//计算val的阶乘,即1*2*3...*val
int factorial(int val)
{
    if (val>1)
		return factorial(val-1) * val;
    return 1;
}

在递归函数中,一定有某条路径是不包含递归调用的;否则,函数将“永远”递归下去,换句话说,函数将不断地调用它自身直到程序栈空间耗尽为止。我们有时候会说这种函数含有递归循环 (recursion loop)。在 factorial函数中,递归终止的条件是 val等于1。

main函数不能调用它自己。

6.3.3 返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用(参见3.5.1节)。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名(参见2.5.1节):

typedef int arrT[10];//arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT = int[10];//arrT的等价声明,参见2.5.1节
arrT* func(int i);//func 返回一个指向含有10个整数的数组的指针

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

要想在声明func 时不使用类型别名,我们必须牢记被定义的名字后面数组的维度:

int arr[10];			//arr是一个含有10个整数的数组
int *p1[10];			//p1是一个含有10个指针的数组
int (*p2)[10] = &arr;	//p2是一个指针,它指向含有10个整数的数组

和这些声明一样,如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。因此,返回数组指针的函数形式如下所示:

Type (*function(parameter_list))[dimension]

举个具体点的例子,下面这个func函数的声明没有使用类型别名:

int (*func(int i))[10];

可以按照以下的顺序来逐层理解该声明的含义:

  • func(int i)表示调用func函数时需要一个int类型的实参
  • (*func(int i))意味着我们可以对函数调用的结果执行解引用操作
  • (*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组。
  • int (*func(int i))[10]表示数组中的元素是int类型。

使用尾置返回类型

在C++11新标准中还有一种可以简化上述 func 声明的方法,就是使用尾置返回类型(trailing return type)。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto:

// func 接受一个int 类型的实参,返回一个指针,该指针指向含有 10个整数的数组
auto func(inti) -> int(*)[10];

因为我们把函数的返回类型放在了形参列表之后,所以可以清楚地看到 func 函数返回的是一个指针,并且该指针指向了含有 10 个整数的数组。

使用decltype

还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。例如,下面的函数返回一个指针,该指针根据参数i的不同指向两个已知数组中的某一个:

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

arrPtr 使用关键字 decltype 表示它的返回类型是个指针,并且该指针所指的对象与odd的类型一致。因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。有一个地方需要注意:decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,要想表示arrPtr 返回指针还必须在函数声明时加一个*符号。

6.4 函数重载

如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数。例如,在6.2.4节我们定义了几个名为print的函数:

void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);

这些函数接受的形参类型不一样,但是执行的操作非常类似。当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数:

函数的名字仅仅是让编译器知道它调用的是哪个函数,而函数重载可以在一定程度上减轻程序员起名字、记名字的负担。

main函数不能重载

重载和const形参

如6.2.3节介绍的,顶层 const (参见2.4.3节)不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:

Record lookup(Phone);
Record lookup(const Phone);		//重复声明了Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const);	//重复声明了Record lookup(Phone*)

在这两组函数声明中,每一组的第二个声明和第一个声明是等价的。

另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:

//对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
//定义了4个独立的重载函数
Record lookup(Account&);		//函数作用于Account的引用
Record lookup(const Account&);	//新函数,作用于常量引用

Record lookup(Account*);		//新函数,作用于指向Account的指针
Record lookup(const Account*); 	//新函数,作用于指向常量的指针

在上面的例子中,编译器可以通过实参是否是常量来推断应该调用哪个函数。因为const不能转换成其他类型(参见4.11.2节),所以我们只能把const对象 (或指向const的指针)传递给const形参。相反的,因为非常量可以转换成const,所以上面的4个函数都能作用于非常量对象或者指向非常量对象的指针。不过,如6.6.1节将要介绍的,当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。

建议:何时不应该重载函数

尽管函数重载能在一定程度上减轻我们为函数起名字、记名字的负担,但是最好只重载那些确实非常相似的操作。有些情况下给函数起不同的名字能使得程序更易理解。举个例子,下面是几个负责移动屏幕光标的函数:
Screen& moveHome();
Screen& moveAbs(int,int);
Screen& moveRel(int,int,string direction);
乍看上去,似乎可以把这组函数统一命名为move,从而实现函数的重载:
Screen& move();
Screen& move(int,int);
Screen& move(int,int,string direction);
其实不然,重载之后这些函数失去了名字中本来拥有的信息。尽管这些函数确实都是在移动光标,但是具体移动的方式却各不相同。以moveHome为例,它表示的是移动光标的一种特殊实例。一般来说,是否重载函数要看哪个更容易理解。

const_cast和重载

在4.11.3节中我们说过,const_cast在重载函数的情景中最有用。举个例子,回忆6.3.2节的shorterString函数:

//比较两个string 对象的长度,返回较短的那个引用
const string &shorterString(const string &sl,const string &s2)
	return s1.size() <=s2.size() ? s1 : s2;

这个函数的参数和返回类型都是 const string 的引用。我们可以对两个非常量的string实参调用这个函数,但返回的结果仍然是const string 的引用。因此我们需要一种新的 shorterstring 函数,当它的实参不是常量时,得到的结果是一个普通的引用,使用const_cast可以做到这一点:

string &shorterString(string &s1,string &s2)
{
	auto &r = shorterString(const_cast<const string&>(s1),const_cast<const string&>(s2));
	return const_cast<string&>(r);
}

在这个版本的函数中,首先将它的实参强制转换成对 const 的引用,然后调用了shorterString 函数的const 版本。const 版本返回对 const string 的引用,这个引用事实上绑定在了某个初始的非常量实参上。因此,我们可以再将其转换回一个普通的string&,这显然是安全的。

调用重载的函数

定义了一组重载函数后,我们需要以合理的实参调用它们。函数匹配 (function matching)是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定(overload resolution)。编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。
在很多(可能是大多数)情况下,程序员很容易判断某次调用是否合法,以及当调用合法时应该调用哪个函数。通常,重载集中的函数区别明显,它们要不然是参数的数量不同,要不就是参数类型毫无关系。此时,确定调用哪个函数比较容易。但是在另外一些情况下要想选择函数就比较困难了,比如当两个重载函数参数数量相同且参数类型可以相互转换时(第4.11节)。我们将在6.6节介绍当函数调用存在类型转换时编译器处理的方法。
现在我们需要掌握的是,当调用重载函数时有三种可能的结果:

  • 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
  • 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息。
  • 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用(ambiguous call)

6.4.1 重载与作用域

一般来说,将函数声明置于局部作用域内不是一个明智的选择。但是为了说明作用域和重载的相互关系,我们将暂时违反这一原则而使用局部函数声明。

对于刚接触C++的程序员来说,不太容易理清作用域和重载的关系。其实,重载对作用域的一般性质并没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名:

string read();
void print(const string &);
void print(double); // 重载print函数
void fooBar(int ival)
{
    bool read = false;	//新作用域:隐藏了外层的 read
    string s = read();	//错误:read是一个布尔值,而非函数
    // 不好的习惯:通常来说,在局部作用域中声明函数不是一个好的选择
    void print(int);	//新作用域:隐藏了之前的 print
    print("Value:");	//错误:print(const string &)被隐藏掉了
    print(ival);		//正确:当前print(int)可见
    print(3.14);		//正确:调用print(int);print(double)被隐藏掉了
}

大多数读者都能理解调用read函数会引发错误。因为当编译器处理调用read的请求时,找到的是定义在局部作用域中的read。这个名字是个布尔变量,而我们显然无法调用一个布尔值,因此该语句非法。
调用print函数的过程非常相似。在fooBar内声明的print(int)隐藏了之前两个print函数,因此只有一个print函数是可用的:该函数以int 值作为参数。
当我们调用 print 函数时,编译器首先寻找对该函数名的声明,找到的是接受 int值的那个局部声明。一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。剩下的工作就是检查函数调用是否有效了。

在C++语言中,名字查找发生在类型检查之前。

第一个调用传入一个字符串字面值,但是当前作用域内 print 函数唯一的声明要求参数是 int 类型。字符串字面值无法转换成 int 类型,所以这个调用是错误的。在外层作用域中的print(const string&)函数虽然与本次调用匹配但是它已经被隐藏掉了,根本不会被考虑。
当我们为 print 函数传入一个 double 类型的值时,重复上述过程。编译器在当前作用域内发现了 print(int)函数,double类型的实参转换成 int 类型,因此调用是合法的。
假设我们把print(int)和其他 print 函数声明放在同一个作用域中,则它将成为另一种重载形式。此时,因为编译器能看到所有三个函数,上述调用的处理结果将完全不同:

void print(const string &);
void print(double);		//print函数的重载形式
void print(int);		//print函数的另一种重载形式
void fooBar2(int ival)
{
    print("Value:");	//调用print(const string &)
    print(ival);		//调用print(int)
    print(3.14);		//调用print(double)
}

6.5 特殊用途语言特性

本节我们介绍三种函数相关的语言特性,这些特性对大多数程序都有用,它们分别是:默认实参、内联函数和 constexpr 函数,以及在程序调试过程中常用的一些功能。

6.5.1 默认实参

某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参(default argument)。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
例如,我们使用 string 对象表示窗口的内容。一般情况下,我们希望该窗口的高宽和背景字符都使用默认值。但是同时我们也应该允许用户为这几个参数自由指定与默认值不同的数值。为了使得窗口函数既能接纳默认值,也能接受用户指定的值,我们把它定义成如下的形式:

typedef string::size_type sz;//关于typedef参见2.5.1节
string screen(sz ht = 24,sz wid = 80,char backgrnd ='');

其中我们为每一个形参都提供了默认实参,默认实参作为形参的初始值出现在形参列表中。我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

使用默认实参调用函数

如果我们想使用默认实参,只要在调节函数的时候省略该实参就可以了。例如,screen函数为它的所有形参都提供了默认实参,所以我们可以使用0、1、2或3个实参调用该函数:

string window;
window = screen();					//等价于screen(24,80,'')
window = screen(66);				//等价于screen(66,80,'')
window = screen(66,256);		    //screen(66,256,'')
window = screen(66,256,'#');	   //screen(66,256,'#')

函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。例如,要想覆盖 backgrnd 的默认值,必须为 ht和wid提供实参:

window = screen(,,'?');		//错误:只能省略尾部的实参
window = screen('?');		//调用window('?',80,'')

需要注意,第二个调用传递一个字符值,是合法的调用。然而尽管如此,它的实际效果却与书写的意图不符。该调用之所以合法是因为'?’是个 char,而函数最左侧形参的类型string::size_type 是一种无符号整数类型,所以 char 类型可以转换成(参见4.11节)函数最左侧形参的类型。当该调用发生时,char 类型的实参隐式地转换成string::size_type,然后作为 height 的值传递给函数。在我们的机器上,'?'对应的十六进制数是0x3F,也就是十进制数的63,所以该调用把值63 传给了形参height。

当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。

默认实参声明

对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。假如给定

//表示宽度和高度的形参没有默认值
string screen(sz,sz,char = '');

我们不能修改一个已经存在的默认值:

string screen(sz,sz,char = '*');		//错误:重复声明

但是可以按照如下形式添加默认实参:

string screen(sz = 24, sz = 80, char);	//正确:添加默认实参

通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。

默认实参初始值

局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参:

// 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,'*')
}

我们在函数f2内部改变了 def 的值,所以对 screen 的调用将会传递这个更新过的值。另一方面,虽然我们的函数还声明了一个局部变量用于隐藏外层的 wd,但是该局部变量与传递给screen的默认实参没有任何关系。

6.5.2 内联函数和constexpr函数

在6.3.2节中我们编写了一个小函数,它的功能是比较两个 string 形参的长度并返回长度较小的 string 的引用。把这种规模较小的操作定义成函数有很多好处,主要包括:

  • 阅读和理解shorterString 函数的调用要比读懂等价的条件表达式容易得多。
  • 使用函数可以确保行为的统一,每次相关操作都能保证按照同样的方式进行。
  • 如果我们需要修改计算过程,显然修改函数要比先找到等价表达式所有出现的地方再逐一修改更容易。
  • 函数可以被其他应用重复利用,省去了程序员重新编写的代价。

然而,使用 shorterString 函数也存在一个潜在的缺点:调用函数一般比求等价表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参:程序转向一个新的位置继续执行。

内联函数可避免函数调用的开销

将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。假设我们把shorterString 函数定义成内联函数,则如下调用

cout << shorterString(s1,s2) << endl;

将在编译过程中展开成类似于下面的形式:

cout << (s1.size() < s2.size() ? s1 : s2) << endl;

从而消除了shorterString函数的运行时开销。

在shorterString函数的返回类型前面加上关键字inline,这样就可以将它声明成内联函数了:

//内联函数:寻找两个string对象中较短的那个
inline const string&
    shorterString(const string &s1, const string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。

一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联地展开。

constexpr函数

constexpr函数(constexpr function)是指能用于常量表达式(参见2.4.4节)的函数。定义 constexpr 函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型(参见 2.4.4 节),而且函数体中必须有且只有一条return语句:

constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();	//正确:foo是一个常量表达式

我们把new_sz定义成无参数的 constexpr 函数。因为编译器能在程序编译时验证new_sz函数返回的是常量表达式,所以可以用new_sz函数初始化constexpr类型的变量 foo。
执行该初始化任务时,编译器把对 constexpr 函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
constexpr 函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr 函数中可以有空语句、类型别名(参见2.5.1节)以及using声明。

我们允许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)不是常量表达式

如上例所示,当我们给 scale 函数传入一个形如字面值 2 的常量表达式时,它的返回类型也是常量表达式。此时,编译器用相应的结果值替换对 scale 函数的调用。

如果我们用一个非常量表达式调用 scale 函数,比如 int 类型的对象i,则返回值是一个非常量表达式。当把 scale 函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要求。如果结果恰好不是常量表达式,编译器将发出错误信息。

constexpr函数不一定返回常量表达式。

把内联函数和constexpr函数放在头文件内

和其他函数不一样,内联函数和 constexpr 函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者 constexpr 函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。

6.5.3 调试帮助

C++程序员有时会用到一种类似于头文件保护(参见2.6.3节)的技术,以便有选择地执行调试代码。基本思想是,程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和NDEBUG。

assert预处理宏

assert是一种预处理宏(preprocessor marco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert 宏使用一个表达式作为它的条件:

assert(expr);

首先对 expr 求值,如果表达式为假(即0),assert 输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。

assert 宏定义在cassert 头文件中。如我们所知,预处理名字由预处理器而非编译器管理(参见2.3.2节),因此我们可以直接使用预处理名字而无须提供 using声明。也就是说,我们应该使用assert 而不是std::assert,也不需要为assert 提供using声明。
和预处理变量一样,宏名字在程序内必须唯一。含有 cassert 头文件的程序不能再定义名为 assert 的变量、函数或者其他实体。在实际编程过程中,即使我们没有包含cassert头文件,也最好不要为了其他目的使用 assert。很多头文件都包含了cassert,这就意味着即使你没有直接包含 cassert,它也很有可能通过其他途径包含在你的程序中。
assert 宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阙值。此时,程序可以包含一条如下所示的语句:

assert(word.size() > threshold);

NDEBUG预处理变量

assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。

我们可以使用一个#define 语句定义 NDEBUG,从而关闭调试状态。同时,很多编译器都提供了一个命令行选项使我们可以定义预处理变量:

$ CC -D NDEBUG main.c # use /D with the Microsoft compiler

这条命令的作用等价于在main.c文件的一开始写#define NDEBUG。

定义NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此,assert 应该仅用于验证那些确实不可能发生的事情。我们可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。
除了用于assert外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef 和#endif 之间的代码;如果定义了NDEBUG,这些代码将被忽略掉:

void print(const int ia[], size_t size)
{
    #ifndef NDEBUG
    	//_ _func_ _ 是编译器定义的一个局部静态变量,用于存放函数的名字
    	cerr << _ _func_ _ << ": array size is " << size << endl;
    #endif
    //...
}

在这段代码中,我们使用变量_ _func_ _输出当前调试的函数的名字。编译器为每个函数都定义了_ _func_ _ ,它是 const char 的一个静态数组,用于存放函数的名字。
除了 C++编译器定义的_ _func_ _之外,预处理器还定义了另外4个对于程序调试很有用的名字:

  • _ _FILE_ _ 存放文件名的字符串字面值
  • _ _LINE_ _ 存放当前行号的整型字面值
  • _ _TIME_ _ 存放文件编译时间的字符串字面值
  • _ _DATE_ _ 存放文件编译日期的字符串字面值

可以使用这些常量在错误消息中提供更多信息。

6.6 函数匹配

在大多数情况下,我们容易确定某次调用应该选用哪个重载函数。然而,当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来时,这项工作就不那么容易了。以下面这组函数及其调用为例:

void f();
void f(int);
void f(int,int);
void f(double,double=3.14);
f(5.6);		//调用 void f(double,double)

确定候选函数和可行函数

函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。在这个例子中,有4个名为f的候选函数。
第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数 (viable function)。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
我们能根据实参的数量从候选函数中排除掉两个。不使用形参的函数和使用两个int形参的函数显然都不适合本次调用,这是因为我们的调用只提供了一个实参,而它们分别有0个和两个形参。
使用一个 int 形参的函数和使用两个 double 形参的函数是可行的,它们都能用一个实参调用。其中最后那个函数本应该接受两个 double 值,但是因为它含有一个默认实参,所以只用一个实参也能调用它。

如果函数含有默认实参(参见6.5.1节),则我们在调用该函数时传入的实参数量可能少于它实际使用的实参数量。

在使用实参数量初步判别了候选函数后,接下来考察实参的类型是否与形参匹配。和一般的函数调用类似,实参与形参匹配的含义可能是它们具有相同的类型,也可能是实参类型和形参类型满足转换规则。在上面的例子中,剩下的两个函数都是可行的:

  • f(int)是可行的,因为实参类型double能转换成形参类型int。
  • f(double,double)是可行的,因为它的第二个形参提供了默认值,而第一个形参的类型正好是 double,与函数使用的实参类型完全一致。

如果没找到可行函数,编译器将报告无匹配函数的错误。

寻找最佳匹配(如果有的话)

函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。下一节将介绍“最匹配”的细节,它的基本思想是,实参类型与形参类型越接近,它们匹配得越好。

在我们的例子中,调用只提供了一个 (显式的)实参,它的类型是 double。如果调用f(int),实参将不得不从double转换成int。另一个可行函数f(double,double)则与实参精确匹配。精确匹配比需要类型转换的匹配更好,因此,编译器把 f(5.6)解析成对含有两个 double形参的函数的调用,并使用默认值填补我们未提供的第二个实参。

含有多个形参的函数匹配

当实参的数量有两个或更多时,函数匹配就比较复杂了。对于前面那些名为f的函数,我们来分析如下的调用会发生什么情况:

(42,2.56);

选择可行函数的方法和只有一个实参时一样,编译器选择那些形参数量满足要求且实参类型和形参类型能够匹配的函数。此例中,可行函数包括 f(int,int)和 f(double,double)。接下来,编译器依次检查每个实参以确定哪个函数是最佳匹配。如果有且只有一个函数满足下列条件,则匹配成功:

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

如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息。
在上面的调用中,只考虑第一个实参时我们发现函数 f(int,int)能精确匹配;要想匹配第二个函数,int 类型的实参必须转换成 double类型。显然需要内置类型转换的匹配劣于精确匹配,因此仅就第一个实参来说,f(int,int)比f(double,double)更好。

接着考虑第二个实参2.56,此时f(double,double)是精确匹配;要想调用f(int,int)必须将2.56从double类型转换成int类型。因此仅就第二个实参来说,f(double,double)更好。
编译器最终将因为这个调用具有二义性而拒绝其请求:因为每个可行函数各自在一个实参上实现了更好的匹配,从整体上无法判断孰优孰劣。看起来我们似乎可以通过强制类型转换(参见4.11.3节)其中的一个实参来实现函数的匹配,但是在设计良好的系统中,不应该对实参进行强制类型转换。

调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。

6.6.1 实参类型转换

为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
1.精确匹配,包括以下情况:

  • 实参类型和形参类型相同。
  • 实参从数组类型或函数类型转换成对应的指针类型(参见6.7 节,将介绍函数指针)。
  • 向实参添加顶层const或者从实参中删除顶层const。

2.通过const转换实现的匹配(参见4.11.2节)。
3.通过类型提升实现的匹配(参见4.11.1 节)。
4.通过算术类型转换(参见4.11.1节)或指针转换(参见4.11.2节)实现的匹配。
5.通过类类型转换实现的匹配(参见 14.9 节,将详细介绍这种转换)。

需要类型提升和算术类型转换的匹配

内置类型的提升和转换可能在函数匹配时产生意想不到的结果,但幸运的是在设计良好的系统中函数很少会含有与下面例子类似的形参。

分析函数调用前,我们应该知道小整型一般都会提升到 int 类型或更大的整数类型。

假设有两个函数,一个接受int、另一个接受 short,则只有当调用提供的是 short 类型的值时才会选择 short 版本的函数。有时候,即使实参是一个很小的整数值,也会直接将它提升成int 类型;此时使用 short 版本反而会导致类型转换:

void ff(int);
void ff(short);
ff('a');	//char提升成int:调用 f(int)

所有算术类型转换的级别都一样。例如,从int向unsigned int 的转换并不比从int 向 double的转换级别高。举个具体点的例子,考虑

void manip(long);
void manip(float);
manip(3.14);// 错误:二义性调用

字面值3.14 的类型是double,它既能转换成long 也能转换成float。因为存在两种可能的算数类型转换,所以该调用具有二义性。

函数匹配和const实参

如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向 const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数:

Record lookup(Account&);		//函数的参数是Account 的引用
Record lookup(const Account&);	//函数的参数是一个常量引用
const Account a;
Account b;
lookup(a);		// 调用 lookup(const Account&)
lookup(b);		// 调用 lookup(Account&)

在第一个调用中,我们传入的是const对象a。因为不能把普通引用绑定到const对象上,所以此例中唯一可行的函数是以常量引用作为形参的那个函数,并且调用该函数与实参a精确匹配。
在第二个调用中,我们传入的是非常量对象b。对于这个调用来说,两个函数都是可行的,因为我们既可以使用b初始化常量引用也可以用它初始化非常量引用。然而,用非常量对象初始化常量引用需要类型转换,接受非常量形参的版本则与b精确匹配。因此应该选用非常量版本的函数。

指针类型的形参也类似。如果两个函数的唯一区别是它的指针形参指向常量或非常量,则编译器能通过实参是否是常量决定选用哪个函数:如果实参是指向常量的指针,调用形参是 const*的函数;如果实参是指向非常量的指针,调用形参是普通指针的函数。

6.7 函数指针

函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:

//比较两个string对象的长度
bool lengthCompare(const string &,const string &);

该函数的类型是 bool(const string&,const string&)。要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可:

// pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf)(const string &,const string &); // 未初始化

从我们声明的名字开始观察,pf前面有个*,因此 pf 是指针;右侧是形参列表,表示pf指向的是函数:再观察左侧,发现函数的返回类型是布尔值。因此,pf 就是一个指向函数的指针,其中该函数的参数是两个 const string 的引用,返回值是 bool类型。

*pf 两端的括号必不可少。如果不写这对括号,则 pf 是一个返回值为 bool指针的函数:

//声明一个名为pf的函数,该函数返回bool*
bool *pf(const string &,const strings);

使用函数指针

当我们把函数名作为一个值使用时,该函数自动地转换成指针。例如,按照如下形式我们可以将lengthCompare的地址赋给 pf:

pf = lengthCompare;		//pf指向名为lengthCompare的函数
pf = &lengthCompare;	//等价的赋值语句:取地址符是可选的

此外,我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针:

bool bl = pf("hello","goodbye");			//调用lengthCompare函数
bool b2 = (*pf)("hello","goodbye");			//一个等价的调用
bool b3 = lengthCompare("hello","goodbye"); //另一个等价的调用

在指向不同函数类型的指针间不存在转换规则。但是和往常一样,我们可以为函数指针赋一个nullptr(参见2.3.2节)或者值为0的整型常量表达式,表示该指针没有指向任何一个函数:

string::size_type sumLength(const string&,const string&);
bool cstringCompare(const char*, const char*);
pf = 0;				//正确:pf不指向任何函数
pf = sumLength;		//错误:返回类型不匹配
pf = cstringCompare;//错误:形参类型不匹配
pf = lengthCompare;	//正确:函数和指针的类型精确匹配

重载函数的指针

当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。如果定义了指向重载函数的指针

void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; //pf1指向ff(unsigned)

编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配

void (*pf2)(int) = ff;		//错误:没有任何一个 ff与该形参列表匹配
double (*pf3)(int*) = ff;	//错误:ff和pf3的返回类型不匹配

函数指针形参

和数组类似(参见6.2.4节),虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用:

// 第三个形参是函数类型,它会自动地转换成指向函数的指针
void useBigger(const string &s1,const string &s2,
               bool pf(const string &,const string &));
// 等价的声明:显式地将形参定义成指向函数的指针
void useBigger(const string &s1,const string &s2,
               bool (*pf)(const string &,const string &));

我们可以直接把函数作为实参使用,此时它会自动转换成指针:

//自动将函数 lengthCompare转换成指向该函数的指针
useBigger(s1,s2,lengthCompare);

正如useBigger的声明语句所示,直接使用函数指针类型显得冗长而烦琐。类型别名(参见2.5.1节)和 decltype (参见2.5.3节)能让我们简化使用了函数指针的代码:

// Func和Func2是函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2;//等价的类型
// FuncP和EuncP2是指向函数的指针
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2;// 等价的类型

我们使用typedef定义自己的类型。Func和Func2是函数类型,而 FuncP和FuncP2是指针类型。需要注意的是,decltype 返回函数类型,此时不会将函数类型自动转换成指针类型。因为 decltype 的结果是函数类型,所以只有在结果前面加上*才能得到指针。可以使用如下的形式重新声明useBigger:

//useBigger的等价声明,其中使用了类型别名
void useBigger(const string&,const string&,Func);
void useBigger(const string&,const string&,FuncP2);

这两个声明语句声明的是同一个函数,在第一条语句中,编译器自动地将 Func 表示的函数类型转换成指针。

返回指向函数的指针

和数组类似(参见6.3.3节),虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。与往常一样,要想声明一个返回函数指针的函数,最简单的办法是使用类型别名:

using F = int(int*,int);		//F是函数类型,不是指针
using PF = int(*)(int*,int);	//PF是指针类型

其中我们使用类型别名(参见 2.5.1 节)将F定义成函数类型,将PF定义成指向函数类型的指针。必须时刻注意的是,和函数类型的形参不一样,返回类型不会自动地转换成指针。我们必须显式地将返回类型指定为指针:

PF f1(int);	//正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int);	//错误:F是函数类型,f1不能返回一个函数
F *f1(int);	//正确:显式地指定返回类型是指向函数的指针

当然,我们也能用下面的形式直接声明f1:

int (*f1(int))(int*,int);

按照由内向外的顺序阅读这条声明语句: 我们看到f1有形参列表,所以f1是个函数;f1前面有*,所以f1返回一个指针;进一步观察发现,指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是int。
出于完整性的考虑,有必要提醒读者我们还可以使用尾置返回类型的方式(参见6.3.3节)声明一个返回函数指针的函数:

auto f1(int) -> int (*)(int*,int);

将auto和decltype用于函数指针类型

如果我们明确知道返回的函数是哪一个,就能使用decltype简化书写函数指针返回类型的过程。例如假定有两个函数,它们的返回类型都是 string::size_type,并且各有两个const strin&类型的形参,此时我们可以编写第三个函数,它接受一个string类型的参数,返回一个指针,该指针指向前两个函数中的一个:

string::size_type sumLength(const string&,const string&);
string::size_type largerLength(const string&,const string&);
// 根据其形参的取值,getFcn 函数返回指向sumLength 或者largerLength 的指针
decltype(sumLength) *getFcn(const string &);

声明 getFcn 唯一需要注意的地方是,牢记当我们将 decltype 作用于某个函数时,它返回函数类型而非指针类型。因此,我们显式地加上*以表明我们需要返回指针,而非函数本身。

小结

函数是命名了的计算单元,它对程序(哪怕是不大的程序)的结构化至关重要。每个函数都包含返回类型、名字、(可能为空的) 形参列表以及函数体。函数体是一个块,当函数被调用的时候执行该块的内容。此时,传递给函数的实参类型必须与对应的形参类型相容。
在C++语言中,函数可以被重载:同一个名字可用于定义多个函数,只要这些函数的形参数量或形参类型不同就行。根据调用时所使用的实参,编译器可以自动地选定被调用的函数。从一组重载函数中选取最佳函数的过程称为函数匹配。

posted @ 2023-01-19 11:53  泠枫Jun  阅读(59)  评论(0编辑  收藏  举报