6.函数
函数
函数对程序的结构化至关重要!!
1 函数基础
函数是一个命名的代码块,通过调用函数执行相应的代码。可以有 0 个或多个参数,可以重载。
- 函数定义:包括返回类型、函数名字和 0 个或者多个形参(parameter)组成的列表和函数体。
- 调用运算符:调用运算符的形式是一对圆括号
()
,作用于一个表达式,该表达式是函数或者指向函数的指针。 - 圆括号内是用逗号隔开的实参(argument)列表。
- 函数调用过程(使用到栈的数据结构):
- 1.主调函数(calling function)的执行被中断。
- 2.被调函数(called function)开始执行。
- 形参和实参:形参和实参的个数和类型必须匹配上。
- 返回类型:
void
表示函数不返回任何值。函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针。 - 名字:名字的作用于是程序文本的一部分,名字在其中可见。
1.1 局部对象
- 生命周期:对象的生命周期是程序执行过程中该对象存在的一段时间。
- 局部变量(local variable):形参和函数体内部定义的变量统称为局部变量。它对函数而言是局部的,对函数外部而言是隐藏的。
- 自动对象:只存在于块执行期间的对象。当块的执行结束后,它的值就变成未定义的了。生命周期从变量声明开始,到函数块末尾结束
- 局部静态对象:
static
类型的局部变量,生命周期贯穿函数调用前后。
1.2 函数声明
- 函数声明:函数的声明和定义唯一的区别是声明无需函数体,用一个分号替代。函数声明主要用于描述函数的接口,也称函数原型。
- 在头文件中进行函数声明:建议变量在头文件中声明;在源文件中定义。
- 分离编译:
CC a.cc b.cc
直接编译生成可执行文件;CC -c a.cc b.cc
编译生成对象代码a.o b.o
;CC a.o b.o
编译生成可执行文件。
2 参数传递
- 形参初始化的机理和变量初始化一样。
- 引用传递(passed by reference):又称传引用调用(called by reference),指形参是引用类型,引用形参是它对应的实参的别名。
- 值传递(passed by value):又称传值调用(called by value),指实参的值是通过拷贝传递给形参。
2.1 传值参数
- 当初始化一个非引用类型的变量时,初始值被拷贝给变量。
- 函数对形参做的所有操作都不会影响实参。
- 指针形参:常用在 C 中,
C++
建议使用引用类型的形参代替指针。
2.2 传引用参数
- 通过使用引用形参,允许函数改变一个或多个实参的值。
- 引用形参直接关联到绑定的对象,而非对象的副本。
- 使用引用形参可以用于返回额外的信息。
- 经常用引用形参来避免不必要的复制。使用引用避免深拷贝
void swap(int &v1, int &v2)
- 如果无需改变引用形参的值,最好将其声明为常量引用(const)
2.3 const 形参和实参
- 形参的顶层
const
被忽略。void func(const int i);
调用时既可以传入const int
也可以传入int
。 - 我们可以使用非常量初始化一个底层
const
对象,但是反过来不行。 - 在函数中,不能改变实参的局部副本。
- 形参的顶层 const 会被忽略掉
- 这个函数承诺不会修改参数,那么我传入 const 或者非 const 的参数都无所谓,所以顶层 const 会被忽略
- 指针或引用形参与 const,底层 const 不允许忽略
- 这个函数需要参数是一个指针,通过指针修改对应地址的值,你给我指向常量(const)的指针,我不接受
如果函数无需改变引用形参的值,最好将其声明为常量引用。
2.4 数组形参
C++允许将变量定义成数组的引用
- 当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
- 以数组作为形参的函数必须确保使用数组时不会越界
2.5 main:处理命令行选项
int main(int argc, char *argv[]);
int main(int argc, char **argv); // the same
第一个形参代表参数的个数;第二个形参是参数 C 风格字符串数组。
2.6 含有可变形参的函数
如果函数的实参数量未知但是全部实参的类型都相同,可以使用 initializer_list 类型的形参。
可变形参常用来处理错误信息,因为错误信息种类不同,数量也不确定。
initializer_list
模板提供的操作(C++11
):
操作 | 解释 |
---|---|
initializer_list<T> lst; |
默认初始化;T 类型元素的空列表 |
initializer_list<T> lst{a,b,c...}; |
lst 的元素数量和初始值一样多;lst 的元素是对应初始值的副本;列表中的元素是const 。 |
lst2(lst) |
拷贝或赋值一个initializer_list 对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素。 |
lst2 = lst |
同上 |
lst.size() |
列表中的元素数量 |
lst.begin() |
返回指向lst 中首元素的指针 |
lst.end() |
返回指向lst 中微元素下一位置的指针 |
initializer_list
使用 demo:
void err_msg(ErrCode e, initializer_list<string> il){
cout << e.msg << endl;
for (auto bed = il.begin(); beg != il.end(); ++ beg)
cout << *beg << " ";
cout << endl;
}
err_msg(ErrCode(0), {"functionX", "okay});
- 所有实参类型相同,可以使用
initializer_list
的标准库类型。 - 实参类型不同,可以使用
可变参数模板
。 - 省略形参符:
...
,只能出现在形参的最后,便于C++
访问某些 C 代码,这些 C 代码使用了varargs
的 C 标准功能。
3 返回类型和 return 语句
3.1 无返回值函数
没有返回值的 return
语句只能用在返回类型是 void
的函数中,返回 void
的函数不要求非得有 return
语句。
3.2 有返回值函数
return
语句的返回值的类型必须和函数的返回类型相同,或者能够隐式地转换成函数的返回类型。- 值的返回:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
- 不要返回局部对象的引用或指针。
- 引用返回左值:函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值;其他返回类型得到右值。
- 列表初始化返回值:函数可以返回花括号包围的值的列表。(
C++11
) - 主函数 main 的返回值:如果结尾没有
return
,编译器将隐式地插入一条return 0
语句。返回 0 代表执行成功。
3.3 返回数组指针
-
数组不能拷贝,所以函数不能直接返回数组
-
声明返回数组指针的函数
Type (*function (parameter_list))[dimension]
-
使用类型别名:
typedef int arrT[10];
或者using arrT = int[10;]
,然后arrT* func() {...}
-
使用
decltype
:int odd[1,3,5,7,9]; decltype(odd) *arrPtr(int i) {...}
-
尾置返回类型: 在形参列表后面以一个
->
开始:auto func(int i) -> int(*)[10]
(C++11
)
typedef int arrT[10]; // arrT is a synonym for the type array of ten ints
// two ways to declare function returning pointer to array of ten ints
arrT* func(int i); // use a type alias
int (*func(int i))[10]; // direct declaration
4 函数重载
- 重载:如果同一作用域内几个函数名字相同但形参列表不同,我们称之为重载(overload)函数。
main
函数不能重载。- 重载和 const 形参:
- 一个有顶层 const 的形参和没有它的函数无法区分。
Record lookup(Phone* const)
和Record lookup(Phone*)
无法区分。 - 相反,是否有某个底层 const 形参可以区分。
Record lookup(Account*)
和Record lookup(const Account*)
可以区分。
- 一个有顶层 const 的形参和没有它的函数无法区分。
- 重载和作用域:若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。内层优先级高于外层
5 特殊用途语言特性
5.1 默认实参
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
- 一旦某个形参被赋予了默认值,那么它之后的形参都必须要有默认值。
5.2 内联函数和 constexpr 函数
5.2.1 内联函数
- 在每个调用点上“内联地”展开,避免函数调用的开销
- 内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求
- 现在一般不写,因为这个是编译器决定的,写 inline 只是建议,不写编译器也可能做这样的优化
//inline version: find the shorter of two strings
inline const string &
shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
5.2.2 constexpr 函数
- 能用于常量表达式的函数:函数的返回类型以及所有的形参都是字面值类型
- 函数体中必须有且只有一条 return 语句
- constexpr 函数被隐式地指定为内联函数
- constexpr 函数并不要求返回常量表达式
constexpr int new_sz()
{
return 42;
}
5.3 调试帮助
- 调试帮助:只在开发过程中使用的代码,发布时屏蔽掉
- assert 预处理宏:cassert 头文件中,用于断言测试
- 用于检测“不能发生”的条件
- 开发阶段使用,发布阶段需要去除
assert
预处理宏(preprocessor macro):assert(expr);
开关调试状态:
CC -D NDEBUG main.c
可以定义这个变量NDEBUG
。
void print(){
# ifndef NDEBUG
cerr << __func__ << "..." << endl;
# endif
}
6 函数匹配
- 重载函数匹配的三个步骤:1.候选函数;2.可行函数;3.寻找最佳匹配。
- 候选函数:选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。
- 可行函数:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数(viable function)。
- 寻找最佳匹配:基本思想:实参类型和形参类型越接近,它们匹配地越好。寻找最佳匹配、不能具有二义性
编译器将实参类型到形参类型的转换划分成几个等级:
- 精确匹配
- 通过 const 转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术类型转换实现的匹配(所有算术类型转换的级别都一样)
- 通过类类型转换实现的匹配
7 函数指针
-
函数指针:是指向函数的指针。
-
bool (*pf)(const string &, const string &);
注:*pf 两端的括号不可少。 -
函数指针形参:
- 形参中使用函数定义或者函数指针定义效果一样。
- 使用类型别名或者
decltype
。
-
返回指向函数的指针:1.类型别名;2.尾置返回类型。
-
函数指针的取地址和解引用是可选的
- 常规函数 bool b = lengthCompare()
- ================
- 函数指针 bool b = pf = &lengthCompare()
- 函数指针(省略&) pf = lengthCompare()
- ================
- 指针调用函数 (*pf)(“hello”,“goodbye”)
- 指针调用函数(省略*) pf(“hello”,“goodbye”)
bool (*pf)(const string &,const string &)
在指向不同函数类型的指针间不存在转换规则
7.1 重载函数的指针
函数本身作为一个类型(函数指针),必须精确匹配
7.1.1 函数指针形参
不能定义函数类型的形参,但是形参可以是指向函数的指针
一般使用是把函数作为参数,比如排序,可以定义 commonSort( int *, ( *sorting_rules)(int a,int b)); 根据传入的 sorting_rules 函数指针,决定怎么排序。
个人表示太复杂,非必要不使用
void useBigger(const string &s1,bool (*pf)(const string &,const string &))
7.1.2 返回指向函数的指针
和数组类似,不能返回数组,但是可以返回数组类型的指针
不能返回函数,但可以返回指向函数的指针(和函数类型的形参不一样,返回类型必须写成指针形式)
int (*f1(int))(int *, int);
// 使用别名,会更加清晰
using F = int(int*,int);
using PF = int(*)(int*,int);
PF f1(int);// 正确,返回一个函数指针
F f1(int);//错误,不能返回一个函数
F* f1(int);//正确,显示指定返回类型是函数指针
常用 auto 和 decltype 用于函数指针类型
8 小结
函数是命名了的计算单元,它对程序(哪怕是不大的程序)的结构化至关重要。每个函数都包含返回类型、名字、(可能为空的)形参列表以及函数体。函数体是一个块,当函数被调用的时候执行该块的内容,此时,传递给函数的实参类型必须与对应的形参类型相同。
在 C++语言中,函数可以被重载:同一个名字可用于定义多个函数,只要这些函数的形参数量或形参类型不同就行,根据调用时所使用的实参,编译器可以自动地选定被调用的函数。从一组重线函数中选取最佳函数的过程称为函数匹配。
9 术语表
二义性调用(ambiguous call): 是一种编译时发生的错误,造成二义性调用的原因时在函数匹配时两个或多个函数提供的匹配一样好,编译器找不到唯一的最佳匹配。
实参(argument): 函数调用时提供的值,用于初始化函数的形参。
Assert: 是一个预处理宏,作用于一条表示条件的表达式。当未定义预处理遍历 NDEBUG 时,assert 对条件求值。如果条件为假,输出一条错误信息并终止当前程序的执行。
自动对象(automatic object): 仅存在于函数执行过程中的对象。当程序的控制流经过此类对象的定义语句时,创建该对象;当到达了定义所在的块的末尾时,销毁该对象。
最佳匹配(best match): 从一组重载函数中为调用选出的一个函数。如果存在最佳匹配,则选出的函数与其他所有可行函数相比,至少在一个实参上时更优的匹配,同时在其他实参的匹配上不会更差。
传引用调用(call by reference): 参见引用传递。
传值调用(call by value): 参见值传递。
候选函数(candidate function): 解析某次函数调用时考虑的一组函数。候选函数的名字应该与函数调用使用的名字一致,并且在调用点候选函数的声明在作用域之内。
constexpr: 可以返回常量表达式的函数,一个 constexpr 函数被隐式地声明成内联函数。
默认实参(defalut argument): 当调用缺少了某个实参时,为该实参指定地默认值。
可执行文件(executable file): 是操作系统能够执行的文件,包含着与程序有关的代码。
函数(function): 可调用的计算单元。
函数体(function body): 是一个块,用于定义函数所执行的操作。
函数匹配(function matching): 编译器解析重载函数调用的过程,在此过程中,实参与每个重载函数的形参列表逐一比较。
函数原型(function prototype): 函数的声明,包含函数名字,返回类型和形参类型。要调用某函数,在调用点之前必须声明该函数的原型。
隐藏名字(hidden name): 某个作用域内声明的名字会隐藏掉外层作用域中声明的同名实体。
initalizer_list: 是一个标准类,表示的是一组花括号包围的类型相同的对象,对象之间以逗号隔开。
内联函数(inline function): 请求编译器在可能的情况下在调用点展开函数。内联函数可以避免常见的函数调用开销。
链接(link): 是一个编译过程,负责把若干对象文件链接起来形成可执行程序。
局部静态对象(local static object): 它的值在函数调用结束后仍然存在。在第一次使用局部静态对象前创建并初始化它,当程序结束时局部静态对象才会被销毁。
无匹配(no match): 是一种编译时发生的错误,原因时在函数匹配过程中所有函数的形参都不能与调用提供的实参匹配。
**对象代码(object code):**编译器将我们的源代码转换成对象代码格式。
对象文件(object file): 编译器根据给定的源代码生成保存对象的文件。一个或多个对象文件经过链接生成可执行文件。
对象生命周期(object lifetime): 每个对象都有相应的生命周期。块内定义的非静态对象的生命周期从它的定义开始,到定义所在的块末尾为止。程序启动后创建全局对象,程序控制流经过局部局部静态对象的定义时创建该局部静态对象;当 main 函数结束时销毁全局对象和局部静态对象。
重载确定(overload resolution): 参见函数匹配。
重载函数(overload function): 函数名与其他函数相同的函数。多个重载函数必须在形参数量或形参类型上有所区别。
形参(parameter): 在函数的形参类别中声明的局部变量。用实参初始化形参。
引用传递(pass by reference): 描述如何将实参床底给引用类型的形参。引用形参和其他形式的引用工作机理类似,形参被绑定到相应实参值的一个副本。
值传递(pass by value): 描述如何将实参传递给非引用类型的形参。非引用类型的形参实际上时相应实参值的一个副本。
预处理宏(perprocessor macro): 类似于内联函数的一种预处理功能。除了 assert 之外,现代 C++程序很少再使用预处理宏了。
递归循环(recurision loop): 描述某个递归寒素没有终止条件,因而不断调用自身直至耗尽程序栈空间的过程。
递归函数(recurision function): 直接或间接调用自身的函数。
返回类型(return type): 是函数声明的一部分,用于指定函数返回值的类型。
分离式编译(separate compilation): 把一个程序分割成多个独立源文件的能力。
尾置返回类型(trailing return type): 在参数类别后边指定的返回类型。
可行函数(viable function): 是候选函数的子集。可行函数能匹配本次调用,他的形参数量于调用提供的实参数量相等,并且每个似乎从类型都能转换成相应的形参类型。
()运算符( ()operator): 调用运算符,用于执行某函数。括号前面是函数名或函数指针,括号内是以逗号隔开的实参列表(可能为空)。