函数详解
函数
基础
一个典型的函数(function)定义包括下面部分:
- 返回类型
- 函数名
- 0个或多个形参组成的列表,其中形参以逗号隔开
- 函数体
我们通过调用运算符来执行函数,调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针,圆括号之内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。调用表达式的类型是就是函数的返回类型。
函数调用完成两项工作:
- 用实参初始化函数对应的形参
- 将控制权转移给被调用的函数,此时主调函数(calling function)的执行被终端,被调函数(called function)开始执行。
形参和实参
- 实参和形参是一一对应的关系,数量和类型需要对应相同,不过在编译器用实参初始化形参的时候顺序不一定按照列表的顺序。
- 形参的列表可以为空,但是不能省略,可以用
void
关键字来表示函数没有形参。 - 形参列表中的形参用逗号隔开,每一个形参都需要一个对应的类型声明符,即使相邻的形参类型相同也需要把各自的类型写出来。
- 任意两个形参都不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。
- 形参名是可选的,但是由于无法使用未命名的形参,所以形参一般都会有名字,不过有的函数个别形参不会被用到,则此类形参不命名以表示在函数体内不会使用它。是否设置未命名的形参不会影响调用时提供的实参数量,即使某个形参不被函数使用,也必须为它提供一个实参。
形参和函数体内部定义的变量统称为局部变量。局部变量的生命周期依赖于定义的方式。
- 自动对象:只存在于块执行期间的对象,当块执行结束后,块中创建的自动对象的值就变成未定义的了。形参是一种自动对象,函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以函数一旦终止,形参也就销毁。未初始化的自动对象的值是未定义的。
- 局部静态对象:在第一次调用函数经过对象定义语句时初始化,直到程序终止才被销毁,在此期间即使所在函数执行结束也不会有影响,只有第一次初始化有作用,后面再调用函数的初始化无作用。如果没有显式初始化,默认初始化为0(static的特性)。
函数声明:
- 函数的名字必须在使用之前声明。
- 函数只能定义一次,可以声明多次。
- 如果一个函数永远也不会被用到,可以只声明不定义。
- 函数的声明不包含函数体,可以选择不包含形参名。
- 函数声明也称为函数原型。
- 建议函数声明尽量在头文件中。
分离式编译:也就是按照逻辑关系将程序分隔到几个文件中去,每个文件独立编译。如果修改了其中一个源文件,那么只需要重新编译那个修改了的文件。
参数传递
传引用参数:当形参是引用类型时,和其他引用一样,引用形参也是它绑定对象的别名,也就是说引用形参是它对应的实参的别名。通过引用形参可以允许函数改变一个或多个实参的值。
传值参数:当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。此时,对变量的改动不会影响初始值。传值参数的机理完全一样,函数对形参做的所有操作都不会影响到实参。
指针形参:指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针可以间接的访问它所指的对象,所以通过指针可以修改它所指的对象的值。
使用引用形参返回额外的信息:一个函数只能返回一个值,但是有时候需要同时返回多个值,可以通过添加引用形参来返回额外的信息。
const形参和实参:
- 当用实参初始化形参时会忽略掉顶层const,即当形参有顶层const时,传给它常量对象或非常量对象都是可以的。
- 可以使用非常量初始化一个底层const对象,反过来不行。
例:
int a=1;
const int * b=&a; // 用非常量初始化一个底层const对象可以
int *c=b; // 反过来不行 const int * 不能转换成 int *
int d=*b; // 这样可以
- 尽量使用常量引用。
数组形参:
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响:
- 不能拷贝数组,所以无法以值传递的方式使用数组参数
- 因为数组会被转换成指针,所以为一个函数传递一个数组时,实际上传递的是指向数组首元素的指针
数组是以指针的形式传递给函数的。所以函数无法知道数组的尺寸,管理数组的指针形参通常由下面三种方法:
- 使用标记指定数组长度,要求数组本身包含一个结束标记,例如C语言风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。这种方法适用于那些有明显结束标记且标记不会和数据混淆的情况。
- 使用标准库规范,传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发。
- 显式传递一个表示数组大小的形参。
传递多维数组:传递多维数组的时候,实际上是传递指向数组首元素的指针,我们处理的是多维数组,首元素本身就是一个数组,指针就是指向一个数组的指针。
含有可变形参的函数:
为了编写能够处理不同数量实参的函数,C++11新标准提供了两种主要的方法:
- 如果所有的实参类型相同,可以传递一个名为
initializer_list
的标准库类型 - 如果实参的类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板
- 还有一种特殊的形参类型 --- 省略符。这种功能一般只用于和C函数交互的接口程序。
省略符:省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为
varargs
的C标准库功能。省略符只能出现在形参列表的最后一个位置。
返回类型和 return 语句
return 语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。有两种形式:
- return;
- return expression;
无返回值函数:没有返回值的 return 只能用在返回类型是void的函数中,返回void的函数不要求非要有return语句,因为在这类函数结尾会隐式的执行return。如果void函数想在中间提前退出可以使用return语句。返回void的函数也可以使用第二种形式,不过expression必须是另一个返回void的函数。
有返回值函数:返回的类型必须和函数类型相同,或者可以隐式的转换。
值是如何被返回的:返回一个值的方式和初始化一个变量或形参的方式完全一样,返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
不要返回局部对象的指针或者引用:一旦函数返回,局部对象被释放,引用和指针都会失效。
引用返回左值:调用一个返回引用的函数得到左值,其他类型的得到右值。可以给非常量引用的函数赋值。
列表初始化返回值:如果返回值是内置类型,花括号包围的列表最多包含一个值;如果是非内置类型,则根据类的定义。
主函数返回值:主函数没有return直接结束,会隐式的插入return。返回0表示执行成功,非0表示失败。
递归函数:调用自身的函数称为递归函数,递归函数一定有某个路径是不包含递归的(终止条件),否则函数将无限递归下去直到栈空间被耗尽。
返回数组指针:数组不能被拷贝,所以不能返回数组,只能返回数组指针。可以使用类型别名来简化。
函数重载
同一作用域内几个函数名字相同但是形参列表不同,称为重载
调用重载函数有三种结果:
- 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。
- 找不到任何一个函数与调用的实参匹配,编译器发出无匹配的错误信息。
- 有多于一个的函数可以匹配,但是每个都不是明显的最佳选择,发生二义性调用错误。
特殊用途语言特性
内联函数:将函数指定为内联函数,通常就是将它在每个调用点上内联的展开,相当于强化版的宏定义,内联函数省去了寻找函数入口地址的过程。一般来说,内联函数适用于规模较小(代码少)、流程直接(非递归)、频繁调用的函数。