- 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 )与非限定查找( unqualified lookup )
- 3.3.2 非限定查找会进行域的逐级查找 名称隐藏( hiding )
- 3.3.3 查找通常只会在已声明的名称集合中进行
- 3.3.4 实参依赖查找( Argument Dependent Lookup: ADL )
- 3.4 重载解析:在名称查找的基础上进一步选择合适的调用函数
- 3.4.1 过滤不能被调用的版本 (non-viable candidates)
- 3.4.2 在剩余版本中查找与调用表达式最匹配的版本,匹配级别越低越好(有特殊规则)
- 4 函数相关的其它内容
- 4.1 递归函数:在函数体中调用其自身的函数
- 4.2 内联函数 / constexpr 函数 (C++11 起 ) / consteval 函数 (C++20 起 )
- 4.3 函数指针
4~7行定义了一个函数Add,在11行调用函数Add:
- 函数名称(如上图的Add)——标识符,用于后续的调用
- 形式参数(上图的x和y)——代表函数的输入参数
- 返回类型(如上图的int)——函数执行完成后所返回的结果类型
函数体是一个语句块( block )(需要带有{ }),包含了具体的计算逻辑。
- 函数声明只包含函数头,不包含函数体,通常置于头文件中
上图橙色,既包含函数头也包含函数体,就是函数的定义。
函数的声明:
- 为什么要区分函数的声明和定义?
函数声明可出现多次,但函数定义通常只能出现一次(存在例外,内联函数可以在不同的翻译单元里出现多次,我们只要保证每个翻译单元内出现一次就行)。
下图4~6行是函数声明,可以出现多个函数声明。
但函数定义不能出现多次:
声明一般放在头文件(.h)里面:
- 把声明放入头文件里:
- 在main.cpp里面,我们可以通过#include "xxx.h"引入xxx.h头文件声明
- 需要提供函数名与实际参数
实际参数用在函数调用;形参用在函数定义里。
- 实际参数拷贝初始化形式参数
如下图,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这一帧
- 返回值优化
- C++17 强制省略拷贝临时对象
将c++函数的外部链接转换成c语言的函数外部链接:(但这样就不支持函数重载了)
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。
-
传值
下图在fun调用后,arg的值不会发生改变。这样的行为叫传值:(只是把arg的值传给了par)
因为,上图等价于:
-
传址
等价于:
-
传引用
上图,par被绑定到arg上,那么接下来对par的任何修改,都会影响arg的值。
之前我们定义一个数组(10行),然后11行中的b并不是指代数组,b的类型会发生退化,退化为int型指针,指向a数组中的第一个元素,这就是拷贝初始化中引入的自动类型退化:
- 实际上我们调用函数时传入参数,这也是拷贝初始化的过程。因此,如果我们这样写:
上图代码是合法的。(我们可以用a来拷贝初始化par)。
对于函数调用,我们还可以这么写:
或:
但实际上,上面这3种写法,编译器都会把par理解成指针。
- 多维数组
上图ptr的类型是一个指针,但这个多维数组只有最高维才被退化。
与之类似,如果想定义一个函数来接收二维数组,下图代码这么写肯定出错:
应改为:
由上图可知,我们可以使用a来拷贝初始化(*ptr)[4]类型的对象。
当然我们也可以按下图这么写,但是编译器会忽略[3],还是会把par设为一个指针,这个指针指向int[4]这样的数组。
- 如何阻止类型退化?
相应地,我们如果要防止类型退化,我们可以使用引用。
- initializer_list(初始化列表)
我们可以通过上述方式使得fun函数传入的参数个数发生改变。
关于initializer_list,有两点需要声明:
(1)initializer_list中的int指initializer_list里面包含的元素类型,如果我们使用initializer_list传递一些变长参数,那么我们传递的这些类型的参数必须是完全相同的。如果传入的参数类型不同(如下图):报错("123"无法转换为int)。
(2)使用initializer_list,通常都如下图橙色这么写,我们不会把它改为initializer_list引用、initializer_list指针。
另外,下图这么写代码非常危险:
- 可变长度模板参数
传入的参数的类型可以不同。讨论模板时再讲。
- 使用省略号表示形式参数(不建议)
可以通过缺省实参来简化函数的调用。
如上图,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函数:
以上代码是合法的。但在不同翻译单元中,在main.cpp和source.cpp翻译单元中,我们都分别定义了y和z的缺省实参。
更过分点,我们还能把翻译单元的缺省实参值给改了:
main.cpp的编译结果:
为何要引入多个fun声明?
我们定义一个头文件header.h:
在header.h中引入一个声明:
接下来我们在source.cpp引入#include “header.h”:
在main函数中也引入#include “header.h”:
以上代码也是合法的,这样的话,main.cpp中相当于通过#include “header.h”,引入了z的缺省值3,而y的缺省值仍为1。source.cpp同理。
从总体分析,上述操作有啥好处?
我们可以对函数里面的形参所对应缺省实参的值进行分级指定。
如我们改变header.h中的z值:
即source和main函数调用的值都会发生改变。
但是如果我们只是改变main.cpp中的y值:
只有main函数中调用的fun函数发生改变,source函数返回的值没发生改变(因为source函数的y本身就有缺省实参,而fun函数中的形参y并没有缺省实参)。
- 缺省实参为对象时,实参的缺省值会随对象值的变化而变化
下图,由int y = x可知,y也是有缺省实参,但现在y的缺省实参是一个变量x(对象),不再是字面值,这会导致在调用fun函数时,如果没有给实参的时候(即直接为fun()),编译器会把fun()解释为fun(x),而不是fun(3)
比如我们在main函数内重新给x赋值:
此时fun(x)为fun(4),而不是fun(3)。
以上,其实关于缺省实参的正规写法应为:
-
无形参版本
-
带两个形参的版本
argv是一个指针,指向一个数组,数组的每个元素是char型,数组中包含argc+1个元素,数组最后一个元素是空指针,其他元素基本上指向字符串(最后面带有终止字符 ),如下图:
那么为什么要构造这样一个结构?
上图不打印数组最后一个元素,因为我们知道最后一个元素是空。
运行:
我们./demo,相当于argc的值为1。
有啥用?
我们可以通过上述操作,给main函数传递相应参数。
2.2 函数体 2.2.1 函数体形成域- 其中包含了自动对象(内部声明的对象以及形参对象)
- 也可包含局部静态对象
执行完第6行,那么会调回第10行,执行10行后面的代码。
关于隐式返回,有几点需要注意:
- 上图fun函数能隐式返回,是因为void,void表明fun函数不需要返回任何数值和对象。
如果没有void,那么编译器会警告:
- fun函数和main函数都隐式返回,但main函数没有void。
这是因为main函数是c++中特殊的存在。它是用来标识整个程序的入口,mian函数返回的是int值,这个int值表示的是程序执行完之后返回的结果。main函数中可以使用隐式返回,相当于返回0。
但其他函数要想使用隐式返回,需要加上void。
2.2.2.2 显式返回关键字: return- return; 语句
fun函数在执行到第6行就触发返回了。 - return 表达式 ;
错:
没有了void,我们需要返回具体的int类型的东西(或者返回的是可以转换为int类型的东西)。
应为:
我们会使用这个100来初始化12行中的x值(相当于初始化了一个对象x)。
复杂点的:
- return 初始化列表 ;
第7行的{1, 2, 3, 4, 5}就是初始化列表。我们可以使用初始化列表来初始化vector(std::vector
但注意,如果使用initializer_list。我们使用{1, 2, 3, 4, 5}初始化initializer_list,会有很多warning:
initializer_list里面包含两个指针,一个指针指向{1, 2, 3, 4, 5}中头元素,一个指向{1, 2, 3, 4, 5}的最后一个元素的下一位。如果在13行,把initializer_list返回之后,则x是initializer_list这样的类型的对象,这个对象里面包含两个指针。但有个问题,{1, 2, 3, 4, 5}是个自动对象(我们会在fun函数结束之后,这个自动对象就被销毁了),如果这个自动对象被销毁了,那么x指向initializer_list(initializer_list里面包含两个指针),但这两个指针指向了一个被销毁的对象第一个元素和最后一个元素的下一位。接下来,如果我们使用x做一系列操作时,这些操作是没有意义的。
下图,x是局部变量(自动对象),x的生存周期是从第6行开始到fun函数结束,接下来return x,但4行使用的是int&,接下来12行使用int& ref = fun();,相当于ref绑定到fun函数返回的对象x上,12行fun函数调用结束后,在后续代码中ref相当于绑定到一个已销毁的对象x上。
与之类似,如果返回的是一个指针,和返回引用类型的行为类似(返回的是一个自动对象的地址,然后ptr指向这个地址):
而下图代码规范:
此时x是局部静态对象(已经不是自动对象了),它的生存周期是从首次进入fun函数执行6行语句开始,到整个程序执行结束。
第6行:构造函数
第7行:拷贝构造函数(涉及拷贝(22行)(使用a来拷贝构造了b),就会调用这个语句)
将上图13行返回类型由int&改为Str,然后15行构造特殊的对象x:
为什么会这样?
15行构造了一个x,16行return x,系统的行为是:首先16行的x会拷贝构造临时对象Str x,接着临时对象Str x会被返回回来;这个返回回来的临时对象x会被用来拷贝构造res。这实际上是两个拷贝构造的过程,因此系统会打印两次下图:
但如果不加下图:
上面的代码什么都不输出。这是因为编译器在理解了上述程序后会引入一系列优化:
21行调用fun来返回Str类型的对象,再将这个Str类型的对象x赋予给res,当系统执行完21行时,系统开辟了一块内存,用来存储res,然后把fun()返回的结果放入res这块内存中。但这样其实很耗费计算资源,故c++会引入优化:
系统会将13行fun()进行修改,引入一个额外的参数,这个额外的参数就是res所对应的地址。接下来15行(系统本来要在栈开辟一块新的内存,在这块内存上构造res类型的对象)不需要再构造新的内存,因为已经有了res这个内存,可以直接在res内存上构造res类型的对象,即系统会把拷贝构造操作省略掉,提高性能。这就是返回值优化。
2.3 返回类型 2.3.1 返回类型表示了函数计算结果的类型,可以为 void 2.3.2 返回类型的几种书写方式 2.3.2.1 经典方法:位于函数头的前部 2.3.2.2 C++11 引入的方式:位于函数头的后部下图->后的int指的是fun函数返回的类型:
为什么要引入尾部返回?某些情况下降返回类型放函数头尾部可以简化函数的定义:
- 第1种情况,我们在使用泛型编程时, 传入的a和b的类型可能不是具体的int(比如定义函数模板,而不是定义函数,那么a,b的具体类型是由函数模板的模板参数确定的),相应地,a+b的类型也是由模板参数确定的,此时如果简单地把a+b的返回类型放在函数头,书写起来并不容易。放在函数头后部,在一定程度上可以简化函数模板的声明。
- 第2中情况,如果我们要编写一个类,fun函数是类的成员函数,那么返回的类型可能属于成员函数内部定义的类型,如果是这样的情况,把返回类型放在函数头的前部,写起来复杂(要使用类的名称)。如下图:
放在函数头的后部:
实际上关于自动推导,之前就有过讨论。
- 如何自动推导出一个对象的类型?
上图代码,编译器能自动推导出x的类型是int。
我们也可以使用decltype来进行自动推导:(使用decltype(3)推导x2的类型)
也可以使用decltype(auto)来推导x2的类型:
但之前讨论的自动推导,更多指的是对象的类型推导。而函数的返回类型也可以进行自动推导。
如下图:3行,fun函数的返回类型是int:
如果不希望显式地给出函数的返回类型,而是希望编译器对函数返回类型进行自动类型推导,那么我们可以将int改为auto:(c++14后合法)
上图,fun函数的返回类型是基于return语句推导出来的(fun函数的返回类型会被推导为return语句的表达式类型(a+b是int类型))
如果去掉return后的表达式,则fun函数的返回类型是void:
在此可知,c++14时,函数的返回类型是基于函数内部的return语句推导的,这里有一个tricking的地方:下图代码有两个return语句,一个return语句的表达式类型是int,一个return语句的表达式的类型是double类型:
如果两条return语句的表达式类型一样,合法:
我们在进行变量的类型推导时,可以使用decltype(auto)来进行推导,与之类似,在函数返回类型的自动推导中,也可以使用类似方式:
那么decltype(auto)和auto有啥区别?
上图,x变量时int类型,故fun函数返回类型也是int。如果作下图修改:将x改为表达式(x):
系统会将fun函数返回函数类型变为int&,这与我们之前讨论decltype类似,当decltype推导一个表达式时,会根据这个表达式是左值还是右值会有不同行为:如果表达式是左值,会返回int类型的左值引用;如果表达式是右值,会返回int类型的右值引用。
但上图这么写,会出现warning。因为5行x是自动对象,这个自动对象返回的是int型引用,如果使用另外一个变量去绑定到变量x身上,在fun函数调用结束后,对象x被销毁了,那么绑定到对象x的这个变量的后续操作就变成未定义操作了。那么我们可以将5行的x定义为局部静态变量(对象):
这样就不会有warning了。
再看几个自动类型推导例子:
3行定义一个变量value;fun函数不接受任何参数:
我们希望上图代码,系统能自动推导出fun函数的返回类型。但9、13行两条return语句表达式类型不同,以上代码是合法的。
但如果7行,把constexpr去掉,则代码不合法。这是因为7行的if constexpr接收的是一个常量表达式,而3行定义的value确实是常量表达式,故在7行if constexpr接收了常量表达式(value)之后,(value)的值是true的话,则执行return 1,如果value是fasle,则执行return 3.14,以此确定函数返回类型。
即,我们通过if constexpr来实现在编译期具有不同类型的返回函数。(仅限于编译期,在运行期,构造运行期函数,此时运行期只有单一类型的返回函数)
但是使用这样的技巧,并不能说明一个函数编译完成之后,只有一种返回类型这样的事实。
2.3.3 返回类型与结构化绑定( C++ 17 )下图:将左16行翻译为右19行。构造了临时变量_fun16,20、21行声明了两个变量v1、v2,这俩变量分别绑定到Str x和Str y:
我们再改一下:
报错。因为fun函数返回的是Str类型的右值,我们在16行想通过一个引用绑定到函数返回的右值上,程序会报错。
我们可以修改为:11行确保引用绑定到局部静态对象上(右21行构造了一个对象的引用,这个引用绑定到fun函数的返回值上,接下来22、23行构造了v1、v2,分别绑定到fun17的x、y上。相应地,我们就可以对v1、v2对fun函数里面的inst进行读写操作)
又如:
但并不是所有的类型都可以进行结构化绑定:
上图,10行,调用完fun(2, 3)后,并没有拿到返回值,空耗计算资源。
我们可以使用[[nodiscard]]修改:
下图3行:fun函数的返回值或返回对象非常重要,不能简简单单调用,把返回值丢了
此时根据[[nodiscard]]的提醒,我们要使用一个变量接收fun函数的返回值或返回对象(10行),这样就不会出现warning:
我们在后面讨论动态内存管理,会涉及到内存分配,基本上也是通过调用函数来实现的。调用函数的目的是我要传入多大的大小的空间,然后会通过指针的方式返回一块内存。如果获取到这块内存之后(调用了函数分配了这块内存),那么这个函数的返回的结果即指向这块内存。所以函数的返回结果非常重要,如果我们丢弃了函数返回结果,一方面,我们不能在后面合理地利用这块已分配的内存;另一方面,会造成内存泄露。
不能基于不同的返回类型进行重载
3.2 编译器如何选择正确的版本完成函数调用 ?参考资源: Calling Functions: A Tutorial
3.3 名称查找 3.3.1 限定查找( qualified lookup )与非限定查找( unqualified lookup ) 3.3.2 非限定查找会进行域的逐级查找 名称隐藏( hiding ) 3.3.3 查找通常只会在已声明的名称集合中进行 3.3.4 实参依赖查找( Argument Dependent Lookup: ADL )只对自定义类型生效
3.4 重载解析:在名称查找的基础上进一步选择合适的调用函数 3.4.1 过滤不能被调用的版本 (non-viable candidates)- 参数个数不对
- 无法将实参转换为形参
- 实参不满足形参的限制条件
- 级别 1 :完美匹配 或 平凡转换(比如加一个 const )
- 级别 2 : promotion 或 promotion 加平凡转换
- 级别 3 :标准转换 或 标准转换加平凡转换
- 级别 4* :自定义转换 或 自定义转换 加平凡转换 或 自定义转换加标准转换
- 级别 5* :形参为省略号的版本
- 函数包含多个形参时,所选函数的所有形参的匹配级别都要优于或等于其它函数
通常用于描述复杂的迭代过程(示例)
4.2 内联函数 / constexpr 函数 (C++11 起 ) / consteval 函数 (C++20 起 ) 4.3 函数指针- 函数类型与函数指针类型
- 函数指针与重载
- 将函数指针作为函数参数
- 将函数指针作为函数返回值
- 小心: Most vexing parse



