函数
函数基础
包括返回类型、函数名字、0到多个形参组成的列表及函数体。
调用运算符来执行函数。函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。
形参和实参
形参和实参数量类型顺序必须一一对应。
局部对象
形参和函数内部定义变量统称局部变量。同时局部变量还会隐藏在外层作用域中。函数体之外的对象存在于程序的整个执行过程中。局部变量的生命周期依赖定义的方式。
自动对象
对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。
局部静态对象
可以将局部变量定义成static类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。
函数声明
函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。
因为函数的声明不包含函数体,所以也就无须形参的名字。在函数的声明中经常省略形参的名字。尽管如此,写上形参的名字可以帮助使用者更好地理解函数的功能。
函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型。通常把它放在头文件中。
分离式编译
一个项目由若干个源文件共同实现,而每个源文件(.cpp)单独编译成目标文件(.obj),最后将所有目标文件连接起来形成单一的可执行文件(.exe)的过程。
1 | ---------------test.h------------------- |
上面程序在编译器内部的过程为:
- 在编译mian.cpp的时候,编译器并不知道f的实现,所以当碰到对f的调用时只是给出一个指示,指示连接器为它寻找f的实现体,所以main.obj中没有关于f实现的二进制代码。
- 在编译test.cpp的时候,编译器找到了f的实现,所以在test.obj里有f实现的二进制代码。
- 连接时,连接器在test.obj中找到f实现的二进制地址,然后将main.obj中未解决的f地址替换成该二进制地址。
作者:凉拌姨妈好吃
链接:https://www.jianshu.com/p/9ca511da30f4
来源:简书
参数传递
形参初始化的机理与变量初始化一样。形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
- 当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
- 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用。
传值参数
ret *= val--; //将val的值减1
尽管fact函数改变了val的值,但是这个改动不会影响传入fact的实参。调用fact (i)不会改变i的值。
形参指针
1 | int n= 0,i = 42; |
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:
C++中建议使用引用代替指针
传引用参数
引用参数可以改变引用对象的值。
使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚全有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。如string类型。若想避免改动可以加上const
使用引用形参返回额外的信息
一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。
举个例子,我们定义一个名为find_char的函数,它返回在string对象中某个指定字符第一次出现的位置。同时,我们也希望函数能返回该字符出现的总次数,该如何定义函数使得它能够既返回位置也返回出现次数呢?
- 定义一个新的数据类型,让它包含位置和数量两个成员。
- 还有另一种更简单的方法,我们可以给函数传入一个额外的引用实参,令其保存字符出现的次数:
caonst形参和实参
和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
void fcn ( const int i){/* fcn能够读取i,但是不能向i写值*/}
1 | void fcn (const int i){/* fcn能够读取i,但是不能向i写值*/ } |
虽然函数可以重载,但是上面由于忽略了const,所以时一模一样的。
指针或引用 形参与const
1 | //该函数接受一个int对象的引用,然后将对象的值置为0 |
1 | int i =42; |
将同样的初始化规则应用到参数传递上可得如下形式:
1 | int i = 0 ; |
调用reset只能使用int类型。不能使用字面值、结果为int的表达式。
尽量使用常量引用
则尽可能在不需要改变的形参前加const。
数组形参
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式://尽管形式不同,但这三个 print函数是等价的
1 | //每个函数都有一个const int*类型的形参 |
因为被转化为指针所以需要一个额外参数记录数组长度。
使用标准库规范
也可以使用标准库里的begin指针
main:处理命令行选项
main()函数的参数可以是不为空,main()函数中的参数有两个,一个是argc表示数组中元素的个数,一个是char *argv[],表示的是指向一个字符串数组的指针,所以也可以写成char **argv。有参的main()函数可写为main(int argc,char *argv[])。
当实参传给main()函数时,argv的第一个元素是指向程序的名字或者是一个空字符串,接下来就是将实参传递给形参。最后一个元素是保证是0。
1 | argv[0] = “test”; |
这个数组的长度是5,因为argv[]实参是从1开始,argv[0]元素是程序的名字,非用户输入。
含有可变形参的函数
initializer_list形参
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。initializer_list类型定义在同名的头文件中。它的操作与vector相似,不同的时这个的元素永远时常量
返回类型和return语句
无返回值函数
可以使用return退出函数,也可以return 返回一个void的函数。
有返回值的函数
该函数必须return一个与函数返回值类型相同的类型,也可以返回一个能隐式转换成返回值类型的类型。
值是如何被返回
返回的值是用于初始化调用的一个临时变量。
如果使用引用作为返回值,则返回的知识一个别名,并不会拷贝对象。
不要返回局部对象的引用和指针
引用返回左值
调用一个返回引用的函数得到左值,其他为右值,常量引用不能赋值。
列表初始化返回值
C++11新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化,否则,返回的值由函数的返回类型决定。
1 | vector<string> process () |
主函数main的返回值
之前介绍过,如果函数的返回类型不是void,那么它必须返回一个值。但是这条规则有个例外:我们允许main函数没有return语句直接结束。如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。
递归
指针函数调用自身。main函数不能嗲用自己
返回数组指针
因不能拷贝数组,所以可以返回一个数组指针
声明返回数组指针的函数
Tvpe ( * function (parameter list) )[dimensionl
int(* func(int i)) [10];
可以按照以下的顺序来逐层理解该声明的含义:
func(int i)
表示调用func函数时需要一个int类型的实参。(*func(int i))
意味着我们可以对函数调用的结果执行解引用操作。(*func(int i) ) [10]
表示解引用func的调用将得到一个大小是10的数组。int(*func(int i) ) [10]
表示数组中的元素是int类型。
使用位置返回类型
由于这里即使复杂度提升也没有很明显的提升,所以可以使用下面的方法
使用decltye
1 | int odd[] ={1,3,5,7,9} ; |
如何要返回指针务必加一个*
。
函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。中我们定义了几个名为print 的函数,main函数不能重载。
定义重载函数
对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。在上面的代码中,虽然每个函数都只接受一个参数,但是参数的类型不同。
不允许两个函数除了返回类型外其他所有的要素都相同。假设有两个函数,它们的形参列表一样但是返回类型不同,则第二个函数的声明是错误的。
判断两个形参的类型是否相异
1 | //每对声明的是同一个函数 |
重载和const形参
因为顶层const不影响函数的对象,所以顶层const和无const形参无法区分。
1 | Record lookup (Phone) ; |
const_cast和重载
可以使用该函数去掉或者加上const,再返回去。
调用重载的函数
现在我们需要掌握的是,当调用重载函数时有三种可能的结果:
- 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息。
- 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用( ambiguous call)。
重载与作用域
重载对作用域的一般性质并没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名:
1 | string read o) ; |
特殊用于语言特性
默认实参
在声明函数时可以给形参赋予一个默认值,默认实参作为形参的初始值出现在形参列表中。我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
1 | typedef string: :size_type sz; //关于typedef参见2.5.1节(第60页) |
使用默认实参调用函数
函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参‘靠右侧位置),调用函数时,传入的实参是从左向右依次传入函数形参中。
1 | string window; |
默认实参声明
对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参,换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
默认实参初始值
局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参:
1 | // wd、def和ht的声明必须出现在函数之外 |
用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时:
1 | void f2(){ |
内联函数和constexpr函数
把规模较小的操作定义成函数有很多好处,主要包括:
- 阅读和理解shorterString函数的调用要比读懂等价的条件表达式容易得多。
- 使用函数可以确保行为的统一,每次相关操作都能保证按照同样的方式进行。
- 如果我们需要修改计算过程,显然修改函数要比先找到等价表达式所有出现的地方再逐一修改更容易。
- 函数可以被其他应用重复利用,省去了程序员重新编写的代价。
然而,使用shorterstring 函数也存在一个潜在的缺点:
- 调用函数一般比求等价表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。
内联函数可避免函数调用的开销
将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。假设我们把shorterstring 函数定义成内联函数,则如下调用
cout << shorterstring(s1, s2) <<endl;
将在编译过程中展开成类似于下面的形式cout<<( (s1.size() < s2.size() ? s1 : s2)<<endl;
从而消除了shorterString函数的运行时开销。
在shorterstring函数的返回类型前面加上关键字inline,这样就可以将它声明成内联函数了:
1 | //内联版本:寻找两个string对象中较短的那个 |
内联说明知识向编译器发生的一个请求,编译器可以选择忽略。
constexpr函数
是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:
1 | constexpr int new_sz() { return 42;} |
执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
我们允许constexpr函数的返回值并非一个常量:
1 | //如果arg是常量表达式,则scale (arg)也是常量表达式 |
当scale的实参是常量表达式时,它的返回值也是常量表达式;反之则不然:
1 | int arr[scale(2)]; //正确:scale (2)是常量表达式 |
如果非常亮表达式调用scale函数,当scale函数用在需要常量表达式时,会报错。
与const的比较
const并不能代表“常量”,它仅仅是对变量的一个修饰,告诉编译器这个变量只能被初始化,且不能被直接修改(实际上可以通过堆栈溢出等方式修改)。
constexpr可以用来修饰变量、函数、构造函数。一旦以上任何元素被constexpr修饰,那么等于说是告诉编译器 “请大胆地将我看成编译时就能得出常量值的表达式去优化我”。
把内联函数和constexpr函数放在头文件内
调试帮助
基本思想是,程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和 NDEBUG。
assert预处理宏
assert ( expr);
首先对expr求值,如果表达式为假(即 0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。头文件为cassert。
assert宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阈值。此时,程序可以包含一条如下所示的语句:assert(word . size() > threshold);
NDEBUG预处理变量
assert 的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
我们可以使用一个#define语句定义NDEBUG,从而关闭调试状态。
此外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef和#endif之间的代码;如果定义了NDEBUG,这些代码将被忽略掉:
1 | void print (const int ia[], size_t size){ |
调试函数名 | 含义 |
---|---|
_ _FILE_ _ |
存放文件名的字符串字面值 |
_ _func_ _ |
存放调试函数的名字 |
_ _LINE_ _ |
存放当前行号的整型字面值 |
_ _TIME_ _ |
存放文件编译时间的字符串字面值 |
_ _DATE_ _ |
存放文件编译日期的字符串字面值 |
1 | if (word.size () < threshold) |
则可得到下面的错误
1 | Error : wdebug.cc : in function main at line 27 |
函数匹配
1 | void f(); |
确定候选函数和可行函数
- 匹配第一步调用对应的重载函数集,集合中函数称为候选函数。具有两个特征,1:函数名相同。2:声明在调用点可见。上面的例子中,四个均为候选函数。
- 第二部考察实参,从候选函数选出能被这组实参调用的函数,成为可行函数,也有两个特征,1:形参,实参数量相等,2:类型对应相等,或者能够互相转化。
则这里只有函数2和4可以调用,但如果找不到可行函数,会报错。
寻找最佳匹配
基本思想:实参类型与形参类型越接近,它们匹配得越好。所以例子中硬调用双double的函数。
多个形参的函数匹配
若调用为(42,2.56)
,则第一个参数与f(int,int)匹配,第二参数与f(double, double)匹配。这样会报错,因为二义性调用。调用重载避免强制类型转换。
实参类型转换
精确匹配,包括以下情况:
·实参类型和形参类型相同。
实参从数组类型或函数类型转换成对应的指针类型。
向实参添加顶层const或者从实参中删除顶层const。
通过const转换实现的匹配。
通过类型提升实现的匹配。
通过算术类型转换(参见4.11.1节,第142页)或指针转换实现的匹配。
通过类类型转换实现的匹配(参见14.9节,第514页,将详细介绍这种转换)。
需要类型提升和算术类型转换的匹配
分析函数调用前,我们应该知道小整型一般都会提升到int类型或更大的整数类型。
假设有两个函数,一个接受int、另一个接受short,则只有当调用提供的是short类型的值时才会选择short版本的函数。有时候,即使实参是一个很小的整数值,也会直接将它提升成int类型;此时使用short版本反而会导致类型转换,如char。
所有转换级别一样:
1 | void manip (long); |
函数匹配和const实参
如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。指针类型也同样如此
1 | Record lookup (Account&); //函数的参数是Account的引用 |
函数指针
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:
1 | //比较两个string对象的长度 |
*pf两端的括号必不可少。如果不写这对括号,则pf是一个返回值为bool指针的函数:
使用函数指针
初始化与使用:
1 | pf = nullptr; //不指向任何函数 |
函数指针不存在转换规则,必须精确匹配,否则报错。
函数指针形参
函数形参可以时指向函数的形参。
1 | //第三个形参是函数类型,它会自动地转换成指向函数的指针 |
使用函数也会自动转换成指针
1 | //自动将函数lengthCompare转换成指向该函数的指针 |
简化方法:
1 | // Func和Func2是函数类型 |
注意这里typedef的用法,它这里定义了Func表示一类返回值为bool,形参为(/**/)的函数。
返回指向函数的指针
起类型别名将函数返回
1 | using F = int (int* , int); // F是函数类型,不是指针 |
函数类型需要强制进行转化为函数指针类型:
1 | PF f1(int); //正确:PF是指向函数的指针,f1返回指向函数的指针 |
也可直接声明,但要麻烦得多:
1 | int (*f1 (int) ) (int*, int) ; |
auto和decltype
decltype(函数名),可以返回对应的函数类型,加*
可以转化为函数指针。