函数就是命了名的代码块。
函数基础
通过调用运算符来执行函数,调用运算符的形式是一对圆括号,作用于一个表达式,该表达式是函数或函数的指针,圆括号内是逗号隔开的实参列表,执行时首先用实参初始化形参。
实参是形参的初始值,第一个实参对应第一个实参,依次类推,但是并没有规定实参的求职顺序。
函数的形参列表可以为空,但是不能省略,为了兼容c,可以在括号中填入void表示没有形参。任意两个形参都不能同名,并且函数最外层作用域的局部变量也不能使用与函数形参一样的名字。
函数的返回类型不能是数组类型或函数类型。但可以是指向数组或函数的指针。
在c++语言中,名字有作用域,对象有生命周期。形参和函数体内定义的变量统称为局部变量。同时局部变量会隐藏外层作用域同名的所有声明。在所有函数体外定义的对象存在于程序运行的整个生命周期。
我们把只存在于块执行期间的对象称为自动对象,当块执行结束时他们自动销毁。形参是一种自动对象,当函数开始时为形参分配存储空间,函数结束时形参被销毁。
局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止销毁,生命周期跨越函数的多次调用。内置类型的局部静态变量初始化为0。
函数在使用之前必须声明,函数可以生命多次,但只能定义一次。函数声明无需函数体,用分号代替。函数的声明可以省略形参的名字,但是写上形参的名字可读性更好。函数三要素(返回类型,函数名,形参列表)描述了函数的接口,函数声明也叫函数原型。在头文件中进行函数声明,函数声明的头文件应包含到函数定义的源文件中。变量的声明使用关键字extern并且不要显示的初始化,变量只能定义一次但可以声明多次。
参数传递
每次函数调用都会创建形参并用实参进行初始化。如果形参是引用类型,它将绑定到对应的实参上,否则把实参的值拷贝后赋值给形参。如果形参是引用类型,我们说实参被引用传递或函数被传引用调用。引用形参是对应实参的别名。当实参的值被拷贝给形参时,形参和实参是两个相对独立的对象,这样的实参被值传递或函数被传值调用。
传值参数时,如果修改形参则实参的值不会改变,传递的指针也是如此,不过可以通过指针修改被指向的对象。
为了避免拷贝可以使用引用形参,如果无需更改引用实参的值,可以把形参声明为常量。通过引用形参返回额外信息。
可以向const形参传递非const变量,这样在函数中将不能更改const形参。
如果在函数内不需要修改参数,则建议使用常量引用。可以用非常量的实参传递给常量引用,但是不能将常量实参传递给非常量引用。
不允许拷贝数组以及使用数组时会将数组转换成指针。以数组作为形参的函数一定要保证使用数组时不会越界。通过三种方式来保证访问数组的安全性:使用特定标记指定数组长度,当遇到指定标记时任务数组已经结束;使用标准库规范传递数组的首元素指针和尾后元素指针;显示传递一个表示数组大小的形参。
如果函数不需要对数组执行写操作,把形参定义成常量指针。也可以把形参定义成数组的引用int (&arr)[100]。传递多维数组时实际上传递的是指向数组的指针。
通过initializer_list传递可变参数,该类型定义在同名的头文件中,用来定义包含相同类型元素的列表。initializer_list中的元素永远是常量值,和vector一样通过begin和end遍历列表中的元素。调用时只需在大括号中指定元素值即可。如:
void fun(std::initializer_list<string> params) { } fun({ "one", "tow" });
省略符形参应只应用于C和C++通用的类型,省略符形参只能出现在形参列表的最后一个位置。在C++中不推荐使用省略符形参。
返回类型和return语句
没有返回值的return语句只能用于返回void的函数中,返回void的函数可以不写return语句,在函数的最后会隐式执行return。返回void的函数可以使用return返回一个返回void的函数调用。
有返回值的函数必须使用return语句返回一个有效值,编译器可以检查return语句的正确性,但是可能无法检查没有写return语句的情况,如果没有写return语句并且编译器没有检查运行时将是未定义的,这包括某些执行路径没有执行return语句的情况。
函数返回值与初始化变量和形参的方式完全一样,如果函数返回引用,该引用紧是所引用对象的别名。不要返回局部对象的引用或指针,函数执行完成后,局部对象将全部释放。
函数的返回类型决定函数调用是否是左值,调用一个返回引用的函数可以得到左值,其他为右值。
C++11新标准规定,函数可以返回花括号包围的值列表,值列表用于初始化返回的列表类型,如果函数返回内置类型,则花括号中最多包含一个值。
main函数可以不包含返回语句,编译器自动在最后返回0,返回0表示成功。
如果一个函数调用了它自身,不管是直接调用还是间接调用,都称该函数为递归函数,递归函数一定有某条路径不包含递归调用。
因为数组不能被拷贝,所以函数不能返回数组,但是函数可以返回数组指针,声明一个返回数组指针的函数比较复杂和难于理解。如以下函数表示返回整型数组的指针:
int (*func(int i))[10] { }
函数重载
同一作用域内的多个函数名字相同但参数列表不同称为函数重载。不能定义只有返回类型不同,其他都相同的函数。形参的名字可以省略,拥有顶层const的形参无法与没有顶层const的形参区分开来。
不要滥用函数重载,只有操作确实一样时才使用函数重载。const_cast可以将非常量转换成常量。
当调用重载的函数时,编译器可能会发出无匹配或调用二义性的错误。
在不同的作用域中无法重载函数名,内层作用域将隐藏外层作用域的名字。
特殊用途语言特性
调用含有默认实参的函数时,可以传递该实参也可以省略该实参。
默认实参作为形参的初始值出现在形参列表中,我们可以为多个形参定义默认值,但是一旦为某个形参定义默认值后,其后的参数都要指定默认值。
通常在函数声明中指定默认实参,并将该声明放在指定的头文件中。默认实参只能指定一次,如果有多个函数声明,在一个声明中指定了默认实参在另一个声明中就不能在指定。局部变量不能作为默认实参,但其他合法的表达式可以。作为默认实参的表达式在函数调用时求值,在函数声明时解析。
内联函数可避免调用函数的开销,在返回类型前面加上关键字可以把函数声明为内联函数,内联声明只是向编译器发出请求,编译器可能会忽略。
constexpr函数是指能用于常量表达式的函数,constexpr函数隐式为内联函数,根据参数的不同可能返回非常量。
内联函数和constexpr函数可以在程序中多次定义,但是它的多次定义必须完全一致,通常定义在头文件中。
可以使用assert预处理宏和NDEBUG预处理变量帮助调试程序,默认NDEBUG未定义,可以在编译器参数中指定NDEBUG定义。
重载函数的匹配过程有可能很复杂,编译器寻找最佳匹配过程分为多个步骤。调用重载函数时尽量避免强制类型转换。
函数指针
函数指针指向的是函数而非对象,要想声明一个可以指向函数的指针,只需用指针替换函数名即可,如bool (*fp)(const string &,const string &)//未初始化,fp指向一个这种类型的函数,注意fp两端的括号不能少。
当把函数名作为一个值使用时,函数名自动转换为函数指针,可以直接赋值给函数指针,并且无需解引用就可以通过函数指针调用函数。指向不同类型的函数指针不存在转换规则。
当使用重载函数的指针时,指针类型必须与重载的函数精确匹配才能赋值。
无法定义函数类型的形参,但是可以定义指向函数的指针的形参,当形参声明为一个函数时,将自动转换为指向函数的指针。直接把函数当做实参使用,会自动转换为指针。
通过typedef和decltype简化函数指针的类型声明,但是注意decltype返回的是函数不会自动转换为指针。
函数可以返回函数指针,通过使用尾置返回类型或auto或decltype关键字简化返回函数指针的函数声明。