函数"/>
【C++基础】第6章:函数
函数
- 1 函数基础
- 1.1 函数:封装了一段代码,可以在一次执行过程中被反复调用。
- 1.1.1 函数头(如上图第4行)
- 1.1.2 函数体(如第一张图的5~7行)
- 1.2 函数声明与定义
- 1.3 函数调用
- 1.4 拷贝过程的(强制)省略
- 1.5 函数的外部链接
- 2 函数详解
- 2.1 参数
- 2.1.1 函数可以在函数头的小括号中包含零到多个形参
- 2.1.2 函数传值、传址、传引用
- 2.1.3 函数传参过程中的类型退化
- 2.1.4 变长参数
- 2.1.5 函数可以定义缺省实参
- 2.1.6 main 函数的两个版本
- 2.2 函数体
- 2.2.1 函数体形成域
- 2.2.2 函数体执行完成时的返回
- 2.2.2.1 隐式返回
- 2.2.2.2 显式返回关键字: return
- 2.2.2.3 小心返回自动对象的引用或指针
- 2.2.2.4 返回值优化( RVO )—— C++17 对返回临时对象的强制优化
- 2.3 返回类型
- 2.3.1 返回类型表示了函数计算结果的类型,可以为 void
- 2.3.2 返回类型的几种书写方式
- 2.3.2.1 经典方法:位于函数头的前部
- 2.3.2.2 C++11 引入的方式:位于函数头的后部
- 2.3.2.3 C++14 引入的方式:返回类型的自动推导
- 2.3.2.4 扩展:使用 constexpr if “ 构造具有不同返回类型” 的函数
- 2.3.3 返回类型与结构化绑定( C++ 17 )
- 2.3.4 [[nodiscard]] 属性( C++ 17 )
- 3 函数重载与重载解析
- 3.1 函数重载:使用相同的函数名定义多个函数,每个函数具有不同的参数列表
- 3.2 编译器如何选择正确的版本完成函数调用 ?
- 3.3 名称查找(广义重载解析的第一步)
- 3.3.1 限定查找( qualified lookup )
- 3.3.2 非限定查找( unqualified lookup )
- 3.3.3 实参依赖查找( Argument Dependent Lookup: ADL )
- 3.3.4 函数模板、类模板的实参依赖查找(ADL)
- 3.4 重载解析:在名称查找的基础上进一步选择合适的调用函数
- 3.4.1 过滤不能被调用的版本 (non-viable candidates)
- 3.4.2 在剩余版本中查找与调用表达式最匹配(实参和形参进行匹配)的版本,匹配级别越低越好(有特殊规则)
- 4 函数相关的其它内容
- 4.1 递归函数:在函数体中调用其自身的函数
- 4.2 内联函数
- 4.3 constexpr 函数 (C++11 起 )
- 4.4 consteval 函数 (C++20 起 )
- 4.5 constexpr 函数 / consteval 函数与内联函数的关系
- 4.6 函数指针
- 4.6.1 函数类型
- 4.6.1.1 数组类型和函数类型的异同
- 4.6.2 函数指针类型
- 4.6.2.1 数组的指针类型
- 4.6.2.2 函数的指针类型
- 4.6.2.3 函数指针类型继续和数组比较
- 4.6.3 函数指针与重载
- 4.6.4 将函数指针作为函数参数
- 4.6.5 将函数指针作为函数返回值
- 4.6.6 小心: Most vexing parse最令人苦恼的解析
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指针。
另外,下图这么写代码非常危险:
- 可变长度模板参数
传入的参数的类型可以不同。讨论模板
更多推荐
【C++基础】第6章:函数
发布评论