第6章 函数
本文最后更新于:2023年8月25日 凌晨
第6章 函数
1. 函数的基础
一个典型的函数定义包括以下部分:返回类型 、函数名、形参列表 以及函数体。本章的内容也是围绕这几个点展开的。
1 |
|
1.1 局部对象
在c++语言中,名字有作用域,对象有生命周期
- 名字的作用域是程序文本的一部分,名字在其中可见
- 对象的生命周期是程序执行过程中该对象存在的一段时间
局部变量
形参和函数内部定义的变量统称局部变量
- 局部变量只在函数内部起作用
- 外部全局变量和局部变量同名,局部变量会覆盖全局,这里是名称的覆盖,不是值的覆盖
局部静态变量
使用static
关键字定义静态变量。在程序的执行路径第一次经过变量定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
1 |
|
静态变量存在于程序的整个生命周期,第一次调用
count_calls
, 定义并初始化ctr
,之后再调用函数不会再执行初始化,ctr相当于一个全局的变量
1.2 函数声明
和变量名一样,函数也必须在使用之前声明,函数可以声明多次,但是只能定义一次。函数的定义不是必须的,比如声明一个函数,我们从没有调用它,那么它可以不用定义。函数的声明是没有函数体的,声明可以不写形参名,只声明类型,但在定义时如果在函数用到形参,则需要写上变量名。
1 |
|
2. 参数传递
形参初始化的机制和变量初始化一样(本节内容可结合第2章的内容看)
在c++中传参的方式主要有两种:引用传递、值传递
2.1 传值参数
普通类型形参
当初始化一个非引用类型的变量时,初始值被拷贝给变量,在函数体内改变的是实参的副本,不会对实参有影响
1 |
|
指针形参
指针形参也是值传递的一种方式,传入的指针是实参的副本(一个拷贝出来的指针),同样在函数体中改变指针的值(指向的地址)不会影响实参的值。
1 |
|
值传递的两种方式都是通过拷贝实参进行传值的,如果传入的实参比较大,拷贝会影响程序的性能。
建议使用下面将要介绍的引用传参的方式
2.2 传引用参数
相比于值传递,引用传递是直接将对象传入函数,没有拷贝带来的性能损失,所以在函数体中改变通过引用传入的形参的值,会改变实参
1 |
|
建议使用引用传参, 对于不需要改变引用形参的值,可以将其声明为常量引用
引用形参的一种用法
我们知道函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。就是我们把需要返回的一个或多个需要返回的值声明为引用形参,在函数体把值写入到引用形参中。
2.3 const形参和实参
这里涉及到顶层const和底层const的概念。
- 用实参初始化形参时会忽略顶层const,也就是说对于一个含有顶层const的形参,可以给它传递常量和非常量对象
- 我们可以使用非常量初始化一个底层const对象,但是反过来不行
将变量的初始化规则应用到参数传递
1 |
|
尽量使用常量引用
- 当我们把一个不需要改变的形参定义成非常量的话,会给人误导
- 定义成常量引用的形参,调用者可以传递常量和非常量实参
2.4 数组形参
- 不允许拷贝数组,所以数组不能以值传递的方式传入
- 为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
1 |
|
以上三个函数的形参虽然表现形式不一样,但是他们是等价的,都是
const int*
类型形参
管理指针形参
和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不越界,下面介绍三种常用的管理指针形参的技术:
- 使用标记指定数组长度(如C风格字符串以空字符\0
结束) -
使用标准库规范(传递数组首元素和尾后元素的指针) -
显示的传递一个表示数组大小的形参
数组引用形参
c++允许将变量定义成数组的引用,同样形参也可以是数组的引用
1 |
|
上面函数可传入
int arr[10]
类型,形参中维度是类型的一部分
&arr
两端的括号不能少,下面两个函数定义不等价
f(int &arr[10])
//错误,形参是引用类型的数组,不存在这种类型
f(int (&arr)[])
//正确,arr是具有10个整数类型数组的引用
传递多维数组
1 |
|
上述语句将matrix声明成指向含有10个整数的数组的指针,
*matrix
两端的括号不可少
1 |
|
2.5 main: 命令行选项
main
函数是可以带参数的,我们在命令输入的命令就是传递到main函数中,假设main函数位于可执行文件
prog
之内,我们可以向程序传递下面的选项:
1 |
|
这些命令可以通过两个形参传递给main函数
1 |
|
argc
表示命令行参数的个数,包括可执行程序本身的文件名,argv
存放命令行参数
prog -d -o ofile data0
命令, argc是5, argv内容为:
argv[0] = "prog ";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
当我们需要使用命令行输入的参数时,从
argv[1]
开始读取,argv[0]
保存的是程序名
2.6 含有可变形参的函数
省略符形参
省略符形参是C语言的标准,在C++中也是适用的,它的形式有以下两种
1 |
|
省略符形参只能出现在形参列表的最后一个位置
第一种形式指定了
foo
函数的部分形参的类型,这些形参和正常的形参一样。省略符形参所对应的传入的实参无须类型检查。在第一种形式中,形参声明后的逗号是可选的。
initializer_list 形参
C++11
initializer_list
是一种标准库类型,改类型定义在同名的头文件中,它提供的操作如表
使用示例:
1 |
|
initializer_list
是一个泛型类,使用时需指定类型,所以传递的多个参数必须是同一种类型除了initializer_list 之外,函数也可以有其他的形参
3. 返回类型和return语句
return
语句终止当前正在执行的函数并控制返回到调用该函数的地方。return语句有两种形式
1 |
|
3.1 无返回值函数
没有返回值的 return
语句只能用在返回类型是
void
的函数中。返回 void 的函数不要求非得有
return语句,这类函数的最后一句后面会隐式地执行
return,return可以放在函数内的其他位置,表示提前结束函数
1 |
|
返回值为
void
的函数体中不可以使用return experssion;
返回语句
3.2 有返回值函数
只要函数的返回类型不是 void, 则该函数内的每条 return 语句必须返回一个值。return 语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型 。
不要返回局部对象的引用或是指针
1 |
|
局部对象的指针或是引用,在返回时就会被释放,用变量接收函数的返回值得到的是不存在的对象。
引用返回左值 ----> 能为返回类型是非常量引用的函数的结果赋值
列表初始化返回值 ----> 函数可以返回花括号包围的值的列表
递归 ----> 在函数中调用了自身。递归调用必须要有终止条件
3.3 返回数组指针
数组不能拷贝,所以函数不能返回数组。不过函数可以返回数组的指针或是引用。直接定义一个返回数组的指针或是引用的函数比较烦琐,先看下使用别名的方式。
1 |
|
声明一个返回数组指针的函数
首先对下面的定义区分一下
1 |
|
返回数组指针的函数的形式如下
1 |
|
dimension
是指数组的维度,比如和上面使用别名定义的函数的等价形式
1 |
|
对于这个函数的定义,我们可以逐层的理解: func(int i)
表示一个名为 func
,参数为 int i
的函数;
*
表示的返回的是指针类型,int* [10]
表示是数组类型的指针
使用尾置返回类型
C++11 新标准提供了一种简便的方式定义这样的函数,就是使用 尾置返回类型 ,它的形式是这样的
1 |
|
使用 decltype
还有一种情况,如果我们知道函数返回的指针将指向哪个数组,可以使用
decltype
关键做声明返回类型
1 |
|
4. 函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数, 下面的几个参数列表不同的函数
1 |
|
定义重载函数
重载函数唯一区分的指标就是形参列表的数量和类型,只有返回类型不同的函数不是重载函数
重载和const形参
顶层const是无法区分形参 ,所以顶层cosnt形参无法实现重载
1 |
|
如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层const:
1 |
|
在本章的第2节中讲函数的参数时,有提到形参是常量对象的函数既可以传入常量对象,也可以传入非常量对象,比如上面两组函数中的第二个,它们可以传入常量对象也可传入非常量对象。
在这里,因为它们都有非常量形参的重载函数,那么在传入非常量对象时编译器会优先选用非常量版本的函数。
4.1重载与作用域
不要在某个语句块(函数体)的内部声明和外部名字一样的变量和函数。(受到作用域限制,会隐藏外层变量/函数)
5. 特殊用途语言特性
5.1 默认实参
有些时候,我们调用函数时,某些形参的值总是被赋予同样的值,只是在少数情况下需要要不同的值。这时我们可以把这样的形参赋予一个默认的值
1 |
|
注意:
- 默认形参必须定义在形参列表的最后
- 实参是按位置解析的,比如需要改变 w 的值,那么h的值也必须传入
- 局部变量不能作为默认实参
5.2 内联函数和constexpr函数
内联函数
为了避免函数调用的开销,将一些规模较小、流程直接的函数声明成内联函数,内联函数不会有调用的过程,而是直接在调用点把函数体内的语句嵌入进来。
1 |
|
constexpr函数
在第2章中有讲到,使用constexpr
关键字定义常量表达式,并且可以使用函数的返回值初始化定义的常量。这里使用的函数就是
constexpr函数
, 语法形式如下
1 |
|
constexpr函数在编译阶段就已经计算出了返回值,对于constexpr函数的调用是直接用计算的值替代的。为了编译过程随时展开,constexpr函数被隐式地指定为内联函数。
constexpr函数需遵循:
- 函数的返回类型及所有的形参类型都得是字面值类型
- 函数体中必须有且只有一条 return 语句
5.3 调试帮助
assert预处理宏
assert
是一种预处理宏,所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达作为它的条件:
1 |
|
首先对 expr
求值,如果表达式为假(即0),assert输出信息并终止程序的执行;如果表达式为真(即非0),assert什么也不做。
assert宏定义在cassert
头文件中,预处理名字由预处理器而非编译器管理,所以我们可以直接使用预处理名字而无需提供
using 声明。
NDEBUG 预处理变量
assert的行为依赖于一个名为 NDEBUG
的预处理变量的状态。如果定义了
NDEBUG,则assert什么也不在。默认状态下没有定义NDEBUG。定义NDEBUG既可以在程序中定义,如下
1 |
|
也可以在编译时加上NDEBUG这个参数
1 |
|
除了assert之外,我们也可以使用NDEBUG编写自己的条件调试代码
1 |
|
上面代码中 __func__
是编译器定义的变量,它是 const char
的一个静态数组,存放当前函数的名字,除了这个,还有其他变量
__FILE__
存放文件名的字符串字面值__LINE__
存放当前行号的整型字面值__TIME__
存放文件编译时间的字符串字面值__DATE__
存放文件编译日期的字符串字面值
我们可以利用上面这些变量提供错误的详细信息
6. 函数匹配
当重载函数的参数数量一样,只是类型不同的情况下,函数匹配变得有点困难了。
1 |
|
6.1 实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体如下所示
- 精确匹配,包括以下情况:
- 实参类型和形参类型相同
- 实参从数组类型或函数类型转换成对应的指针类型(函数指针下小节会讲)
- 向实参添加顶层const或者从实参中删除顶层const
- 通过const转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术类型转换实现的匹配
- 通过类类型转换实现的匹配
7. 函数指针
函数指针是指针,它指向的是函数。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:
1 |
|
*pf两端的括号不能少
1 |
|
使用函数指针
函数名和数组名一样,直接使用名字(不用取地址符)会自动地转换为指针,例如
1 |
|
此外,我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针:
1 |
|
函数指针也可以赋予 nullptr
,
函数指针赋值要和定义的类型一致才可以赋值。
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。
1 |
|
使用别名简化写法
1 |
|
返回指向函数的指针
和数组类似,我们不能返回函数,但是可以返回函数指针
1 |
|
原始的声明是从里向外读, (*f1(int))
表示 f1
是一个函数,参数是int
,返回的是指针 *
,
指针指向的是函数类型 int(int*, int)
在前面我们声明返回数组指针的函数使用过返回类型后置,同样这里也适用
1 |
|
使用 decltype
自动检测类型
1 |
|
decltype作用于函数时返回的是函数类型而非指针类型,所以需要显示地加上
*
声明为指针。