第六章 函数
6.1 函数基础
函数的形参列表
不带形参的函数,两种定义方法
void f1(){} //隐式定义
void f2(void){} //显式定义
6.2 参数传递
6.2.1 传值参数
指针形参
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接的访问它所指的对象,所以通过指针可以修改它所指对象的值。
//Q:了解指针形参
void reset(int *p){
*p = 50;
cout << "形参p的值:" << p << endl;
p = 0;
cout << "p=0执行后,形参p的值:" << p << endl;
}
void e1(){
int i = 42;
cout << "i的初始值:" << i << endl;
int *q = &i;
cout << "实参q的值:" << q << endl;
reset(q);
cout << "函数运行过后i的值:" << i << endl;
cout << "函数运行过后实参q的值:" << q << endl;
}
以前很长一段时间都把指针形参等同于引用形参,这是错误的。要注意区分指针形参和引用形参的区别。指针形参还是值传递,引用形参是引用传递。
6.2.2 传引用参数
使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本不支持拷贝。当某种类型不支持拷贝操作时,函数只能通过引用形参(指针形参不行吗?)访问该类型的对象。
如果函数无需改变引用形参的值,最好声明为常量引用***(const type &i)***。
使用引用参数返回额外信息
想让函数返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。下面总结一下返回多个值的方法。
- 定义一个新的数据类型,比如XX类。返回这个类,这个类里面有你要的那些值
- 返回的是某个对象,还想返回其他东西,则形参定义引用,然后通过引用改变外面那个值(实参),最后在函数外使用实参即可得到。
- 返回类型void,全部使用指针或者引用来完成
6.2.3 const形参和实参
①
void fcn(const int i){} //fcn能读取i,但不能像i写值
void fcn(int i){} //错误,重复定义fcn(int)
上述代码看起来好像是重载,但是利用实参初始化形参时,如果形参有顶层const,传给它的常量对象或者非常量对象都是可以的。那么,当主调函数在调用fcn的时候,用的实参都是非常量对象,那编译器就无法区分到底应该调用哪一个fcn了,因为两个都可以匹配。
6.2.4 数组形参
//一维
void print(const int*);
void print(const int[]);
void print(const int[10]);
void print(int (&arr)[10]); //数组引用形参
对于多维数组来说,数组的第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略。
//多维
void print(int (*matrix)[10],int rowSize); //第二维是10个int的数组,其实这么看*(matrix[10])更好理解
void print(int matrix[][10],int rowSize);
6.2.5 main:处理命令行选项
prog -d -o ofile data0
int main(int argc,char *argv[]){}
int main(int argc,char **argv){}
当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。
如上面命令行,argc为5。
argv[0] = "prog"; //argv[0]保存程序的名字,而非用户输入
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
6.2.6 含有可变形参的函数
initializer_list形参
如果函数的实参数量未知但是全部实参的类型都相同,即可用它。注意包含头文件***<initializer_list>***。
initializer_list对象中的元素永远是常量值。无法改变initializer_list对象中元素的值。
//Q:initializer_list形参 -- 只有initializer形参
void error_msg(initializer_list<string> li){
for(const auto &elem : li){ //还可以使用迭代器来实现
// elem = "okay"; //试图改变initializer_list对象中的元素,编译报错。因为initializer_list对象中的元素是常量
cout << elem << endl;
}
}
void e3(){
string i = "i is false", j = "j is true";
if(i == j){
error_msg({"i == j",i,j,"okay"});
}else{
error_msg({"i != j",i,j});
}
}
//Q:initializer_list形参 -- 还拥有其他形参
void error_msg(int code,initializer_list<string> li){
cout << "Err_code is " << code << endl;
for(auto begin = li.begin(),end = li.end(); begin != end ; ++begin){
cout << *begin << endl;
}
}
void e4(){
string i = "i is false", j = "j is true";
if(i == j){
error_msg(520,{"i == j",i,j,"okay"});
}else{
error_msg(404,{"i != j",i,j});
}
}
省略符形参
一般用于与C函数交互的程序接口。这些代码使用了名为varargs的C标准库功能。
省略符形参只能出现在形参列表的最后一个位置,他的形式无外乎两种:
void foo(param_list,...){}
void foo(...){}
在第一种形式中,形参声明逗号后面是可选的。
省略号的优先级别最低,所以在函数解析时,只有当其它所有的函数都无法调用时,编译器才会考虑调用省略号函数的。
由于还涉及到一些新的类型和方法书上没有细说,以后再仔细研究
6.3 返回类型和return语句
return有两种形式。
return ;
return expression;
6.3.1 无返回值函数
没有返回值的函数只有void返回值类型的函数。这种函数最后无需显式的写return语句,因为在这类函数的最后一句后面会隐式的执行return。
当然,void返回值类型的函数也可以显式的写 return expression 类型的语句。但是,此时return语句的expression必须是另一个返回void的函数(这句其实调用了一个函数)。强行令void函数返回其他类型的表达式将产生编译错误。
6.3.2 有返回值函数
主函数main的返回值
6.3.1里面说过如果函数返回值类型不是void,那它必须返回一个值。除了main函数。如果main函数没有return语句直接结束,编译器将隐式的插入一条返回0的return语句。
为了使返回值和机器无关,***cstdlib***头文件定义了两个预处理变量。
int main(){
if(flag){
return EXIT_FAILURE; //定义在cstdlib头文件中
}else{
return EXIT_SUCCESS; //定义在cstdlib头文件中
}
}
递归
main函数不能调用他自己。
6.3.2 返回数组指针
使用类型别名的方法来定义
typedef int arrT[10]; //或者 using arrT = int[10];
arrT* func(); //返回一个指向含有10个整数的数组的指针
返回数组指针的函数,普通定义
Type (*function(parameter_list))[dimension]
注意要有括号,如果没有括号,将会返回指针的数组。
下面举个具体的例子:
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类型。
使用尾置返回类型
尾置返回类型跟在形参列表后面并以一个 -> 符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto:
auto func(int i) -> int( ***** ) [10];
注意这个带括号的解引用符,表示func函数返回的是一个指针,并且改指针指向了含有10个整数的数组。
使用decltype
如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。
int odd[] = {1,3,5,7,9};
int even[] = {2,4,6,8,0};
//返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i){
return (i%2) ? &odd : &even; //返回一个指向数组的指针
}
这里涉及到一个知识点,***decltype***对于数组类型的解析,并不会把数组类型转换成对应的指针。所以*decltype的结果是一个数组。因此需要一个来表示返回的是数组指针。
6.4 函数重载
如果同一个作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
main函数不能重载。
对于重载函数来说,他们应该在形参数量或形参类型上有所不同。
重载和const形参
前面说过如果形参是顶层const,实参可以是常量也可以是非常量。因此,像下面这种情况:
int findNum(Phone);
int findNum(const Phone);
不是重载,因为const是顶层const,实参可以是常量也可以是非常量。还有下面这种情况:
int findNum(Phone*);
int findNum(Phone* const);
Phone* const 说明指针是个常量。是顶层const。因此这也不是重载。
当然如果形参是底层const,这样构成重载。因为如果实参是常量,不能传给非底层const的形参,因为可以传的话就相当于可以用形参来改变常量,不符合逻辑。而实参不是常量的话,看起来形参是不是底层const都可以。但是一旦有这种重载,编译器就会区分,如果是非常量编译器会优先选择非常量版本,如果是常量只能选择常量版本。
int findNum(Account&);
int findNum(const Account&);
构成重载,因为第一个不是常量引用,第二个是常量引用,底层const。记住,对于引用来说,不管const怎么放,都是底层const。
int findNum(Account*);
int findNum(const Account*);
同上一个类似,因为const Account* 是指向常量的指针,是底层const。
const_cast 和重载
通过下面的例子来了解用法。前面提过const_cast,是用来显式转换的,只能改变运算对象的底层const。
const string &shorterString(const string &s1, const string &s2){
return s1.size() <= s2.size() ? s1 : s2;
}
string &shorterString(string &s1, string &s2){
auto &r = shorterString(const_cast<const string&>(s1),const_cast<const string&>(s2));
return const_cast<string&>(r);
}
其实上面两个函数,因为形参列表不同且第一个函数const不是顶层const,所以是重载函数。这里主要是展示当需要普通string的时候,如何通过普通的非常量实参,去调用函数。并且返回的也是非常量实参。
利用const_cast来实现,当在第二个函数中调用第一个函数时,利用其把非常量转换成常量,最后返回一个常量引用。注意这个r其实是对s1或s2的一个引用,只是在这里是常量引用,这是符号逻辑的。由于第二个函数最后需要的是非常量引用,因此可以利用const_cast再次转换,然后返回。
6.5 特殊用途语言特性
6.5.1 默认实参
一旦某个形参被赋予了默认值,它后面所有形参都必须有默认值。
int find(int ht = 24 , int wd = 80 , char backgrnd = '');
想要覆盖backgrnd的默认值,必须为ht和wd提供实参。
默认实参声明
同一个函数可以多次被声明,但是在给定作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,并且该形参右侧的所有形参都必须有默认值(一旦某个形参被赋予了默认值,它后面所有形参都必须有默认值)。
假如给定
int find(int a,int b,char c = '');
我们不能修改一个已经存在的默认值:
int find(int a,int b,char c = '*'); //错误,重复声明
但是可以按照如下形式添加默认实参:
int find(int a = 24,int b = 80,char c);
默认实参初始值
局部变量不能作为默认实参
//Q:默认实参初始值案例
//A:局部变量不能作为默认实参,其他可以,但是要注意位置。
int ht(){
return 55;
}
int wd = 100;
char def = 'a';
int myFind(int ht = ht(), int wd = wd, char def = def){
cout << " ht = " << ht
<< " wd = " << wd
<< " def = " << def << endl;
}
void e5(){
wd = 200; //wd值被改变,由于改变的是全局变量wd,且在调用之前改变的,所以默认实参也就改变了
char def = 'b'; //局部变量def被改变,并不影响默认实参,局部变量不能作为默认实参
myFind();
}
6.5.2 内联函数和constexpr函数
内联函数可以避免函数调用的开销
在函数的返回类型前面加上关键字***inline***,这样就可以将它声明成内联函数了。内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。一般来说内联函数用于规模小、流程直接、频繁调用的函数。所以如果函数声明为内联函数,但是规模较大,编译器很可能只是把他当成普通函数来处理。
constexpr函数
定义constexpr函数的方法和其他函数类似,不过要遵循几项约定:
-
函数的返回类型及所有形参的类型必须是字面值类型(算术类型,引用和指针都属于字面值类型)
-
函数中必须有且仅有一条***return***语句
constexpr被隐式的指定为内联函数。
我们允许constexpr函数的返回值并非一个常量。
constexpr int scale(int cnt){ return 42 * cnt; }
当实参是常量表达式的时候,他的返回值也是常量表达式,反之则不然
int arr[sacle(2)]; //正确,scale(2)是常量表达式 int i = 2; int a[scale(i)]; //错误,scale(i)不是常量表达式
记得前面之前说过,constexpr函数就是为了编译的时候就得到结果。而且得到常量的结果。但是constexpr函数不一定返回常量表达式。上面的例子,如果需要的是常量表达式,那就得返回常量表达式,否则就会报错。
内联函数和constexpr函数放在头文件
和其他函数不同,内联函数和constexpr函数在程序中可以多次定义。但是多个定义必须完全一致。所以内联函数和constexpr函数通常定义在头文件中。
在这里突然有点疑问。其他函数只能定义一次,内联函数和constexpr函数可以多次定义但必须完全一致,那和一次定义有什么区别吗?为什么这里强调通常把他们定义在头文件中。
想了一个解释,因为内联函数和constexpr函数一般来说要较小的函数,所以放在头文件中比较合适。而一般的函数由于要用到很多东西,有时还要使用到对应文件的数据,所以在程序中定义较合适。
6.7 函数指针
函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
bool lengthCompare(const string &,const string &);
该函数类型是***bool (const string &,const string &)***。声明一个指向函数的指针,只需要用指针替换函数名即可,bool ( ***** ***pf)(const string &,const string &)***。
使用函数指针
①
//赋值
pf = lengthCompare;
pf = &lengthCompare; //和上面是等价的,取地址符是可选的
②
//使用,无需提前解引用
bool b1 = pf("hello","goodbye");
bool b2 = (*pf)("hello","goodbye"); //等价调用
bool b3 = lengthCompare("hello","goodbye"); //另一个等价调用
注意,不同函数类型的指针不存在转换规则。使用上述的***pf***,只能用于***bool (*** ***** ***pf)(const string &,const string &)***类型的函数。
//Q:函数指针类型及调用的案例
//A:函数指针只能指向同类型的函数,不能转换。
void func1(int a){
cout << "func1被调用" << endl;
}
void func2(int b){
cout << "func2被调用" << endl;
}
void e6(){
void (*p)(int) = func1; //定义函数指针,且指针初始化指向func1
p(1); //通过函数指针的方式调用函数,没有用解引用符
p = func2; //给函数指针重新赋值,这是同类型赋值
(*p)(2); //通过函数指针的方式调用函数,用解引用符
}
重载函数的指针
注意一点,使用重载函数的指针,必须清楚的界定用哪个函数。因为重载函数形参列表不同,使用重载函数的类型不同,则重载函数的指针就不同。
函数指针形参
函数指针可以作为形参。
void userFuncPointer(void (*pf)(int),int x){
pf(x);
}
当然上面那么写比较麻烦,可以使用别名替代。如下:
//Q:函数指针形参案例
//Func 和 Func2 是函数类型
typedef void Func(int);
typedef decltype(func1) Func2; //等价的类型
//FuncP 和 FuncP2 是指向函数的类型
typedef void (*FuncP)(int);
typedef decltype(func1)* FuncP2; //等价的类型
void userFuncPointer(FuncP2 pf,int x){ //FuncP2换成上面任意一种都可以
pf(x);
}
void e7(){
userFuncPointer(func1,1);
userFuncPointer(func2,2);
}
注意一点,decltype返回函数类型,此时不会将函数类型自动转换成指针类型,需要自己加指针,如FuncP2。
返回指向函数的指针
注意是返回指向函数的指针,不能返回函数。不使用类型别名的情况写函数***f1***,返回的是一个函数指针。
int (*f1(int))(int*,int);
从内向外看
-
***f1(int)***是一个函数。
-
*f1前面有一个。说明要返回指针
-
这个指针有形参列表,说明返回的是函数指针
-
最开头的那个***int***,说明函数指针指向的函数,返回类型是***int***
上述写法较麻烦,可以使用别名。
using F = int(int*,int); //F是函数类型,不是指针
using PF = int(*)(int*,int); //PF是函数指针
注意返回的是函数指针,使用返回类型应该是 PF 或者 F***** ,不是***F***。
将auto和decltype用于函数指针类型
注意一点,decltype返回函数类型,不是函数指针。