1、函数组成
一个函数的定义由4部分组成:返回值类型、函数名、0个或多个形参组成的参数列表、函数体。
1.1、函数调用和返回
函数的调用完成2项工作:一是用实参初始化函数形参,二是将控制权交给被调函数;
函数的return语句也完成2项工作:一是返回return语句的值,二是将控制权还给主调函数。
1.2、实参与函数的形参
实参要与函数的形参类型匹配、数量一致,实参的类型如果可以转换成形参类型也是可以的。
1.3、函数的返回类型
函数的返回类型不能是数组或函数类型,但可以是指向数组或函数的指针。
1.4、函数声明
函数只能定义一次,但可以声明多次,函数声明不需要函数体,因此形参可以不用名字。函数的声明应该放在头文件中,定义放在源文件中,源文件应该要包括此头文件。
2、局部变量
形参和函数内部的变量统称为局部变量。只存在于块执行期间的对象称为自动对象,形参是一种自动对象,在函数开始时为形参申请内存空间,函数结束时被销毁。在所有函数体之外定义的变量存在于程序的整个执行过程中,在程序启动时被创建,程序结束时才会销毁。局部静态对象,用static定义,在程序执行路径第一次经过定义语句时初始化,直到程序结束时才被销毁,在此期间,即使对象所在的函数执行结束毁也不会对它有影响。
3、参数传递
一般,参数传递有2种方式,与形参的类型相关,一类是值传递,一类是引用传递,其中,值传递包括一般形参和指针形参。
3.1、值传递
将实参的值拷贝后赋值给形参,形参与实参是两个独立的对象
(1)一般值传递
形参是一般类型,对形参的改变不会影响实参。
(2)指针传递
形参是指针,将实参的指针拷贝赋值给形参,形参和实参是2个独立的指针,但是指向同一个对象。
void fun(int*p){
*p=0;//改变指针指向对象的值
p=0;//改变了ip的局部拷贝,实参并未改变
}
int i=1;
fun(&i);//改变i的值,而非地址
3.2、引用传递
与一般的引用一样,引用形参是它对应实参的别名。对形参的改变就是对实参的改变。
- 使用引用可以避免拷贝
拷贝大的类型对象或容器对象时比较低效,甚至有的类类型根本不支持拷贝操作,此时,只能通过引用形参访问该类型对象。
- 使用引用形参返回额外信息
一个函数只能返回一个值,然而有时候函数需要返回多个值,如何定义函数使得它能返回多个值呢?
一种情况,定义一个新的数据类型作为返回值,另一种就是给函数传入一个额外的引用实参。
建议使用引用类型的形参代替指针
4、const形参
4.1、观察下面2个函数:
void fun(const int i){};
void fun(int i){};
上面的2个函数如果出现在同一个文件中就会出现错误,因为第一个函数的形参的顶层const会被忽略,
程序会认为两个函数的形参是相同的。
4.2、形参尽量使用常量引用
常量引用类型的形参可以接受任何能转化成其类型的实参,但是一般引用的形参只能接受与形参类型一致的实参,限制了函数的使用范围,并且在一些不需要修改实参的函数中,使用常量引用,一方面避免了对实参的修改,另一方面避免了对实参进行拷贝,因此,函数的形参尽量使用常量引用,比如:
void fun(string &s);//只能接受string类型的实参,不能接受const string、字面值字符串类型,像fun("abc")这样调用都是错误的
void fun(const string &s);//可以接受const string、字面值字符串类型,像fun("abc")这样调用是可以的
5、数组形参
5.1、数组的传递
数组有2个重要的性质:一是不能直接拷贝数组,二是在使用数组时通常将其转换成指针。因此,在函数调用时,实际上传递的时指向数组首元素的指针。下面的三个函数是等价的:
void print(const int*);
void print(const int[ ]);
void print(const int[10]);//虽然显示的数组大小,但是实际不一定
上面三个函数传递的都是指向数组首元素的指针const int*,数组的大小对函数的调用没有影响。如果传入的是个数组,则自动被转换成指针。
5.2、数组的使用
因为数组是以指针的形式传入的,所以不知道数组的大小,使用者需要提供额外的信息。有三种管理数组大小的方法:
(1)使用标记
这种方法要求数组本身包含有明显的结束标记,容易与普通数据区分的情况。比如C风格字符串,存储在字符数组中,在数组的最后一个字符是空字符,以此来判断数组的结束。
void print(const char*s){
if(s)//如果指针非空
while(*s)//如果指针所指的字符不是空字符
cout<<*s++;
}
(2)使用标准库
这种方法传递的是指向数组首元素和尾后元素的指针(指向数组最后一个的元素的下一位置),需要用到标准库begin和end函数。
void print(const int*beg,congst int *end){
while(beg!=end)
cout<<*beg++;
}
调用
int a[2]={10,1};
print(begin(a),end(a));
(3)显式传递数组大小
这种方法专门定义一个形参用来传递数组的大小。
void print(const int a[],size_t size){
for(size_t i=0;i!=size;i++){
cout<<a[i];
}
}
5.3、引用类型的数组形参
形参可以是数组的引用:
void print(const int (&a)[10]){
for(auto elem:a)
cout<<elem;
}
5.4、多维数组的形参
多维数组的第二维(以及后面所有维度)的大小都是数组类型的一部分,不可省略。在调用多维数组时会转换成指向其首元素的指针,多维数组的首元素还是个数组,因此,形参需要是一个指向数组的指针:
void pirnt(const int(*matrix)[10],int rowSize){}
void print(const int matrix[][10],int rowSize){}
上面2个函数是等价的,第二个函数实际上是指向含有10是整数的数组的指针。如果上面2个函数出现在同一个文件中,则编译不会通过。
6、函数返回值
函数的可以有返回值,也可以没返回值(void),除了void类型,其他所有类型都要有返回值。main函数是个例外,如果不写return语句,程序会默认添加return 0,表示执行成功,非0表示失败,具体什么值依机器而定。
6.1、返回机制
(1)值返回
返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
string fun(const string &s){return s;}//返回值s将被拷贝到调用点
因此,可以返回花括号包围的值的列表,如对于返回类型是vector<string>类型的函数,返回{“yes",”no"}是可以的
(2)引用返回
调用一个返回引用的函数得到左值,其他返回类型得到右值。我们能为返回类型是非常量引用的函数结果赋值:
char & fun(string &s,string::size_type i){
return s[i];
}
//调用
string s("abcd");
fun(s,0)='b';//将s[0]的值改为b
但是如果返回值类型是常量引用,那么不能给调用的结果赋值:
fun1(“abc”)=”x"//如果fun1的返回类型是const string &类型,那么不能给函数调用结果赋值,因为,函数调用已经为const类型初始化了,因此这个值就不能再改变了。
const string & fun(const string &s){return s;}//形参和返回类型都是const string,因此,调用函数和返回结果都不会拷贝s。
6.2、返回局部对象的引用或指针
函数完成后,它占有的内存空间就会被释放,那么指向局部变量的引用和指针将会指向不再有效的区域。因此,不要返回局部对象的引用或指针,注意是局部对象。
const string & fun(){
string s("abc");
if(!s.empty())
return s;}//错误,返回一个局部对象的引用
else
return "Empty";//错误,返回一个临时局部变量
}
6.3、返回数组指针
数组不能直接拷贝,因此不能返回数组,但是,函数可以返回数组的指针或引用。有一些方法可以实现:
(1)类型别名
typedef int arr[10];//定义arr是含有10整数的数组
using arr=int[10];//等价声明
arr* fun(int);//函数返回一个指向10个整数的数组的指针
(2)不使用类型别名
如果不使用类型别名,则数组的维度必须跟在名字之后,但是形参列表也要在名字之后,所以维度就在形参列表之后:
type(*function(parameter_list))[dimension]
例:int(*fun(int i))[10];//就像int (*p)[10]在定义指向数组的指针一样
(3)使用尾置返回类型
C++11新标准后的方法,尾置返回类型跟在形参列表后面并以一个->符号开头,在本该出现返回类型的地方放置了一个auto:
auto fun(int i)->int(*)[10];
(4)使用decltype
如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。
int odd[]={1,3,5};
int even[]={0,2,4};
decltype(odd)*fun(int i){//由于decltype不会自动将数组转换成指针,因此需要加上*
return(i%2)?&odd:&even;//返回一个指向数组的指针,注意&
}
7、函数重载
如果同一作用域的几个函数的函数名相同,形参类型和形参数量不同,我们称它们为重载函数,如果有2个函数的形参列表一样但返回值不同,则第二个函数是错误的。mian函数不能重载。
调用重载函数的时候可能出现三种结果:
- 编译器找到一个与实参最佳匹配的函数,并声生成调用该函数的代码。
- 找不到任何一个与实参匹配的函数,编译器发出无匹配的错误信息。
- 有多于一个函数可以匹配但每一个不是明显的最选择,此时也将发生错误,称为二义性调用。
7.1、重载和const形参
(1)一般const
一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:
int fun(int);
int fun(const int);//重复声明了函数
因为这样的形参只是实参的一个拷贝,是否是const对原来的实参没有影响。
(2)形参是指针或引用
如果形参是某种类型的指针或引用,则可以通过区分其指向的是常量对象还是非常量对象来实现函数的重载:
int fun(int&);
int fun(const int&);//新函数
对于const类型的实参只能调用第二个函数,而非const类型的实参是二者都可以调用的,但根据最佳匹配原则的话会优先匹配第一个,因为匹配第二个要进行类型转换。
7.2、重载和作用域
看下面的例子:
string read();
void print(const string&);
void print(double);
void fun(int ival){
bool read=false;//新的read隐藏了外部的read()函数
string s=read();//错误,read是一个bool类型
void print(int);//声明了新的print函数,隐藏了外部的所有print函数
print("Value:");//错误,print(const string&)被隐藏了,没有匹配的函数
print(ival);//正确,调用新的print(int)函数
print(3.14);//正确,调用print(int),而非print(double),进行了强制类型转换
}
在C++中名字查找发生在类型检查之前,如果在当前作用域中找到了所需的名字,编译器就会忽略外层作用域中的同名实体。
7.3、函数匹配
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数,候选函数具备2个特征:一是与被调用的函数名相同,二是其声明在调用可见。
第二步是从候选函数中选出能被调用的函数,被选出来的函数称为可行函数,可行函数具备2个特征:一是其形参与实参的数量一致,而是其形参类型与实参的类型相同或能转换成形参的类型。
第三步是从可行函数中选出与本次调用最匹配的函数,形参与实参的类型越接近越匹配,如果没有找到最匹配的函数就会发生无匹配或二义性错误。
7.3.1、最佳匹配
8、默认实参
可以为函数的形参提供默认的值,称这样的值为默认实参,调用含有默认实参的函数时,可以提供该新的实参值,也可以省略实参值而使用默认实参。一旦某个形参被赋予了默认值,那它后面的所有形参都必须要有默认值。因此在调用含有默认实参的函数时只能省略尾部的实参。
(1)默认实参声明
一般来说,函数的声明通常放在头文件中,并且一个函数只声明一次,但多次声明同一个函数也是合法的,但后续声明只能为之前没有默认值的形参添加默认值,并且,该形参右边的所有形参都必须要有默认值。
string fun(int ,int ,char='');
string fun(int,int,char='*');//错误,重复声明
string fun(int=0,int=1,char);//正确
(2)默认实参的解析和求值
用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时:
int a=0;
int b=1;
char c='c';
string fun(int=a,int=b,char=c);//函数声明
void fun1(){
c='d';//改变了默认实参的值
int a=1;//隐藏了外层的a,但没有改变默认值
string s=fun();//调用fun(0,1,'d');
}
这个默认实参指的是什么哪个变量就用哪个变量,不会改变,但是这个变量的值是可能改变的。
9、内联函数
函数调用包含着一系列的工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。将函数指定为内联函数可以避免函数调用的开销,通常将它在调用点内联地展开,声明方法只要在函数返回值类型前面加上inline关键字就行了。内联机制用于优化规模较小、流程直接、频繁调用的函数。
10、函数指针
(1)函数类型
函数指针指向的是函数,函数的类型由函数的形参列表和返回值类型决定,与函数名无关。
bool fun(const string&,const string&)的类型是bool(const string&,const string&)
(2)函数指针的声明
要想声明一个函数指针,用指针替换函数名即可:
bool(*pf)fun(const string&,const string&);
(3)函数指针的使用
(3.1)指针赋值
当我们把函数名作为一个值使用时,该函数自动转换成指针:
pf=fun;//pf指向名为fun的函数
pf=&fun;//等价的赋值语句,&是可选的
如果函数是重载函数,根据函数指针的类型精确匹配来确定指向哪个函数。
也可以为指针赋值nullptr或值为0的整型常量表达式,表示该指针没有指向任何一个函数。
(3.2)指针调用
可以直接使用函数指针来调用函数,无须解引用:
bool b1=pf("hello","world");//调用函数
bool b2=(*pf)("hello","world");//等价调用
bool b3=fun("hello","world");//另一个等价调用
(4)函数指针作为形参
与数组类似,虽然不能定义函数类型的形参,但形参类型可以是指向函数的指针,如果直接使用函数或函数名作为形参,那么它们都会被自动转换成函数指针:
void fun1(int ,int ,bool fun(const string&,const string&));//函数fun自动转换成函数指针
void fun1(int ,int,bool(*pf)(const string&,const string&));//pf是函数指针
void fun1(int ,int,fun);//自动转换成函数指针
(5)返回函数指针
与函数指针的形参不同,编译器不会自动的将函数类型的返回值当场对应的指针处理,必须显式地将返回类型定为指针:
using ff=bool(int,int);//ff是函数类型
using FF=bool(*)(int,int);//FF是函数指针
函数返回:
FF f1(int);//正确,返回函数指针
ff f1(int);//错误,ff是函数类型,不能返回函数
ff* f1(int);//正确
当然也可以用尾置返回类型的方式和auto和declype,与返回数组的方式类似。