一、函数是什么?
相信我们第一次接触函数是在学习数学的时候,比如:一次函数 y = kx + b ,k 和 b 都是常数,给一个任意的 x ,就会得到一个 y 值。
其实在 C++ 语言中就引入了函数(function)的概念,有些翻译为:子程序(子程序这种翻译更加准确一些)。
函数就是一个完成某项特定的任务的一小段代码,这段代码是有特殊的写法和调用方法的。
我们在前面学习的时候,就已经见过和接触过函数了,比如:main 函数、scanf 函数、printf 函数、pow 函数、sqrt 函数等等。
C++ 语言程序是由若干个小的函数组合而成的,也可以这样说:一个大的计算任务可以分解成若干个较小的函数(对应较小的任务)完成。同时一个函数如果能完成某项特定任务的话,这个函数也是可以复用的,这显著提高了软件开发的效率。
函数的好处:
(1)模块化开发,一个大的功能,总能拆分成各种子功能,每个子功能都可以设计成一个函数,每个函数可以作为一个独立的模块存在,这样程序的逻辑会更加清晰,逻辑关系会更加明确。
(2)代码可以复用,只要根据需要定义出一个函数,需要用到这个功能的地方,直接调用函数就行,这显著降低了代码的冗余,并提高了开发效率。
(3)方便多个程序员之间协作开发,方便程序的多个模块之间的互相交互。
(4)熟悉函数的使用之后,代码的编写、阅读、调试、维护都会变得更加容易。
二、函数的分类
在 C++ 中,函数一般分为库函数和自定义函数。
1、库函数
前面内容中学到的 printf 、 scanf 都是库函数。库函数是标准库中提供的现成的函数,我们只要学会函数的功能,就可以直接使用。有了库函数,一些常见的功能就不需要程序员自己实现了,一定程度上提升了效率;同时库函数的质量和执行效率上都是更有保证的。
编译器的标准库中提供了一系列的库函数,这些库函数根据功能的划分,在不同的头文件中进行了声明。
2、自定义函数
在了解库函数之后,我们可以知道其实库函数的功能是有限的,在实际开发过程中还是得根据需要将代码封装成自定义的函数。自定义的函数就是自己设计和实现的函数。
(1)函数的语法形式
ret_type fun_name(形式参数)
{
}
1)ret_type 是函数计算结果的类型,有时候返回类型可以是 void ,表示什么都不返回。
2)fun_name 是函数名,函数有了名字就方便调用,所以函数名尽量要根据函数的功能来起,要起的有意义。
3)函数的参数就相当于工厂加工需要的原材料,函数的参数可以是 void ,明确表示函数没有参数。如果有参数,多个参数用逗号隔开,每个参数要分别交代清楚参数的类型和名字。
4){} 括起来的部分被称为函数体,函数体就是完成计算的过程。
(2)函数定义
函数的实现就是函数的定义。
函数的参数部分需要交代清楚:参数个数,每个参数的类型是啥,形参的名字叫啥。
以定义一个加法函数为例:
给加法函数取名: Add ,函数 Add 需要接收2个整型类型的参数,函数计算的结果是整型,即返回一个整型。
三、函数参数和返回值
1、实参和形参
实际上,在函数定义和使用的过程中,函数的参数被分为两种:
1)实际参数,简称实参
2)形式参数,简称形参
(1)实参
实际参数就是真实传递给函数的参数。
(2)形参
形式参数没有使用的时候是不会向内存申请空间的,不是真实存在的。形式参数只有在函数被调用的过程中为了存放实参传递过来的值,才会向内存申请空间,这个过程就是形参的实例化。
(3)实参和形参的关系
形参是实参的一份临时拷贝。形参和实参各自是独立的内存空间。
2、函数传参
(1)数组做函数参数
在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。
我们需要知道的是:
1)函数的实参的名字和形参的名字可以相同,也可以不同;
2)函数的形式参数要和函数的实参个数匹配;
3)函数的实参是数组,形参也写成数组形式的;
4)形参如果是一维数组,数组大小可以省略不写;
5)形参如果是二维数组,行可以省略,但是列不能省略;
6)数组传参,形参是不会创建新的数组的;
7)形参操作的数组和实参的数组是同一个数组。
比如:
(2)字符串做函数参数
如果将字符串做函数参数,直接在形参的部分使用字符串来接收。这里的形参是实参的一份临时拷贝,对形参的修改不能影响实参。
比如:
(3)全局变量不用传参
全局变量的作用域很大,在整个程序中都可以使用,那么只要把变量、数组等定义成全局变量,如果要在函数中使用,就可以不用传参。当然,有时候将变量或者数组定义成全局的时候,是不能解决问题的,比如递归等场景,这时候,定义成全局变量反而会出现问题,所以就得考虑传参的问题。
3、返回值
(1)return 后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式的结果。函数返回的值,可以使用变量来接收,如果不需要这个返回值,也可以不接收。
(2)return 后边可以什么都没有,直接写 return; 这种写法适合函数返回类型是 void 的情况。
(3)return 返回的值和函数返回类型不一致,系统会自动将返回的值的类型隐式转换为函数的返回类型。
(4)如果函数中 return 语句被执行,则函数直接返回,即使函数中 return 语句后还有其他代码也不再执行。
四、函数的声明和调用
1、函数声明
(1)函数要满足先声明后使用。函数声明只需要在函数调用之前就可以,可以在全局声明也可以局部声明。
(2)函数的定义是一种特殊的声明。函数声明中的形参的变量名可以省略掉,只保留类型就可以(因为函数声明是不会使用这个函数的,所以不需要规定形参的名字)。
2、函数调用
函数调用的方式,一般会分为两类:传值调用和传址(引用)调用。
(1)传值调用
传值调用就是将实参的数据直接传递给形参。对形参的修改,不会影响实参。这种情况下参数传递的方式只能从实参到形参,也就是单向传递。
传值调用一般应用的场景是:仅需要通过传参的方式将实参的值传递给被调函数,被调函数就可以完成工作,而不需要改变实参的值。
(2)引用
1)引用概念
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量占用的是同一块内存空间。
引用使用格式:
类型 & 引用变量名 = 引用实体;
举例:
2)引用特性
1.引用在定义时必须初始化;
2.一个变量可以有多个引用;
3.引用一旦引用一个实体,再不能引用其他实体。
(3)传址(引用)调用
传引用调用方式的本质是将实参变量的地址传递给了形参,而形参使用指针直接找到实参来进行操作。
举例:
(4)传值、传引用效率比较
在采用传值调用的过程中,实参传递给形参的时候,形参会创建新的空间,再将实参的数据给形参拷贝一份。但是引用传参的方式,就不存在数据的拷贝,只是在形参的部分建立引用的关系,形参就是实参。
所以引用传参的效率要高于传值调用的方式。
五、函数重载
C++中的函数重载(Function Overloading)是指在同一个作用域中可以有多个同名函数, 它们的函数名称相同,但是参数列表不同。
函数返回类型 函数名(参数1, 参数2,...);
这里的 “不同” 指的是参数的数量、类型或顺序至少有一个不同。函数的返回类型并不影响函数的重载,因为 C++ 编译器不会根据返回类型来区分不同的函数。
举例:
六、递归
1、递归的概念
递归是一种在计算机科学中非常重要的编程技术,它可以简化许多复杂的问题。递归具体是指函数在定义的时候直接或间接调用自身的方式。
2、递归的思想
递归可以把一个大型复杂问题层层转化为一个与原问题相似,但规模较小的子问题来求解;直到子问题不能再被拆分,可以直接求解,递归就结束了。递归的思考方式就是把大事化小的过程。
递归中的递就是递推的意思,归就是回归的意思。
3、递归的必要条件
递归在书写的时候,有2个必要条件:
(1)递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
(2)每次递归调用之后都会越来越接近这个限制条件。
4、递归的举例
详见:
C语言函数递归-CSDN博客
5、递归与循环(迭代)的比较
首先我们需要知道:
在 C++ 中每一次函数调用,都需要为本次函数调用在内存的栈区,申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。
函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题。
我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,但是这些问题的迭代实现往往比递归实现效率更高。当一个问题非常复杂,难以使用迭代的方式实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。