6.1函数基础
函数定义包括:返回类型、函数名字、形参列表和函数体。
调用运算符是(),它作用域表达式,该表达式是函数或者指向函数的指针;圆括号内是实参列表,用实参初始化函数的形参。
调用表达式的类型就是返回值类型。
函数的返回类型不能是数组类型或函数类型。但是,可以是指向数组或函数的指针。
6.1.1局部对象
在C++中,名字有作用域,对象有声明周期。
函数体是一个语句块。块构成一个新的作用域,可以在其中定义变量。形参和函数体内部定义的变量称为局部变量。局部变量对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏(hide)在外层作用域中同名的其他所有声明中。
在所有函数体外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,在程序结束时销毁。
自动对象
只存在于块执行期间的对象称为自动对象。块的执行结束后,块中创建的自动对象的值就变成未定义的了。
形参是一种自动对象。函数开始时为形参申请存储空间,一旦函数终止,形参也就被销毁。
局部静态对象
局部变量的生命周期贯穿函数调用以及之后的时间。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且知道程序终止才销毁,在此期间即使对象所在的函数结束执行,也不会对它有影响。
在控制流第一次经过ctr的定义前,ctr被创建并初始化为0.每次调用ctr加1并返回新值。每次执行count_calls函数时,ctr的值都已经存在且等于上次函数退出时ctr的值。
6.1.2函数声明
函数只定义一次,可以声明很多次。
声明不需要函数体,用分号替代。声明不需要形参的名字,只需要类型,写了名字容易理解。
变量、函数在头文件声明。在源文件中定义。
定义函数的源文件,应该把含有函数声明的头文件包含起来,编译器负责验证函数的定义和声明是否匹配。
6.1.3分离式编译
分离式编译,允许我们把程序分割到多个文件,每个文件单独编译。
编译和链接多个源文件
举例,假设fact函数的定义位于一个名为fact.cc的文件中,它的声明位于名为Chapter6.h的头文件中。显然与其他所有用到fact函数的文件一样。fact.cc应该包含Chapter6.h头文件。另外,我们在factMain.cc的文件中创建main函数,main函数将调用fact函数。要生成可执行文件,必须告诉编译器我们用到的代码在哪里。
一起编译
$CC factMain.cc fact.cc -o main #生成main可执行文件
分离式编译:
$CC -c factMain.cc #生成factMain.o
$CC -c fact.cc #生成fact.o #生成fact.o
$CC factMain.o fact.o -o main #把对象文件链接在一起,形成main可执行文件
6.2参数传递
每次调用函数时,都会重新创建它的形参,并用传入的实参对形参初始化。
如果形参是引用类型,它将绑定到对应的实参(形参是实参的别名);如果形参是值类型,则将实参的值拷贝后赋值给形参(拷贝后,形参和实参是相互独立的)。
6.2.1传值参数
传值参数,函数对形参的操作,不会影响实参。
指针形参
当执行拷贝指针操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的,但是通过它们可以修改同一个对象。
6.2.2传引用参数
对于引用的操作,实际上是作用在引用所引的对象上。
使用引用,避免了拷贝。
拷贝大的类类型对象或者容器对象比较低效,甚至有的类根本不支持拷贝操作。
使用引用,返回额外信息。
一个函数只能返回一个值,然而有时函数需要同时返回多个值。
通过传引用的方式,就可避免返回多个不同类型的值。
6.2.3const形参和实参
当形参是const时。顶层const作用域对象本身。
当用实参初始化形参时,会忽略掉顶层const。形参的顶层const被忽略。当形参有顶层const时,传给它常量对象或非常量对象都可以:
能够读取i,但是不能向i写值。
调用func函数时,既可以传const int,也可以传int。
c++允许定义相同名字的函数,不过前提是不同函数的形参列表有明显的区别。因为顶层const被忽略掉了,所以上面2个func的参数是完全一样的。
指针或引用形参与const
我们可以使用非const实参初始化底层const对象/形参,但是反过来不行。(不能用底层const实参初始化非const对象/形参,因为非const形参难免会修改const对象)
尽量使用常量引用
把函数不会改变的形参定义成普通引用(非const引用),是不对的。因为,这样我们就不能把const对象、字面值或需要类型转换的对象传递给普通的引用形参。
6.2.4数组形参
不允许拷贝数组;使用数组时会将其转换成指针。
因为不能拷贝数组,所以无法以值传递的方式使用数组参数。因为数组会转换成指针,所以当我们为函数传递一个数组时,实际上传递的是数组首元素的指针。
print、print1、print2三者等价。形参都是const int*。如果我们传入print函数是一个数组,则实参自动转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。
以数组作为形参的函数也必须确保:使用数组不越界。
为此,需要额外信息,才能确定数组的尺寸,才能保证不越界。
使用标记,表示数组结束。
字符数组以’\0’表示结束。int[] 不知道。
使用标准库规范
传递指向数组首元素和尾后元素的指针。
用到begin(),end()
显示传递数组大小的形参
数组形参和const
只有当函数确实要修改元素值的时候,才把形参定义为非const的指针。
数组引用形参
C++允许将变量定义成数组的引用,形参也可以是数组的引用。
但是,这样就限制了数组的长度为7。
使用模板类型参数:
编译器会使用字面量常量的大小代替N。(16.1.1 P580)
6.2.5main:处理命令行选项
int main(int argc, char* argv[]) { . . . }
第二个形参argv是数组,它的元素是指向 C风格字符串的指针;
第一个形参argc表示数组中字符串的数量。
当实参传递给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。
若命令行为:
prog -d -o ofile data0
则,argc=5,argv应该包含
argv[0]=”prog”
argv[1]=”-d”
argv[2]=”-o”
argv[3]=”ofile”
argv[4]=”data0”
argv[5]=0;
6.2.6含有可变形参的函数
有时,我们无法预知应该向函数传递几个实参。
C++11新标准提供了2种方法:如果所有实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参类型不同,我们可以编写一种特殊的函数,也就是可变参数模板。
initializer_list形参
实参数量未知,但是类型都相同。
6.3返回类型和return语句
return 语句终止当前正在执行的函数,并将控制权返回到调用该函数的地方。
值是如何被返回的
返回一个值的方式 和 初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域。
返回局部对象的引用、局部对象的指针,都是错误的。
引用返回左值
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。
可以像其他左值那样来使用返回引用的函数的调用,我们能为返回类型是非const引用的函数的结果赋值:
返回值是引用,因此调用是个左值,能出现在赋值运算符=的左侧。引用绑定的是有名字的、已经初始化的变量(非const),所以是左值,不是临时变量。
6.3.3 返回数组指针
因为数组不能被拷贝,因此函数不馁返回数组。不过,可以返回数组的指针或引用。
用类型别名可以省事。
使用尾置返回类型
?*?
decltype
decltype不负责把数组类型转换成对应的指针,需要手动加* 。
6.4函数重载
同一作用域内,函数名字相同,但是形参列表不同,称之为重载函数。main不能重载
重载和const形参
一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
重复定义。
重复定义
如果形参是某种类型的指针或引用,则通过区分其指向的是const对象还是非const对象可以实现函数重载,此时的const是底层的。
当我们传递一个非const对线或指向非const对象的指针,编译器会优先选用非const版本的函数。
const_cast和重载
old版本的输入、输出都是const引用,实际上并不方便使用,因为我们操作的string通常是非const,且我们希望输出结果也是非const,最好是引用,省去拷贝。
因此新版本采用string&,并使用const_cast强制转换。把非const引用,转换成const引用,调用old版本。新版本输出结果再由const引用转换成非const引用。
6.4.1重载与作用域
不同的作用域中,无法重载同名函数。
6.5特殊用途语言特性
默认实参
函数的声明习惯是放在头文件,一般只声明一次。在给定作用域中,一个形参的默认实参只能赋予一次。该函数后续声明,只能为前面没有默认值的形参添加默认实参,而且该形参右侧所有形参必须有默认值。 后续声明不能改前面声明过的默认实参。
6.5.2内联函数和constexpr函数
内联函数可以避免函数调用的开销
inline内联,在每个函数调用点展开。内联机制用于优化规模较小、流程直接(没有循环)、频繁调用(省得频繁地找该函数)。就是在许多调用处插入该函数,插入很多备份。
constexpr函数
能用于常量表达式的函数。返回值及所有形参类型都得是字面值类型,且有return。
编译器能在程序编译时 验证返回值是不是常量表达式constexpr。执行初始化任务时,编译器把constexpr函数的调用替换成结果值。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
我们允许constexpr函数返回值并非一个常量。
constexpr函数不一定返回常量表达式。
6.5.3调试帮助
assert(expr);
首先对expr求值,若真则程序继续执行;若假,则程序终止。
__FILE__ 存放文件名的字符串字面值
__LINE__ 存放当前行号的整形字面值
__TIME__ 存放文件编译时间的字符串字面值
__DATE__ 存放文件编译日期的字符串字面值
6.6函数匹配
当几个重载函数的形参数量相等,且某些形参类型可以由其他类型转换得来时,很难确定究竟是调用了那个重载函数。
确定候选函数和可行函数
第一步:选定本次调用对应的重载函数集,集合中的函数称为候选函数。(同名;在调用处可见。)
第二步:考察调用提供的实参,从候选函数中选出可行函数。特征(1形参数量与实参数量相等;2每个实参类型与形参类型相同,或能转换成形参的类型。)
第三步:从可行函数中选出与本次调用最匹配的函数。
6.6.1实参类型转换
编译器将实参类型到形参类型的转换划分了5级:
1精确匹配。包括:类型相同;实参从数组类型或函数类型,转换成对应的指针类型;向实参添加顶层const或删除实参的顶层const。
2通过const转换实现的匹配。允许将非const指针/引用,转换成const指针/引用。
3通过类型提升实现的匹配。小数类型转换成大数。
4通过算术类型转换。运算对象转换成最宽的类型。
5通过类类型转换实现匹配。
6.7函数指针
函数指针指向的是函数,而非对象。函数指针指向某种特点类型,函数的类型由它的返回类型和形参共同决定,与函数名无关。
当我们把函数名作为一个值使用时,该函数自动转换成指针。
可以直接使用指向函数的指针调用该函数,不需要提前解引用指针。
指向不同函数类型的指针不存在转换规则。
重载函数的指针
编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
函数指针的形参
不能定义函数类型的形参,但是形参可以是指向函数的指针
返回指向函数的指针
编译器不会自动地将函数返回类型当成对应指针类型处理。
如果我们明确知道返回的函数是哪一个,可以使用decltype简化书写函数指针返回类型的过程。