第七章函数
7.1 函数的定义
7.2 参数传递
7.2.1 非引用形参
(1)指针形参
函数并不能改变实参指针的值,但是可以改变其所指对象的值。若要保护指针指向的值,则形参需要定义为指向const对象的指针:
void use_ptr (const int *p) {… }
此时,实参可以是const int *q或者是int *q,但是对于void use (int *p) {… },不允许实参是const int *q,因为不允许指向非const对象的指针指向const对象。
(2)const形参
如果将形参定义为非引用的const类型,在函数中不能改变实参的局部副本。不过,令人吃惊的是,尽管函数的形参是const,但编译器却将函数的形参声明为普通类型,这是为了兼容C语言。
void fcn (const int i) {} //原函数
void fcn (int i) {} //编译器之后的函数
7.2.2 引用形参
当需要修改实参值,或者复制对象代价太大时候,往往用引用形参。
(1)使用引用形参返回额外信息
将额外信息用引用形参表示。查找函数,返回指向该元素的迭代器以及出现次数:
vector<int>::const_iterator find_val (vector<int>::const_iterator beg,
vector<int>::const_iterator end, int value, vector<int>::size_type &occurs )
{
vector<int>::const_iterator res_iter = end;
occurs= 0;
for( ; beg != end; ++beg)
{
if(value == *beg)
{
++occur; //保存次数
if(res_iter == end) //res_iter保存指向第一个值为value的迭代器
res_iter= beg;
}
}
return res_iter;
}
(2)利用const引用避免复制
如果使用形参的唯一目的是避免复制实参,则应将形参定义为const引用。
(3)更灵活的指向const的引用
int incr (int &val) { return ++val; }
short v1 = 0; const int v2= 42; int v3 = 2; int v4;
v4= incr (v1); v4 = incr (v2); //error,不会进行类型转换
应该将不需要修改的引用形参定义为const引用。普通的非const引用形参在使用时不灵活。既不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化。
(4)传递指向指针的引用
void ptrswap (int *&v1, int *&v2) { …}
int *&v1;表示:v1是一个引用,与指向int型对象的指针相关联。
7.2.3 vector和其它容器类型的形参
通常,函数不应该有vector或其它标准库容器类型的形参。调用含有普通的非引用vector形参的函数将会复制vector的每一个元素。但也不推荐用const引用,更倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器。
7.2.4数组形参
void printValues (int *p) {} //更好,因为与实际效果最接近
void printValues (int p[]) {}
void printValues (int p[10]) {}
上述3种形参表示效果相同,C++编译器自动忽略形参的数组长度,当编译器检查数组形参关联实参时,它只会见擦实参是不是指针、指针的类型和数组元素的类型是否匹配,而不会检查数组的长度。当不需要修改数组形参的元素时,应该定义为指向const对象的指针。
数组的形参可以声明为数组的引用int (&arr)[10],这时,编译器不会将数组实参转化为指针,而是传递函数的引用本身。这样,数组的大小就成为形参和实参类型的一部分,就会检查其是否匹配,这更安全。
多维数组在参数传递时,除了第一维以外的所有维的长度都是元素类型的一部分,必须明确指定。
7.2.5 传递给函数的数组处理
任何处理数组的程序都要确保程序停留在数组的边界内,有3种编程技巧确保:
(1)在数组本身放置一个标记来检测数组的结束。C风格字符串就是例子,用null。
(2)使用标准库规范,传递指向数组的第一个和最后一个元素的下一个位置的指针。
(3)显示传递表示数组大小的形参。
7.2.6 main:处理命令行选项
int main (int argc, char *argv[]) { … }
第一个形参argc用于传递数组argv中字符个数,第二个形参argv是一个C风格字符串数组。用命令行执行程序时,如果输入如下命令:
prog -d -o ofile data0 //prog是该可执行文件文件名
那么argc为5,argv[]中元素为:”prog”,”-d”,”-o”,”ofile”,”data0”。
7.2.7 含有可变形参的函数
void foo (parm_list, …); //parm_list是显示声明的形参
void foo(…);
C++中的省略符形参是为了编译使用了varargs的C语言程序,只能讲简单数据类型传递给含有省略符形参的函数。
7.3 return语句
7.3.1 没有返回值的函数
return;
return fun (…); //void fun (…) {…} 返回另一个返回值为void的函数调用结果。
7.3.2 具有返回值的函数
return expression;
在含有return语句的循环后面没有提供return语句是很危险的,编译器不能检测这个问题。
(1)主函数main的返回值
返回类型不是void的函数必须返回一个值,有个例外:允许main函数没有返回值就结束,编译器会隐式插入返回0的语句。
return EXIT_FAILURE; //运行失败
return EXIT_SUCCESS; //运行成功,都在cstdlib头文件中定义
(2)返回非引用类型
函数的返回值用于初始化在调用函数处创建的临时对象。
(3)返回引用
函数返回引用类型时,没有复制返回值,而是返回对象本身。千万不能返回局部变量的引用,因为当函数执行完毕,将释放分配给局部对象的存储空间。
(4)引用返回左值
char &get_val (string &str, string::size_type ix) { return str[ix]; }
get_val(s,0) = ‘A’; //给函数返回值赋值,因为函数返回的是一个引用。
7.3.3 递归
递归函数必须定义一个终止条件,main函数不能调用自身。
7.4 函数声明
函数声明由函数返回类型、函数名和形参列表组成,这3个元素成为函数原型。提倡在头文件中提供函数声明,在源文件中定义。函数声明时候,形参只需要提供类型,不必指明形参名,就算指定,编译器也会忽略。
默认实参是通过给形参表中的形参提供初始值来指定。既可以在函数声明也可以在函数定义中指定默认实参。但是在一个文件中,只能为一个形参指定默认实参一次。通常,应该在函数声明中指定默认实参,并且放在头文件中。如果在函数定义的实参表中提供默认实参,那么只有在包含该函数定义的源文件中调用该函数时,默认实参才有效。
//ff.h
int ff(int = 0);
//ff.c
#include“ff.h”
int ff (int i = 0) { …} //error,在一个文件中多次指定默认实参
7.5 局部对象
默认情况下,局部变量的生命周期局限在所在函数的每次执行期间。如果一个变量位于函数作用域内,但生命周期却跨越了这个函数的多次调用,这种变量应该定义为static(静态局部变量)。这种对象一旦定义,在程序结束前都不会被撤销。
7.6 内联函数
一些小操作定义为函数有很多优点,但是存在的开销大的缺点,这时,就可以用内联函数来避免函数调用的开销。内联函数只需要在普通函数定义前面加上inline关键字。内联函数会在编译时,自动在每个函数调用点“内联地”展开。内联函数应该在头文件中定义,这点不同于其他任何函数。
7.7 类的成员函数
7.7.1 定义成员函数的函数体
(1)this指针
每个成员函数都含有额外的、隐含的形参this,将该成员函数与调用该函数的类对象捆绑在一起。在调用成员函数时,形参this初始化为调用函数的对象的地址。
const对象、指向const对象的指针或引用只能用于调用其const成员函数,如果尝试用它们来调用非const成员函数,则是错误的。
7.7.2 在类外定义成员函数
double Sales_item::avg_price () { }
7.7.3 构造函数
函数名与类名相同,没有返回类型。
Sales_item(): units_sold(0), revenue(0.0) { } //中间为初始化列表
由编译器创建的默认构造函数通常称为合成的默认构造函数,一般适用于仅包含类类型成员的类。而对于含有内置类型或复合类型成员的类,通常应该定义它们自己的默认构造函数初始化这些成员。
7.7.4 类代码文件的组织
类定义应置于名为type.h的头文件中,而类外成员函数的定义则一般存储在type.cc的源文件中,type为类名。
7.8 重载函数
出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则称为重载函数。任何程序都仅有一个main函数实例,不可以被重载。
7.8.1 重载与作用域
一般的作用域规则同样适用于重载函数名。如果局部地声明一个函数,则该函数将屏蔽而不是重载在外层作用域声明的同名函数。
C++中,名字查找发生在类型检查之前。
7.8.2 函数匹配与实参转换
编译器实现调用与函数的匹配时,结果有三种可能:
(1) 找到与实参最佳匹配的函数;
(2) 找不到匹配的,给出编译错误信息;
(3) 存在多个匹配,调用具有二义性。
7.8.3重载确定的三个步骤
(1)确定该调用所考虑的重载函数集合,该集合中的函数称为候选函数;
(2)从候选函数中选择一个或多个可行函数;
(3)确定最佳匹配,若有多个可行函数,却无最佳匹配则出现二义性。
7.8.4 实参类型转换
为了确定最佳匹配,编译器将实参到相应形参类型的转换划分等级。转换等级以降序排列如下:(1)精确匹配;(2)通过类型提升(5.12.2);(3)通过标准转换(5.12.3);(4)通过类类型转换。
仅当形参是引用或指针时,形参是否为const才有影响。
7.9 指向函数的指针
函数指针是指指向函数的指针,函数类型由返回类型以及形参表确定,而与函数名无关。
bool (*pf) (const string &, const string &);
将pf声明为一个指向函数的指针,它所指向的函数带有两个const string &类型的形参和bool类型的返回值。
可以使用typedef简化函数指针的定义:
typrdef bool (*cmpFcn) (const string &, const string &);
cmpFcn pf; //等效于上面的定义
函数指针只能通过同类型的函数或函数指针或0值常量表达式进行初始化或赋值。指向不同函数类型的指针之间不存在转换。
非零值且初始化后的函数指针,可以直接调用所指向的函数,不需要解引用符。
函数的形参可以是指向函数的指针,有两种形式编写:
void useBiger (const string &, bool (const string &, const string &) );
void useBiger (const string &, bool (*) (const string &, const string &) );
函数的返回可以是指向函数的指针:
int( *ff(int) ) (int *, int); //函数ff带有1个int形参,返回类型是int(*)(int *, int)
typedef int (*PF) (int *, int); PF ff (int); //更容易理解