函数基础
典型的函数包括:返回类型、函数名字、由 0 个或多个形参组成的列表以及函数体。
通过调用运算符(call operator)来执行函数。
调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是一个用逗号隔开的实参(argument)列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。
函数的调用会完成两项工作:
1.用实参初始化函数对应的形参;
2.将控制器转移给被调用函数,主调函数的执行被暂时中断,被调函数开始执行。
执行函数的第一步是(隐式地)定义并初始化它的形参。
return 语句完成两项工作:
1.返回 return 语句中的值(如果有的话)
2.将控制权从被调函数转移回主调函数。
实参是形参的初始值。
函数的形参列表可以为空,但是不能省略。
返回类型不能是数组或函数类型,但是可以是数组指针或函数指针。
局部对象
名字有作用域,对象有生命周期。
1.名字的作用域是程序文本的一部分,名字在其中可见。
2.对象的生命周期是程序执行过程中该对象存在的一段时间。
函数体是一个语句块。块构成一个新的作用域,我们可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量(local variable)。它们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。
局部变量的生命周期依赖于定义的方式:
1.自动对象(只存在于块执行期间的对象):当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
内置类型的未初始化局部变量将产生未定义的值。
2.局部静态对象(static 型):局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
函数声明
函数的名字也必须在使用之前声明。
函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。
函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型(function prototype)。
含有函数声明的头文件应该被包含到定义函数的源文件中。
参数传递
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
形参的类型决定了形参和实参交互的方式:如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
指针形参:当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。
引用形参在传递实参时直接传入对象。
如果函数无须改变引用形参的值,最好将其声明为常量引用。
const 形参和实参
当用实参初始化形参时会忽略掉顶层const,即形参类型为 const int 和 const 是一样的。
const int ci = 42;//不能改变ci,const是顶层的
int i = ci;//正确:当拷贝ci时,忽略了它的顶层const
int * const p = &i;//const是顶层的,不能给p赋值
*p= 0;//正确:通过p改变对象的内容是允许的,现在i变成了0
尽量使用常量引用
数组形参
三种等价声明:
void print(const int*);
void print(const int[]);
void print(const int[10]);
管理指针形参有三种常用的技术:
1.使用标记指定数组长度;(例如 cout 会输出到字符串的空字符停止)
2.使用标准库规范;(例如使用while循环判断)
3.显式传递一个表示数组大小的形参。
形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上:
//正确:形参是数组的引用,维度是类型的一部分
void print(int (&arr)[10]){
for (auto elem: arr)
cout << elem <<endl;
}
//&arr两端的括号必不可少:
f(int &arr[10])//错误:将arr声明成了引用的数组
f(int (&arr)[10])//正确:arr是具有10个整数的整型数组的引用
可以给 main 函数传递实参
int main (int argc,char *argv[]){
}
int main (int argc, char **argv){
}
含有可变形参的函数
有时无法提前预知应该向函数传递几个实参。
编写能处理不同数量实参的函数:
1.如果所有的实参类型相同,可以传递一个名为 initializer_list 的标准库类型;
2.如果实参的类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板。
initializer_list 提供的操作:
省略符形参
省略符形参只能出现在形参列表的最后一个位置.
void foo(parm_list, ...);
void foo (...);
返回类型和 return 语句
return;//终止当前正在执行的函数,将控制权返回到调用该函数的地方
return expression;//返回 return 语句中的值
无返回值函数
没有返回值的 return 语句只能用在返回类型是 void 的函数中。返回 void 的函数最后一句后面会隐式地执行 return。
有返回值函数
只要函数地返回类型不是 void ,则该函数内地每条 return 语句必须返回一个值。return 语句返回值地类型必须与函数地返回类型相同,或者能隐式地转换成函数地返回类型。
返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
不要返回局部对象的引用或指针:函数终止意味着局部变量的引用将指向不再有效的内存区域。
函数的返回类型决定函数调用是否是左值:调用一个返回引用的函数得到左值,其他返回类型得到右值。
char &get_val(string &str,string:: size_type ix){
return str[ix] ;//get_val假定索引值是有效的
}
int main(){
string s("a value");
get_val(s,0)='A';//将 s[0]的值改为A
cout<<s<<endl;//输出 A value
return 0;
}
允许 main 函数没有 return 语句直接结束,编译器隐式地插入一条 return 0;
递归: 函数调用它本身,不管是直接还是间接。
//计算val 的阶乘,即1*2* 3 ...* val
int factorial (int val){
if (val >1)
return factorial (val-1) *val;
return 1;
}
main函数不能调用它自己。
返回数组指针
int arr[10];// arr是一个含有10个整数的数组
int *p1[10];// p1是一个含有10个指针的数组
int (*p2)[10] = &arr;// p2是一个指针,它指向含有10个整数的数组
返回数组指针的函数形式:Type (* function (parameter_list) )[dimension]
func(int i);//表示调用func函数时需要一个int类型的实参。
(*func(int i));//意味着可以对函数调用的结果执行解引用操作。
(*func(int i))[10];//表示解引用func的调用将得到一个大小是10的数组。
int(*func(int i))[10];//表示数组中的元素是int类型。
任何函数的定义都能使用尾置返回。尾置返回类型跟在形参列表后面并以一个->
符号开头。
//func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i)-> int(*)[10];
**decltype:**如果知道函数返回的指针将指向哪个数组,就可以使用 decltype 关键字声明返回类型。
int odd[] ={1,3,5,7.9};
int even [] = {0,2,4,6,8};
//返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i){
return(i % 2)? &odd : &even;//返回一个指向数组的指针
}
要返回数组指针必须在函数声明时要加一个 *
函数重载
main函数不能重载。
调用重载函数时应尽量避免强制类型转换。
函数匹配
函数匹配:指在一个过程,把函数调用与一组重载函数中的某一个关联起来。函数匹配也叫做重载确定。
编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。
候选函数的两个特征:一是与被调用的函数同名,二是其声明在调用点可见。
第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。
可行函数的两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
调用重载函数时地三种可能结果:
1.编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
2.找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息。
3.有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用(ambiguous call)。
特殊用途语言特性
默认实参
调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
局部变量不能作为默认实参。
内联函数和 constexpr 函数
内联函数可避免函数调用的开销,通常就是将它在每个调用点上“内联地”展开。inline 内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
内联机制用于优化规模较小、流程直接、频繁调用的函数。
constexpr 函数指能用于常量表达式的函数。constexpr函数被隐式地指定为内联函数。
定义 constexpr 函数需要:1.函数的返回类型及所有形参的类型都是字面值类型 ;2.函数体中必须有且只有一条 return 语句。
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();//正确: foo是一个常量表达式
constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr函数中可以有空语句、类型别名以及using声明。
constexpr函数不一定返回常量表达式。
应该把内联函数和constexpr函数的定义放到头文件里。
调试帮助
assert 预处理宏使用一个表达式作为它的条件:assert(expr);
首先对expr求值,如果表达式为假(即 0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。assert宏常用于检查“不能发生”的条件。
assert宏定义在cassert头文件中。和预处理变量一样,宏名字在程序内必须唯一。
NDEBUG 预处理变量
assert的行为依赖于一个名为NDEBUG的预处理变量的状态:如果定义了 NDEBUG,则 assert 什么也不做,默认状态下没有定义 NDEBUG。
预处理器定义的对于程序调试很有用的名字:
__func__;//局部静态变量,用于当前函数名的字符串字面值
__FILE__;//当前文件名的字符串字面值
__LINE__;//当前行号的整型字面值
__TIME__;//文件编译时间的字符串字面值
__DATE__;//文件编译日期的字符串字面值
函数指针
函数指针指向的是函数而非对象。
函数指针指向某种特定类型。
// pf指向一个函数,该函数的参数是两个const string的引用,返回值是boo1类型
bool (*pf)(const string &,const string &);//未初始化
//*pf两端的括号必不可少。如果不写这对括号,则pf是一个返回值为bool指针的函数:
//声明一个名为pf的函数,该函教返回bool*
bool *pf (const string &, const string &);
把函数名作为一个值使用时,该函数自动地转换成指针:
pf = lengthcompare;//pf指向名为 lengthcompare 的函数
pf =& lengthCompare;//等价的赋值语句:取地址符是可选的
返回指向函数类型的指针
使用类型别名:
using F=int(int*, int);//F是函数类型,不是指针
using PF = int(*) (int*, int);//PF 是指针类型
显式指定:
PF f1 (int);//正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int);//错误:F是函数类型,f1不能返回一个函数
F *f1 (int);//正确:显式地指定返回类型是指向函数的指针
重要术语
- 二义性调用(ambiguous call) 是一种编译时发生的错误,造成二义性调用的原因是在函数匹配时两个或多个函数提供的匹配一样好,编译器找不到唯一的最佳匹配。
- 自动对象(automatic object) 仅存在于函数执行过程中的对象。当程序的控制流经过此类对象的定义语句时,创建该对象:当到达了定义所在的块的末尾时,销毁该对象。
- 最佳匹配(best match) 从一组重载函数中为调用选出的一个函数。如果存在最佳匹配,则选出的函数与其他所有可行函数相比,至少在一个实参上是更优的匹配,同时在其他实参的匹配上不会更差。
- 函数匹配(function matching) 编译器解析重载函数调用的过程,在此过程中,实参与每个重载函数的形参列表逐一比较。
- 对象生命周期(object lifetime) 每个对象都有相应的生命周期。块内定义的非静态对象的生命周期从它的定义开始,到定义所在的块末尾为止。程序启动后创建全局对象,程序控制流经过局部静态对象的定义时创建该局部静态对象;当main函数结束时销毁全局对象和局部静态对象。