函数
- 1 函数基础
- 2 函数详解
- 3 函数重载与重载解析
- 4 函数相关的其它内容
1 函数基础
1.1 函数:封装了一段代码,可以在一次执行过程中被反复调用。
4~7行定义了一个函数Add,在11行调用函数Add:
1.1.1 函数头(如上图第4行)
- 函数名称(如上图的Add)——标识符,用于后续的调用
- 形式参数(上图的x和y)——代表函数的输入参数
- 返回类型(如上图的int)——函数执行完成后所返回的结果类型
1.1.2 函数体(如第一张图的5~7行)
函数体是一个语句块( block )(需要带有{ }),包含了具体的计算逻辑。
1.2 函数声明与定义
- 函数声明只包含函数头,不包含函数体,通常置于头文件中
上图橙色,既包含函数头也包含函数体,就是函数的定义。
函数的声明:
- 为什么要区分函数的声明和定义?
函数声明可出现多次,但函数定义通常只能出现一次(存在例外,内联函数可以在不同的翻译单元里出现多次,我们只要保证每个翻译单元内出现一次就行)。
下图4~6行是函数声明,可以出现多个函数声明。
但函数定义不能出现多次:
声明一般放在头文件(.h)里面:
- 把声明放入头文件里:
- 在main.cpp里面,我们可以通过#include "xxx.h"引入xxx.h头文件声明
1.3 函数调用
- 需要提供函数名与实际参数
实际参数用在函数调用;形参用在函数定义里。
- 实际参数拷贝初始化形式参数
如下图,x会使用2来拷贝初始化,y会用3来拷贝初始化
- 返回值会被拷贝给函数的调用者
- 栈帧结构
如下图7行,我们在调用函数的过程中,函数可能包含一些参数,变量,所有的这些东西都会放在内存当中,这些东西在内存中是通过栈帧(Frame)结构来组织的。
下图每个方框都叫栈,栈的特点是后进先出(往里面放东西,拿出来时是最后放进去的最先拿出来)。
funcA Frame是一帧,这一帧里面可能包含了funcA所调用需要的一些信息,包括它的形参、变量等。接下来,funcA可能会调用funcB,此时系统会在funA上再开辟一块内存(新的一帧),funB可能会调用funC。。。以此类推
当funC调用结束后,funC这一帧会被扔出去(后进先出),此时funB又活了,系统又funcB这个状态,funcB执行完之后,系统又回到funcA这个状态:
对于下图代码:main算一帧,main函数里面又调用Add函数(又算一帧),当Add这一帧执行完后出栈了,再回到mian这一帧
1.4 拷贝过程的(强制)省略
- 返回值优化
- C++17 强制省略拷贝临时对象
1.5 函数的外部链接
将c++函数的外部链接转换成c语言的函数外部链接:(但这样就不支持函数重载了)
2 函数详解
2.1 参数
2.1.1 函数可以在函数头的小括号中包含零到多个形参
0个形参:
2个形参:
- 包含零个形参时,可以使用 void 标记
等价于:
- 对于非模板函数来说,其每个形参都有确定的类型,但形参可以没有名称
形参名称的作用:在函数内我们可以使用这个形参名称来去访问实参所对应的数字,如下图,我们可以使用x来访问fun(1)中的实参1。
不写形参名称也可以通过编译:(一般这么写是留出接口以作备用)
- 形参名称的变化并不会引入函数的不同版本
这样不算引入不同函数版本,依旧算函数重复定义:
- 实参到形参的拷贝求值顺序不定
如下图,是先用1来初始化z,还是先用2来初始化y,这个初始化的顺序是不确定的。
由于这种不确定性,如果我们写出一下这样的代码会很危险:(不同编译器下y输出结果可能不同)
- C++17 强制省略复制临时对象
我们在调用10行的1和2时,会把1拷贝给z,2拷贝给y,
但有一种情况:
橙色相当于建立了一个临时对象。当我们将这个临时对象拷贝给y时,c++17标准而言,我们会把这个拷贝的过程强制省略掉,也即并不会把int{}拷贝给y。
2.1.2 函数传值、传址、传引用
-
传值
下图在fun调用后,arg的值不会发生改变。这样的行为叫传值:(只是把arg的值传给了par)
因为,上图等价于:
-
传址
等价于:
-
传引用
上图,par被绑定到arg上,那么接下来对par的任何修改,都会影响arg的值。
2.1.3 函数传参过程中的类型退化
之前我们定义一个数组(10行),然后11行中的b并不是指代数组,b的类型会发生退化,退化为int型指针,指向a数组中的第一个元素,这就是拷贝初始化中引入的自动类型退化:
- 实际上我们调用函数时传入参数,这也是拷贝初始化的过程。因此,如果我们这样写:
上图代码是合法的。(我们可以用a来拷贝初始化par)。
对于函数调用,我们还可以这么写:
或:
但实际上,上面这3种写法,编译器都会把par理解成指针。
- 多维数组
上图ptr的类型是一个指针,但这个多维数组只有最高维才被退化。
与之类似,如果想定义一个函数来接收二维数组,下图代码这么写肯定出错:
应改为:
由上图可知,我们可以使用a来拷贝初始化(*ptr)[4]
类型的对象。
当然我们也可以按下图这么写,但是编译器会忽略[3],还是会把par设为一个指针,这个指针指向int[4]这样的数组。
- 如何阻止类型退化?
相应地,我们如果要防止类型退化,我们可以使用引用。
2.1.4 变长参数
- initializer_list(初始化列表)
我们可以通过上述方式使得fun函数传入的参数个数发生改变。
关于initializer_list,有两点需要声明:
(1)initializer_list中的int指initializer_list里面包含的元素类型,如果我们使用initializer_list传递一些变长参数,那么我们传递的这些类型的参数必须是完全相同的。如果传入的参数类型不同(如下图):报错("123"无法转换为int)。
(2)使用initializer_list,通常都如下图橙色这么写,我们不会把它改为initializer_list引用、initializer_list指针。
另外,下图这么写代码非常危险:
- 可变长度模板参数
传入的参数的类型可以不同。讨论模板时再讲。
- 使用省略号表示形式参数(不建议)
2.1.5 函数可以定义缺省实参
可以通过缺省实参来简化函数的调用。
如上图,fun()里面可能包含很多形参,在调用时,需要为每个形参配一个实参,这个过程很麻烦。因此我们可以为这个函数赋予缺省实参(int x = 0
):
- 如果某个形参具有缺省实参,那么它右侧的形参都必须具有缺省实参
下图代码不合法,因为3行左边的int x = 0为缺省实参,其右边不是缺省实参:
但这么写合法:
为什么会这样?
缺省值的目的是?缺省值的目的,如下图代码:
当我们使用fun(1)来调用fun函数时,形参和实参之间是要有匹配的,只有完成匹配之后,我们才能确定函数的具体行为。我们调用fun(1)时,一定要知道我们为x赋予什么值,为y赋予什么值,为z赋予什么值。上图中x没有缺省值,那么我们会把fun(1)中的1赋予给x,y对应其缺省值1,z对应其缺省值2,此时x,y,z都有对应的缺省值。
- 具有缺省实参的函数调用时,传入的实参会按照从左到右的顺序匹配形参
那么编译器是怎么完成刚才说的实参和形参的对应的呢?答:传入的实参会按照从左到右的顺序匹配形参
即,上图,1会和x匹配,然后fun(1)里面没有实参了,我们就拿3行的缺省值来匹配y和z。但如果按下图这么写,fun(1)中的1该匹配谁呢?实际上还是会从左到右的优先顺序,给x匹配,那么y就没有实参与之匹配了。故整个代码不合法。
- 在一个翻译单元中,每个形参的缺省实参只能定义一次
下图,3行为fun函数的声明,5行为fun函数的定义。但代码报错,这是因为3行和5行都定义了缺省实参。
应改为:
以下这样也是合法的:(缺省实参int z = 3
和int y = 2
都只定义了一次)(注意,4行并没有违反“如果某个形参具有缺省实参,那么它右侧的形参都必须具有缺省实参”这个性质,因为z在3行已经缺省实参化了)
同理这样也没问题:
但下图违反了“如果某个形参具有缺省实参,那么它右侧的形参都必须具有缺省实参”
但以上的前提,是在同一个翻译单元。
如果是不同翻译单元呢?
我们把mian.cpp中的fun函数放到另一个.cpp文件(fun.cpp)(翻译单元)中:
然后在main.cpp里,我们可以不用再去定义fun函数了,可以直接通过引入fun函数的声明,那么在main.cpp里面,我们就可以直接调用fun函数了:
我们再引入一个翻译单元Source.cpp,在Source.cpp我们还是引入fun函数定义,接下来定义另外一个函数source,该函数里面,我们调用fun函数:
此时main.cpp中相应加入source函数声明,然后在main函数里面调用source函数:
以上代码是合法的。但在