第六章 函数
函数可以有0个或者多个参数,可以重载函数,也就是同一个名字可以对应不同的函数。
1.函数基础
函数包括返回类型,函数名,形参列表,函数体。
**函数三要素是返回类型,函数名,形参列表,描述了函数的接口,**说明了调用该函数所需的全部信息。
调用函数时用调用运算符(),()内时实参列表,使用实参初始化函数的形参。
实参是形参的初始值,二者类型需要匹配(可能会发生隐式类型转换),数量需要一致。形参可以为空。
函数返回类型不能是数组或函数类型,但可以是指向数组或者函数的指针。
1.局部对象
C++中,名字有作用域,对象有生命周期。
函数是一个语句块,块构成一个新的作用域。所以形参和函数体内部的变量统称为局部变量,局部变量的生命周期依赖于定义的方式。(与之不同的是,函数体之外定义的对象存在于程序的整个执行过程,此类对象在程序启动时创建,程序结束时才会被销毁)
自动对象:普通局部变量对应的对象在函数控制路径经过变量定义的语句时创建该对象,到达块所在末尾时销毁它。块执行结束后,自动对象的值自动变成未定义。形参是自动对象。
局部静态对象:将变量定义成static类型,局部变量的生命周期贯穿于定义到整个程序终止。
static int a=0;
2.函数声明(函数原型)
函数只能被定义一次,但可以声明多次。如果一个函数用于不会用到,可以只声明不定义。
函数声明无需函数体,用分号代替即可。可以不写形参,但写上更易于理解。
和变量一样,建议函数在头文件中声明而在源文件中定义。含有函数声明的头文件应该被包含到定义函数的源文件中。
3.分离式编译
编译和链接多个源文件。如果我们只修改了其中一个源文件,只需重新编译那个改动改动了的文件。
分离式编译会产生后缀.obj(windoes)或者.o(UNIX)的文件,后缀名的含义式该文件包含对象代码。
2.参数传递
形参初始化机理和变量初始化一样。相当于用实参给形参赋值。
1.值传递/传值参数
**实参的值被拷贝给形参,函数对形参所做的操作不会影响到实参。**比如有个函数是 int fact(int a){ };然后调用fact(5),5的值虽然在fact函数中被改变了,但是实参的值5没有改变。
通过指针可以修改它所指对象的值。
//函数接受一个指针
void reset(int *ip){
*ip=0; //改变指针ip指向的对象的值
ip=0; //只改变了ip的局部拷贝,实参未被改变
}
//调用reset函数。实参所指对象被置为0,但是实参本身的值没变
int i=42;
reset(&i); //改变i的值而非i的地址,输出i=0
。&应该是是取址吧
C中通常用指针类型的形参访问函数外部的对象,C++中建议用引用类型的形参代替指针。
2.引用传递/传引用参数
//函数接受一个引用
void reset(int &ip){
ip=0;
}
//调用reset函数。直接传入对象而不需要传入对象的地址
int i=42;
reset(i); //输出i=0。
这种形参是引用简单得多啊!
函数无需改变引用形参的值的时候,最好用常量引用。
可以利用引用形参返回多个值。
3.const形参与实参
形参是const的时候,实参初始化的时候会忽略顶层const。和没有const的一样。
**形参尽量使用常量引用:**1.防止函数修改实参的值;2.使用普通引用会限制函数所能接受的实参类型,例如我们不能把const对象,字面值或者需要类型转换的对象传递给普通的引用形参。
4.数组形参
数组不能拷贝—无法以值传递使用数组参数
数组通常会被转换为指针—为函数传递数组时,实际上传递的是指向数组首元素的指针
数组是以指针形式传递的,要提供数组大小信息:1.使用标记指定数组长度;2.形参列表传递指向数组首元素和尾后元素的指针;3.形参列表除了数组,再定义一个size_t类型的表示数组大小的形参。
**形参可以是数组的引用。**int (&arr)[10],注意没有括号就是引用的数组。
传递多维数组:void(int (*matrix) [10],int rowsize)
5.含有可变形参的函数
有时候我么无法预知应该向函数传递几个实参。
1.initializer_list形参
函数实参类型相同但数量未知时。
initializer_list是一种标准库类型,表示某种特定类型的值的数组,与vector类似,它也是一种模板类型,定义时需要说明类型,含有begin和end成员函数。但是这个对象中的值永远是常量值。
2.省略符形参
仅仅用于C和C++通用的类型。
3.返回类型和return语句
1.无返回值函数
即return。一般用于void类型的函数中,而且void类型函数最后一句会隐式执行return。
return可以用于void函数中的提前退出,类似于break。
void也可以是return 表达式,但是表达式的值必须是另一个返回void的函数。
2.有返回值函数
return语句的返回值类型必须与函数的返回类型相同,或者能隐式转换为函数的返回类型。
在含有return语句的循环后面必须有一条return语句。
返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
**不要返回局部对象的引用或指针,**因为函数完成后它所占用的存储空间也会被释放掉,因此引用或指针将指向不再有效的内存区域。
如果函数返回指针,引用,或者类的对象,我们可以使用函数调用结果访问其成员函数。
main函数可以没有return语句直接结束,编译器会隐式插入一条return 0,0表示执行成功,非0值的含义依机器来定,也可以用cstdlib头文件中定义的预处理变量作为return值表示成功与失败。
3.递归函数
一个函数调用了自身。递归中一定有某条路径是不包含递归调用的,不然程序会一直执行下去。
这个函数递归终止的条件是val等于1。
int factorial(int val){
if(val>1)
return factorial(val-1)*val;
else
}
//计算一个数的阶乘
int factorial(int val){
if(val>1)
return factorial(val-1)*val;
return 1;
}
4.返回数组指针
数组不能被拷贝,所以函数不能返回数组,但是可以返回的指针或者引用。
注意返回类型和函数定义类型一致。
int (*func(int i)) [10]
表示返回类型是数组的指针。
使用尾置返回类型:任何函数的定义都能使用。但对于复杂的函数比较有效,比如返回类型是数组的指针或者引用。函数真正的返回类型跟在形参列表之后。
auto func(int i) -> int(*)[10]
或者使用decltype关键字。
4.函数重载
重载函数:同一作用域内几个函数名字相同但形参列表不同,称为重载函数。编译器会根据传递的实参类型推断想要的是哪个函数。比如一种数据库应用需要创建几个不同的函数分别根据名字,电话,号码等记录查询。
即返回类型,函数名相同,形参列表不同。
不允许返回类型不同,函数名相同,形参列表相同的两个函数出现。
main函数不能重载。
//这两个是相同的,形参的名字不影响形参列表的内容
record lookup(const account &acct);
record lookup(const account &);
//这两个相同,第二个是第一个的别名,别名并不创建新类型
typedef phone telno;
record lookup(const phone&);
record lookup(const telno&);
**重载和const形参:**顶层const是指针本身是个常量,底层const是指针所指对象是一个常量。
顶层const的形参和没有顶层const的形参不可区分。
record lookup(phone); record lookup(const phone);
record lookup(phone*); record lookup(phone* const);
底层const的形参和没有底层const的形参通过区分其所指对象是常量还是非常量可以区分。
record lookup(account&); record lookup(const account&);
record lookup(account*); record lookup(const account*);
重载不重载取决于是不是比原来容易理解。
重载和const_cast(显式类型转换)
**调用重载的函数:有三种结果,**最佳匹配,无匹配,二义性调用(多于1个函数可以匹配,但每一个都不是最佳选择)
重载与作用域:在内层作用域中声明函数名字,将会隐藏外层作用域中声明的同名实体。所以重载函数的声明都要放在同一个作用域内。
5.特殊用途语言特性
1.默认实参
函数形参可以有初始值,但是一旦某个形参被赋予了默认值,它后面所有的形参都必须有默认值。
调用函数如果想使用默认实参,调用的时候省略该实参就可以了。默认实参负责填补尾部的实参,所以设计的时候,尽量让不怎么使用默认实参的形参出现在前面,经常使用默认实参的出现在后面。
给定作用域中,一个形参只能被赋予一次默认实参。
局部变量不能作为默认实参。
2.内联函数和constexpr函数
一次函数调用包含很多工作,对于规模较小的函数,在其返回类型前添加inline关键字可以把函数指定为内联函数,消除调用的开销。
**constexpr是能用于常量表达式的函数,**其函数返回值和形参类型都要是字面值类型,且只有一个return语句。
内联函数和constexpr函数通常被定义在头文件中。
3.调试帮助(assert和NDEBUG)
程序包含一些用于调试的代码,但只在开发的时候使用,发布时要屏蔽调试代码。
需要用到两项预处理功能:assert和NDEBUG.
assert是一种预处理宏,由预处理器而不是编译器管理,定义在cassert头文件中。
assert(表达式); 表达式为假,assert输出信息并终止程序,为真什么也不做。
assert的行为依赖于NDEBUG这个预处理变量的状态,使用#define语句定义NDEBUG可以关闭调试状态。
还可以用NDEBUG定义自己的调试代码,如果NDEBUG未定义,将执行#idndef和#endif之间的代码,否则这些代码会被忽略。
6.函数匹配
当重载函数形参数量相等,类型可以相互转换时:确定候选函数(同名,调用点可见)—确定可行函数(形参与实参数量相等(如果函数由默认实参,那么调用时传入的实参数量可以少于实际使用的实参),类型相等或者可以转换)—确定最佳匹配(实参形参类型越接近,匹配得越好,如果每个函数匹配都不好的话,编译器会报告二义性调用的错误)
如果重载函数的区别在于形参是否使用了引用了const,则编译器通过实参类型选择合适的函数。
7.函数指针
只需要用指针(*p
)替换函数名即可。注意括号不能少,不然就是声明一个函数,返回int*
类型这种。
好烦不想看这一部分了,后面用到