6.1函数基础
一个典型的函数定义包括以下部分:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。
编写函数
下面我们将编写一个计算阶乘的函数
int fact(int val)
{
int ret = 1;
while(val>1)
ret*=val--;//把ret和val的乘积赋给ret,然后将val减一
return ret;
}
要想使用这个函数,我们可以在主函数中对其进行调用:
int main()
{
int n = 5;
int a = fact(n);
//此时a的值就为5的阶乘
}
调用函数的过程是:在遇到被调函数(fact)时,程序将控制权交到被调函数上,在进行到被调函数的return语句或最后一句是,再将控制权转回到主调函数main(上),再执行剩下的语句。
实参和形参
形参是指函数头内的参数,例如上述的val就是形参;而实参是指传入函数内的参数,上述的n就是实参。在调用函数有以下几个点需要注意:
- 传入的实参类型必须与形参相符,或者可以互相转换的
- 传入实参的数量必须与函数体形参数量相同
函数声明
与变量类似,函数在使用前也必须声明,如果一个函数永远也不会被用到,那么这个函数可以只有声明而没有定义。因为在参数传递时,编译器会创建一个新的对象用于拷贝该值。
6.2参数传递
我们在前面提到过,定义一个引用不会创建一个新对象,所以可以将函数的形参声明为引用,这样可以节约部分内存。
参数定义为引用可能会导致实参的修改,所以参数也可以声明为const形式,例如
int fact(const int &val);
将函数声明成以上形式,则传入的值不会通过val被无意改变。
数组形参
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时通常会将其转换为指针。因为不能拷贝数组,所以我们在传递数组时无法使用值传递,事实上当我们在为函数传递一个数组时,实际上传递的是指向数组首元素的指针。下面是几种数组作为形参的形式:
void print(const int*);
void print(const int []);
vvid print(const int [10]);//这里期望数组大小为10,但实际上无所谓
上述三个函数都将数组形参定义成了指向const的指针,当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针,只有当函数确实要改变元素值的时候,才把形参定义为指向非常量的指针。
数组引用形参
因为C++中允许有数组的引用,所以函数的形参也可以是数组的引用:
void print(int (&arr)[10]);
main:处理命令行选项
我们之前所用的main函数,其参数列表都为空。然而,有时候我们确实需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作,如:
int main(int argc,char *argv[]){...}
当实参传递给,main函数后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的值保证为0。
例如:argc为5,
argv[0] = “prog”;
argv[1] = “-d”;
argv[2] = “-o”;
argv[3] = “ofile”;
argv[4] = “data0”;
argv[5] = 0;
含有可变形参的函数
有时我们无法预知应该向函数传递几个实参,为了能够编写不同数量的实参函数,C++11提供了两种主要的方法:如果所有的形参类型相同,则可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是可变参数模板,它将在16章中被介绍。
initializer_list的主要用法如下图:
和vector一样,initializer_list也是一种模板类型,定义此类型对象时,必须说明列表中所含元素的类型。和vector不同的是,initializer_list中的元素都为常量值,我们无法修改。
void error_msg(initializer_list<string> il);
void error_msg(ErrCode e,initializer_list<string> il);
//含有initializer_list类型的函数也可以同时含有其他类型。
6.3返回类型和return语句
return语句终止当前正在执行的函数,并将控制权返回到调用该函数的地方,return语句有两种形式:
return;
return espression;
无返回值函数
没有返回值的return语句只能用于返回类型是void的函数中,且返回类型为void的函数不要求必须有return语句,通常情况下,void函数如果想在它的中间位置提前退出,可以使用return语句。例如编写一个交换数值的函数,在两数相等时直接退出 :
void swap(int &v1,int &v2)
{
//如果两个值相等,则不需要交换,直接退出
if(v1==v2)
return;
//如果程序执行到这里,说明还需完成一些功能
int temp = v2;
v2 = v1;
v1 = temp;
//此处无需显示的return语句
}
有返回值的函数
return语句返回值类型必须与函数返回类型相同,或者能隐式的转换成函数的返回类型。要注意的是,函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的区域,所以不要返回局部对象的引用或指针。
函数的返回类型决定函数的调用是否是左值。调用一个返回引用的函数得到左值,其他类型返回右值。
主函数main的返回值
之前介绍过,如果函数的返回类型不是void,那么它必须返回一个值。但这条规定有个例外,我们允许main函数没有return语句直接结束。
函数递归
如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数是递归函数。例如,我们可以使用递归函数重新实现求阶乘的过程:
int factorial(int val)
{
if(val>1)
return factorial(val-1)*val;
return 1;
}
下面的表格显示了当给factorial函数传入参数5时,函数的执行轨迹
返回数组指针
因为数组不能被拷贝,所以函数不能返回数组,但是可以返回指向数组的指针或引用。返回数组指针函数形式如下:
Type(*function(parameter_list)) [dimension],其中Type表示元素的类型,dimension表示数组的大小。(*function(parameter_list))两端的括号必须存在,如果没有这对括号,函数的返回类型将是指针的数组。
int (*func(int i)) [10];可以按照以下的顺序来逐层的理解该声明的含义:
- func(int i)表示调用func函数需要一个int实参
- (*func(int i))意味着我们可以对函数的调用结果执行解引用操作
- (*func(int i)) [10]表示解引用func的调用将得到一个大小是10的数组
- int (*func(int i)) [10]表示数组中的元素是int类型
使用尾置返回类型
在C++11标准中还有一种可以简化上述func的声明方法,任何函数的定义都能使用尾置返回类型,但是这种形式对于返回类型比较复杂的函数最有效。
auto func(int i) -> int(*)[10];
因为我们把函数的返回类型放在了形参列表后,所以我们可以清楚的看到func函数返回的是一个指针,并且该指针指向了含有十个整数的数组。
6.4函数重载
如果几个函数名字相同,但形参列表不同,我们称之为重载函数。如:
void print(const char*p);
void print(const int *beg,const int *end);
void print(const int ia[],size_t size);
这些函数接受的形参类型不一样,但是执行的操作非常类似。当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数:
int j[2] = {0,1};
print("Hello World");//调用print(const char*)
print(j,end(j),-begin(j));//调用print(const int*,size_t)
print(begin(j),end(j));//调用print(const int*,cosst int*)
函数的名字仅仅是让编译器知道它调用的是哪个函数,而函数重载可以一定程度上减轻程序员起名字、记名字的负担。
6.5特殊用途语言特性
默认实参
某些函数有这样一种性质,在函数的很多次调用中它们都被赋予同一个值,此时,我们把这个反复出现的值称为函数的默认实参。例如,我们使用string对象来表示窗口的内容。一般情况下,我们希望窗口的高、宽和背景字符都使用默认值。但是同时我们也应该允许用户为这几个参数自定义值,所以我们把它定义成如下形式:
typedef string::size_type sz;
string screen(sz ht = 24,sz wid = 80,char backgrnd = ' ');
其中我们为每个形参都提供了默认实参,要注意的是,一旦某个形参被赋予默认实参,那么它后面的每一个形参都要赋予默认实参。
想使用该实参,只需在调用是省略该实参就可以了:
string window;
window = screen();
window = screen(66);
window = screen(66,256);
window = screen(66,256,'#');
函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置),例如想要覆盖backgrnd的值,就必须为ht和wid提供实参:
window = screen(,,'?');//错误,只能省略尾部实参
window = screen('?');//调用screen('?',80,'' );
内联函数和constexpr函数
把规模较小的操作定义成函数有以下好处:
- 阅读和理解函数的调用比读懂等价的条件表达式容易得多
- 使用函数可以确保行为的统一
- 如果我们需要修改函数,显然比找到所有等价的表达式再修改快得多
- 函数可以被重复利用,省去了程序员重新编写的代价
然而使用函数也包含一个潜在的缺点,调用函数一般比求等价的表达式的值要慢点。因为一次调用其实包含着一系列工作:调用前先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。
把函数定义成内联函数可避免调用的开销。在函数返回类型前加上inline,就可以声明成内联函数。
//shortererString函数用于返回更短的字符串
inlene string shorterString(string s1,string s2)
{
return (s1.size()<s2.size()?s1:s2);
}
//则如下调用
cout<<shorterString(s1,s2)<<endl;
//类似于:
cout<<(s1.size()<s2.size()?s1:s2)<<endl;
注:内联函数只是向编译器发出一个请求,编译器可以忽略这个请求。
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不太可能在调用点内联的展开。
constexpr函数是指能用于常量表达式的函数,函数的返回类型及所有形参类型都是字面值类型,而且函数体中有且只有一条return语句。
6.6函数匹配
与前面的函数重载内容所匹配,本节要介绍的内容就是如何确定某次调用选择哪个重载函数。
1.函数匹配的第一步是要选定本次调用对应的重载函数集,集合中的函数成为候选函数。
候选函数有两个特征:
- 一是与被调用函数同名
- 二是其声明在调用点可见
- 第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出来的函数称为可行函数
可行函数的两个特征:
- 一是其形参数量与本次调用提供的实参数量相等;
- 二是每个实参的类型与对应的形参类型相同,或是能转换成形参的类型。
- 第三步是从可行函数中找出与本次调用最匹配的函数
6.7函数指针
函数指针指向的是函数而非对象。
//比较两个string对象的长度
bool lengthCompare(const string &,const string &);
//要想声明一个指向函数的指针,只要使用指针替换函数名即可
//pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf)(const string &,const string &);//未初始化
随堂练习见下一篇博客。