[C++]C++Primer Chapter 6

函数

6.1 函数基础

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

执行函数的第一步是(隐式地)定义并初始化它的形参。因此,当调用fact函数时,首先创建一个名为val的int变量,然后将它初始化为调用时所用的实参5。

当遇到一条return语句时函数结束执行过程。和函数调用一样,return语句也完成两项工作:一是返回return语句中的值(如果有的话),二是将控制权从被调函数转移回主调函数。函数的返回值用于初始化调用表达式的结果,之后继续完成调用所在的表达式的剩余部分。

形参和实参

实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推。尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序(参见4.1.3节,第123页)。编译器能以任意可行的顺序对实参求值。

6.1.1 局部对象

在C++语言中,名字有作用域(参见2.2.4节,第43页),对象有生命周期(lifetime)。理解这两个概念非常重要。

名字的作用域是程序文本的一部分,名字在其中可见。

对象的生命周期是程序执行过程中该对象存在的一段时间。

在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁。局部变量的生命周期依赖于定义的方式。

自动对象

对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象(automatic object)。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。

局部静态对象

某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

6.1.2 函数声明

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

在头文件中进行函数声明

我们建议变量在头文件(参见2.6.3节,第68页)中声明,在源文件中定义。与之类似,函数也应该在头文件中声明而在源文件中定义。

定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。

6.1.3 分离式编译

分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

编译和链接多个源文件

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

6.2 参数传递

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

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

6.2.1 传值参数

在C++语言中,建议使用引用类型的形参替代指针。

6.2.2 传引用参数

使用引用避免拷贝

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。

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

6.2.3 const形参和实参

尽量使用常量引用

把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。就像刚刚看到的,我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。

当一个a函数的形参是const,a函数要将这个参数作为实参传递给另外一个b函数,最理想的方法是b函数的参数应该也是const,如果无法满足,正确的做法应该是在a函数内部定义一个局部对象,拷贝一份这个形参,然后将局部对象作为参数传递给b函数。

6.2.4 数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组(参见3.5.1节,第102页)以及使用数组时(通常)会将其转换成指针(参见3.5.3节,第105页)。

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

管理指针形参有三种常用的技术:

使用标记指定数组长度

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

使用标准库规范

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

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

第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。使用该方法,可以将print函数重写成如下形式

数组形参和const

当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针(参见2.4.2节,第56页)。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针

数组引用形参

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

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

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

传递多维数组

当将多维数组传递给函数时,真正传递的是指向数组首元素的指针(参见3.6节,第115页)。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略

6.2.5 main:处理命令行选项

第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。

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

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

6.2.6 含有可变形参的函数

为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板,关于它的细节将在16.4节(第618页)介绍。

C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。本节将简要介绍省略符形参,不过需要注意的是,这种功能一般只用于与C函数交互的接口程序。

initializer_list形参

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

initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。

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

省略符形参

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

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

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

6.3 返回类型和return语句

6.3.1 无返回值函数

6.3.2 有返回值函数

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

值是如何被返回的

返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

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

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

引用返回左值

列表初始化返回值

C++11新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化(参见3.3.1节,第88页);否则,返回的值由函数的返回类型决定。

主函数main的返回值

为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量(参见2.3.2节,第49页),我们可以使用这两个变量分别表示成功与失败

递归

如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。

6.3.3 返回数组指针

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

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

使用尾置返回类型

在C++11新标准中还有一种可以简化上述func声明的方法,就是使用尾置返回类型(trailing return type)。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。

尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto

使用decltype

如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。

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

6.4 函数重载

如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数。

定义重载函数

对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。

不允许两个函数除了返回类型外其他所有的要素都相同。

重载和const形参

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

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

当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。

const_cast和重载

const_cast在重载函数的情景中最有用

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

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

调用重载的函数

当调用重载函数时有三种可能的结果:

1.编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。

2.找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息。

3.有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用(ambiguous call)。

6.4.1 重载与作用域

如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名

6.5 特殊用途语言特性

6.5.1 默认实参

我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

使用默认实参调用函数

尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。

默认实参声明

在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。

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

默认实参初始值

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

6.5.2 内联函数和constexpr函数

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

将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。

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

constexpr函数

constexpr函数(constexpr function)是指能用于常量表达式(参见2.4.4节,第58页)的函数。

定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型(参见2.4.4节,第59页),而且函数体中必须有且只有一条return语句

constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr函数中可以有空语句、类型别名(参见2.5.1节,第60页)以及using声明。

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

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

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

6.5.3 调试帮助

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

assert预处理宏

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

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

NDEBUG预处理变量

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

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

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

assert应该仅用于验证那些确实不可能发生的事情。我们可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。

除了用于assert外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef和#endif之间的代码;如果定义了NDEBUG,这些代码将被忽略掉:

我们使用变量__func__输出当前调试的函数的名字。编译器为每个函数都定义了__func__,它是const char的一个静态数组,用于存放函数的名字。

除了C++编译器定义的__func__外,预处理器还定义了另外4个对于程序调试很有用的名字:

__FILE__存放文件名的字符串字面值。

__LINE__存放当前行号的整型字面值。

__TIME__存放文件编译时间的字符串字面值。

__DATE__存放文件编译日期的字符串字面值。

6.6 函数匹配

确定候选函数和可行函数

函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。

第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数(viable function)。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。

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

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

如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息。

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

6.6.1 实参类型转换

有时间再看。

6.7 函数指针

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

要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可

使用函数指针

当我们把函数名作为一个值使用时,该函数自动地转换成指针。

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

重载函数的指针

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

函数指针形参

虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用

直接使用函数指针类型显得冗长而烦琐。类型别名(参见2.5.1节,第60页)和decltype(参见2.5.3节,第62页)能让我们简化使用了函数指针的代码

返回指向函数的指针

虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。

要想声明一个返回函数指针的函数,最简单的办法是使用类型别名

和函数类型的形参不一样,返回类型不会自动地转换成指针。我们必须显式地将返回类型指定为指针

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


[C++]C++Primer Chapter 6
https://erlsrnby04.github.io/2024/09/22/C-C-Primer-Chapter-6/
作者
ErlsrnBy04
发布于
2024年9月22日
许可协议