第 6 章 函数
6.1 函数基础
我们可以通过调用运算符 call operator
来执行函数。
形参 parameter
实参 argument
主调函数 calling function
被调函数 called function
空形参列表可以是空的括号,或是 void
void f1() {}
void f2(void) {}
6.1.1 局部对象
C++ 中名字有作用域,对象有声明周期 lifetime
。
名字的作用域是程序文本的一部分,名字在其中可见。
对象生命周期是程序执行过程中该对象存在的一段时间。
形参和函数体内部定义的变量是局部变量 local variable
,它们仅在函数作用域中可见,同时会隐藏 hide
在外层作用域中的同名的其他声明。
自动对象
只存在于块执行期间的对象称为自动对象 automatic object
。
形参也是自动对象。
局部静态对象
local static object
有些对象的生命周期需要贯穿函数调用及之后的时间。
局部静态变量如果没有显示的初始值,则它会被默认初始化。
6.1.2 函数声明
函数声明也称作函数原型 function prototype
。
函数声明不需要函数体用分号代替。函数声明可以没有形参名字。
6.2 参数传递
形参是引用类型时,实参被引用传递 passed by reference
或函数被传引用调用 called by reference
。
实参值被拷贝给形参时,实参被值传递 passed by value
或者函数被传值调用 called by value
。
6.2.3 const 形参和实参
注意实参初始化形参时会忽略掉顶层 const,因此这时需要注意下列重载是不行的。
void f(const int i);
void f(int i);
6.2.4 数组形参
注意因为数组不能拷贝,因此我们不能以值传递的方式使用数组参数,因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但我们可以把参数形式写成类似数组的形式,下面三个形式是等价的。
void f(const int*);
void f(const int[]);
void f(const int[10]); // 注意这个 10 只是期望大小,实际并不一定
当我期待更改数组内容时使用下面的形式
void f(int*);
数组引用形参
void f(int (&arr)[10]);
但注意,这种写法数组的维度是被限制死的。
传递多维数组
void f(int (*matrix)[10], int row);
void f(int matrix[][10], int row);
6.2.5 main:处理命令行选项
int main(int argc, char *argv[]) {}
其中 argc 是程序接受到的参数个数
argv[0]
是程序名字或空字符串。
6.2.6 含有可变形参的函数
对于不定数量的参数,参数类型相同可以使用 initializer_list ,否则可以使用变长参数模板。
initializer_list<T> lst;
initializer_list<T> lst{a, b, c};
lst2(lst)
lst2 = lst
lst.size();
lst.begin();
lst.end();
注意 initializer_list 中的元素是常量值。
省略符形参
void foo(parm_list, ...);
void foo(...);
这是为了 C++ 代码便于访问 C 代码设置的,这些代码使用了 varargs 的 C 标准库的功能。
6.3 返回类型和 return 语句
6.3.1 无返回值的函数
返回类型的是 void 的函数也可以使用带有表达式的 return 的语句,这时表达式必须是另一个返回 void 的函数。
void f1() {}
void f2() {
return f1();
}
int main() {
f2();
return 0;
}
6.3.2 有返回值函数
值是如何被返回的
返回一个值的方式和初始化变量或形参的方式一致,返回值用来初始化调用点的一个临时量,这个临时量就是函数调用的结果。
如果函数返回值是引用,则它只是所引用对象的别名。
不要返回局部对象的引用或指针!
引用返回左值
如果返回类型时非常量引用,则我们可以对其进行赋值。
列表初始化返回值
可以使用列表初始化初始化返回值,与其他情况使用列表初始化相同。
主函数 main 的返回值
为了使返回值与机器无关 cstdlib 中定义了
EXIT_FAILURE;
EXIT_SUCCESS;
6.3.3 返回数组指针
typedef int arrT[10];
// 或
using arrT = int[10];
arrT* func(int i);
上述写法会好写一点
也可以用下面的写法
int (*func(int i))[10];
使用尾置返回类型
尾置返回类型 trailing return type
auto func(int i) -> int(*)[10];
使用 decltype
当我们知道函数返回的指针指向哪个数组时可以使用
int odd[] = {1, 3, 5};
int even[] = {2, 4, 6};
decltype(odd) *arrPtr(int i) {
return (i % 2) ? &odd : &even;
}
6.4 函数重载
同一作用域内几个函数名字相同但形参列表不同,我们称之为重载 overloaded
函数。
main 函数不能重载。
注意有时候形参看起来不一样,但实际是相同的。
typedef A B;
void f(A a);
void f(B b);
注意不能重载参数只有顶层 const 的不同的函数
void f(A a);
void f(const A a);
void f(P*);
void f(P* const);
如下是正确的重载
void f(A&);
void f(const A&);
void f(A*);
void f(const A*);
编译器将通过实参是否是常量来判断调用哪个函数。
const_cast 和重载
const string &shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
string &shorterString(string &s1, string &s2) {
auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}
调用重载函数
函数匹配 function matching
也称为冲在确定 overload resolution
是将函数调用与一组重载函数中的某个关联起来的过程。
调用时存在三种可能:
找到最佳匹配 best match
。
找不到匹配,发出无匹配no match
错误信息。
有多于一个匹配,并且没有明显的最佳匹配。此时返回错误信息二义性调用 ambiguous call
。
6.4.1 重载与作用域
一般来说不会将函数声明置于局部作用域内。
注意内层声明会屏蔽外层所有同名的声明,不存在内层函数对外层重载。
C++ 中名字查找在类型检查之前。
6.5 特殊用途语言特性
6.5.1 默认实参
default argument
默认实参声明
注意多次声明同一函数是合法的,但在某给定作用域中,形参只能被赋予一次默认实参。
void f(int a,int b,int c=10);
// void f(int a,int b,int c=100); // 错误
void f(int a=10,int b=0,int); // 正确
注意局部变量不能作为默认实参,表达式能转换成形参类型则也可以作为默认实参。
6.5.2 内联函数和 constexpr 函数
constexpr 函数
是能用于常量表达式的函数,函数的返回类型和所有形参必须是字面值类型,函数体中必须有且只有一条 return 语句。
constexpr 会在编译时隐式指定为内联函数。
constexpr 函数不一定返回常量表达式。
当 constexpr 函数的参数不是常量时,则它不是一个常量表达式。
内联函数和 constexpr 函数,可以在程序中多次定义,因为只有声明是无法内联展开的,但这些定义应当完全一致,所以通常把内联函数和 constexpr 函数放在头文件。
6.5.3 调试帮助
assert 是一个定义在 cassert 中的预处理宏。
当定义了 NDEBUG
预处理变量时,cassert 什么都不会做,否则会检查表达式是否为真,如果为假则输出信息并终止程序执行。
可以使用
__func__ // 输出当前函数名字
__FILE__ // 当前文件名
__LINE__ // 当前行号
__TIME__ // 文件编译时间
__DATE__ // 文件编译日期
6.6 函数匹配
函数匹配的第一步是确定候选函数 candidate function
,候选函数满足与被调函数同名,声明在调用点可见。
第二步考察实参,从候选函数中选出可行函数 viable function
,可行函数满足形参数量与实参数量相等,实参类型与形参类型相同,或能转换成形参的类型。
第三步是寻找最佳匹配,当有一个函数满足,每个实参匹配都不劣于其他可行函数的匹配,有至少一个实参匹配优于其他可行函数的匹配,则它是最优匹配。
实参到形参类型转化的级别:
- 精确匹配:实参和形参类型相同,实参从数组类型或函数类型转换成对应的指针类型,向实参添加顶层 const 或删除顶层 const
- 通过 const 转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术类型转换或指针转换实现的匹配
- 通过类类型转换实现的匹配
6.7 函数指针
函数指针指向某种特定类型。
函数类型由返回值和形参类型共同决定。
不同类型的函数指针之间不能转换。
可以将 nullptr 或 0 赋值给函数指针。
以下形式赋值函数指针是相同的。
void f();
void (*fp)() = f;
void (*fp)() = &f;
使用函数指针和使用函数名是类似的。
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是可以定义指向函数的指针。
void f(int a,int b,bool pf(int c,int d));
void f(int a,int b,bool (*pf)(int c,int d));
也可以使用 decltype 得到函数类型,但是这时需要注意 decltype 得到的函数类型不会自动转换成指针类型。不过使用上看起来好像没什么问题,如下 4 种写法都是可以通过编译并运行的。
#include <iostream>
void fvoid() { std::cout << "do something\n"; }
void (*pf)() = fvoid;
typedef void Func();
typedef decltype(fvoid) Func2;
typedef void (*FuncP)();
typedef decltype(fvoid) *FuncP2;
void f1(Func f) { f(); }
void f2(Func2 f) { f(); }
void f3(FuncP f) { f(); }
void f4(FuncP2 f) { f(); }
int main() {
f1(fvoid);
f2(fvoid);
f3(fvoid);
f4(fvoid);
f1(pf);
f2(pf);
f3(pf);
f4(pf);
return 0;
}
返回指向函数的指针
这里最好还是使用类型别名或者尾置返回类型。
#include <iostream>
using F = void(void);
using PF = void(*)(void);
void f(){}
PF f1() { return f; }
// F f2() { return f; } // compile error 不能返回函数类型
F *f3() { return f; }
int main() {
return 0;
}
Next: 《C++ Primer》 拾遗 第 7 章 类