一个典型的函数定义包括以下部分:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。
函数的调用完成2项工作:
(1)用实参初始化函数对应的形参
(2)将控制权转移给被调用函数,此时主调函数的执行被暂时中断,被调函数开始执行
为了与C语言兼容,对于没有形参的函数,在其参数列表中添加void关键字,如:
void fun(void);
函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
函数声明也称作函数原型。
自动对象和局部静态对象
(1)对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
(2)有些时候需要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
函数参数传递
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
如果形参是引用类型,它将绑定到对应的实参上,否则将实参的值拷贝后赋给形参。
C程序员常常使用指针类型的形参访问函数外部的对象,在C++中建议使用引用类型的形参替代指针。
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型)不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
当函数无需修改引用形参的值时,最好使用常量引用。
和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。即形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。
因为数组是以指针形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有3种常用的技术:
(1)使用标记指定数组长度
(2)使用标准库规范,即传递指向数组首元素和尾后元素的指针
(3)显式传递一个表示数组大小的形参,在C语言和过去的C++中常常使用这种方法
新的C++允许将变量定义成数组引用,因此可以将形参设置为数组引用。因为数组的大小是数组类型的一部分,这样只要不超过维度,在函数体内可放心使用数组。但这在无形中也限制了函数只能传递某个尺寸的数组。
可变形参的函数
为了编写能处理不同数量实参的函数,C++ 11提供了3种方法:
(1)如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型
(2)如果实参的类型不同,可以编写一组特殊的函数,就是所谓的可变参数模板
(3)还可以使用特殊的形参类型(即省略符),用它传递可变数量的实参,但该功能一般只用于与C函数交互的接口程序
返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
不要返回局部对象的引用或指针。
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。
C++ 11规定,函数可以返回{}包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化,否则返回的值由函数的返回类型决定。
vector<string> process()
{
if (expected.empty())
{
return {};
}
else if (expected == actual)
{
return {"functionX", "OK"};
}
else
{
return {"functionX", expected, actural};
}
}
C++允许main函数没有return语句直接结束。如果控制到达了main函数的结尾处而没有return语句,编译器将隐式地插入一条返回0的return语句。
main函数的返回值可以看作是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。
返回数组指针
如果想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字后面。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。如:
Type (*function(parameter_list))[dim]
在C++ 11中还有一种可简化上述声明的方法,即使用尾置返回类型。任何函数的定义都能使用尾置返回。如:
auto func(int i) -> int(*)[10];
还可以使用decltype关键字声明返回类型,如:
int odd[] = {1, 3, 5, 7, 9};
decltype(odd) *arrPtr(int i);
需要注意的是decltype只返回基本类型,不返回指针。
函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
对于重载函数来说,它们应该在形参数量或形参类型上有所不同。不允许两个函数除了返回类型外其他所有的要素都相同。
一个拥有顶层const的形参无法和另一个没有顶层const形参的函数区分,如:
// 无法区分
Record lookup(Phone);
Record lookup(const Phone);
而如果形参是某种类型的指针或引用,则可以通过是否是const类型,来区分函数实现重载,如:
// 可以区分
Record lookup(Account&);
Record lookup(const Account&);
Record lookup(Account*);
Record lookup(const Account*);
如果有多于一个函数具有相同的调用匹配程度,但是每一个都不是明显的最佳选择,此时将发生错误,称为二义性调用。
默认实参
一旦某个形参被赋予默认值,它后面的所有形参都必须有默认值。
通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
内联函数和constexpr函数
将函数指定为内联函数,通常就是将它在每个调用点上内联地展开。
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。
constexpr函数是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循以下约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。
为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
内联函数和constexpr函数通常定义在头文件内。
调试帮助
assert是一种预处理宏,它的行为类似于内联函数。使用方式为:
assert(expr);
首先对expr求值,如果其值为假,assert输出信息并终止程序的执行。如果表达式为真,assert什么也不做。
由于预处理名字由预处理器而非编译器管理,我们应该使用assert而不是std::assert。
assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
我们可以在文件开头使用一个#define语句定义NDEBUG,从而关闭调试状态。
除了用于assert外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef和#endif之间的代码。如果定义了NDEBUG,则这些代码将被忽略:
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
cerr << __func__<<": array size is "<< size << endl;
#endif
...
函数指针
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定的类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
要想声明一个可以指向函数的指针,只需用指针替换函数名即可。
还可直接使用指向函数的指针调用函数,而无需解引用指针,如:
bool lengthCompare(const string&, const string&);
pf = lengthCompare;
// 下面3者相同
bool b1 = pf("hello", "goodbye");
bool b2 = (*pf)("hello", "goodbye");
bool b3 = lengthCompare("hello", "goodbye");
在指向不同函数类型的指针间不存在转换规则。
将函数指针用作形参,此时形参看起来是函数类型,实际上却是当成指针使用,如:
void useBigger(const string &s1, const string &s2, bool pf(const string&, const string&));
虽然不能返回一个函数,但能返回指向函数类型的指针。此时可以使用using方法使用类型别名:
using PF = int(*)(int*, int);
也可以直接声明返回函数指针的函数,如:
int (*f1(int))(int*, int);
还可以使用尾置返回类型的方式,如:
auto f1(int) -> int(*)(int*, int);