典型的函数包括:返回类型、函数名字、由 0 个或多个形参组成的列表以及函数体。
通过调用运算符(call operator)来执行函数。
调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是一个用逗号隔开的实参(argument)列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。
函数的调用会完成两项工作:
1.用实参初始化函数对应的形参;
2.将控制器转移给被调用函数,主调函数的执行被暂时中断,被调函数开始执行。
执行函数的第一步是(隐式地)定义并初始化它的形参。
return 语句完成两项工作:
1.返回 return 语句中的值(如果有的话)
2.将控制权从被调函数转移回主调函数。
实参是形参的初始值。
函数的形参列表可以为空,但是不能省略。
返回类型不能是数组或函数类型,但是可以是数组指针或函数指针。
名字有作用域,对象有生命周期。
1.名字的作用域是程序文本的一部分,名字在其中可见。
2.对象的生命周期是程序执行过程中该对象存在的一段时间。
函数体是一个语句块。块构成一个新的作用域,我们可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量(local variable)。它们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。
局部变量的生命周期依赖于定义的方式:
1.自动对象(只存在于块执行期间的对象):当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
内置类型的未初始化局部变量将产生未定义的值。
2.局部静态对象(static 型):局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
函数的名字也必须在使用之前声明。
函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。
函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型(function prototype)。
含有函数声明的头文件应该被包含到定义函数的源文件中。
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
形参的类型决定了形参和实参交互的方式:如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
指针形参:当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。
引用形参在传递实参时直接传入对象。
如果函数无须改变引用形参的值,最好将其声明为常量引用。
当用实参初始化形参时会忽略掉顶层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 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)。
调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
局部变量不能作为默认实参。
内联函数可避免函数调用的开销,通常就是将它在每个调用点上“内联地”展开。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);//正确:显式地指定返回类型是指向函数的指针