6.1 函数基础
一个典型的函数定义包括如下部分:返回类型、函数名、参数列表以及函数体。
通过调用运算符(call operator)来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针。
函数的调用完成两项工作:1.用实参初始化函数对应的形参 2.将控制权转移给被调用函数,此时主调函数的执行被暂时中断,被调函数开始执行。
当遇到一条return语句时函数结束执行的过程。和函数调用一样,return语句也完成两项工作:1.返回return语句中的值(如果有的话)2.将控制权从被调函数转移回主调函数。
函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针。
在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才销毁。
自动对象:对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。把只存在于块执行期间的对象称为自动对象(automatic object)。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
局部静态对象:某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间,此时可以将局部变量定义成static类型。
类似于变量,函数只能定义一次,但可以声明多次。函数声明无须函数体,用一个分号替代即可。因为函数的声明不包含函数体,所以也就无须形参的名字。事实上,在函数的声明中经常省略形参的名字。尽管如此,写上形参的名字还是有用处的,可以帮助使用者更好理解函数的功能。函数声明也被称作函数原型(function prototype)。
之前曾建议过变量在头文件中声明,在源文件中定义。函数也应如此。
分离式编译:允许我们把程序分割到几个文件中去,每个文件独立编译。
6.2 参数传递
如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
当形参是引用类型时,我们说它对应的实参被引用传递。如果是拷贝,则说这样的实参被值传递。
使用引用避免拷贝:拷贝大的类类型对象或者容器对象比较低效,甚至有的类型根本就不支持拷贝操作。如比较两个string对象长度的函数,形参就可以为const string &s1...这既避免了引用操作,又能保证string对象不被修改。
使用引用参数可以返回额外的信息,如一个函数想返回多个参数,那么可以选择return一种新的数据类型,让它包含多个信息成员。另一种更简单的办法就是给函数传入一个额外的引用实参,让其来保存信息。
数组的两个特殊性质:不允许拷贝数组,使用数组时(通常)会将其转换成指针。因此,我们无法以值传递方式使用数组参数,所以当为函数传递一个数组时,实际上传递的时指向数组首元素的指针。
void print(const int*);
void print(const int[]);
void print(const int[10]);//这里的维度表示我们期望数组含有多少元素,实际不一定
尽管形式不同,这三个函数是等价的。每个函数的唯一形参都是const int*类型的。
int (*matrix)[10]; //指向含有10个整数的数组的指针
void print(int matrix[][10], int rowSize) { /*...*/ } //等价定义
下面print函数中matrix的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针。因为编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表内。
为了编写能处理不同数量实参的函数。c++11提供了两种主要方法:1.如果所有实参类型相同,可以传递名为initializer_list的标准库类型;如果实参的类型不同,则编写一种特殊的函数,也就是所谓的可变参数模板。
6.3 返回类型和return语句
void类型函数隐式地执行了return。void类型函数也能使用return expression;,不过此时的expression必须是另一个返回void的函数,否则会产生编译错误。
调用一个返回引用的函数得到左值,其他返回类型得到右值。
c++11新标准规定,函数可以返回花括号包围的值的列表。如vector<string> function(),返回值可以是{},{"xxx","yyy"}。若函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。若返回的是类类型,由类本身定义初始值如何使用。
如果想定义一个返回数组指针的函数,则数组维度必须跟在函数名字之后。函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。如:int (*func(int i))[10]; 要从里到外逐层理解其含义。
在C++11新标准中,还有一种简化上述func声明的方法,就是用尾置返回类型。如:auto func(int i) -> int(*)[10];
P206的三个练习
6.4 函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,称之为重载函数。
重载主要是由编译器根据传递的实参类型推断想要的是哪个函数。
main函数不能重载。
一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的。
建议:最好只重载那些确实非常相似的操作。有些情况下,给函数起不同的名字能使得程序更易理解。
函数匹配是指一个过程,在这个过程中把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定。
如果在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名。一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
在C++中,名字查找发生在类型检查之前。
6.5 特殊用途语言特性
6.5.1 默认实参
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
函数声明通常放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。但要注意在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
6.5.2 内联函数和constexpr函数
在大多数机器上,一次函数调用包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。
将函数指定为内敛函数,通常就是将它在每个调用点上“内联地”展开。
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。
constexpr函数是指能用于常量表达式的函数。有如下约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体必须有且只有一条return语句。
6.5.3 调试帮助
assert是一种预处理宏。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。 预处理名字由预处理器而非编译器管理,所以可以直接使用该名字而无须提供using声明。
assert(expr); 首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。常用于检查“不能发生”的条件。
assert宏定义在cassert头文件中。宏名字在程序内必须唯一。
NDEBUG预处理变量,如果定义了NDEBUG,则assert什么也不做。可以使用一个#define语句定义NDEBUG,从而关闭调试状态。定义NDEBUG能避免检查各种条件所需的运行时开销。
可把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。
C++编译器定义了几个对于程序调试很有用的名字:__func__、__FILE__、__LINE__、__TIME__、__DATE__。
6.6 函数匹配
先选出候选函数(所有同名重载函数,且其声明在调用点可见)->再选出可行函数(实参类型是否与形参匹配)->寻找最佳匹配(如果有的话)。
含有多个形参的函数匹配,如果在寻找最佳匹配时出现二义性,编译器就会拒绝这个调用请求。
6.7 函数指针
函数指针指向的时函数而非对象。
当我们把函数名作为一个值使用时,该函数自动地转换成指针(类似数组)。如func是函数名,pf = func;与pf = &func;是等价的。
此外,还能直接使用指向函数的指针调用该函数,无须提前解引用指针。
虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。
。。。