目录
- 函数是什么
- 函数的分类
- 函数参数
- 函数调用
- 函数的嵌套调用和链式访问
- 函数的声明和定义
- 函数递归
1.函数是什么
在数学中,像y=2x+1的式子叫函数。c语言中的函数类似数学中的函数,维基百科中对函数的定义:子程序
1.在计算机科学中,子程序(英语: Subroutine, procedure, function, routine, method,subprogram, callable unit ),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。2.一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
2.函数的分类
函数分为两类,一种是库函数,另一种是自定义函数
1.库函数
为什么要有库函数呢?我们平常敲代码时,总是要频繁的使用某个功能。因此,c语言中把常用的功能进行了封装,变成一个个函数,提供给大家使用。比如:
- scanf
- printf
- strlen
- strcmp
- rand
- srand
但是c语言并不去直接实现库函数,而是提供了c语言的标准和库函数的约定。比如 scanf(输入)、printf (输出),c语言规定了这些库函数的功能、名字、参数和返回值,但是库函数的实现一般是有编译器去实现的,例如VS2022、gcc ,不同的编译器去实现的方式略有差异。
进入这个网站,假设我们想找库函数scanf,由于这个是新的版本,不支持搜索,不方便我们查找要找的函数,可以点击右上方红线标记部分,进入这个页面
搜索scanf,我们就可以看到scanf函数相关的信息
常用的库函数有:
使用库函数的一个例子:strcpy
从中可以了解,char *destination 是个指针,它的内容将被覆盖,也就是说把 char * source 的内容放到 char *destination去
库函数还有很多,我们在学习库函数的时候不是生硬的背下来,而是在编写代码的过程中有需要的库函数可以利用上面的网站搜索,反复使用,自然就记住了。
注意:使用库函数时必须要有对应的头文件。
2.自定义函数
自定义函数与库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计,给我们有很大的发挥空间。
函数的组成:
我们不妨举个例子:写个函数找出两个数的最大值
再举个例子,写个函数交换两个整型变量的内容
当我们这样写代码时,没有出现我们要的效果。这说明代码有bug,虽然可以运行,语法也没问题,但结果是错误的,也叫运行时错误。
我们可以调试看看:
发现x和y交换了,但是a和b 并没有交换。这是因为当形参传递给实参的时候,形参是实参的一份临时拷贝,所以形参的修改不会影响实参。
那么正确的是什么样的?这里我们要用到指针的知识。
通过指针把a和b的地址传给*px和*py,让*px和*py找到a和b的值,并交换。注意:交换的是变量的值,没有交换地址。就好比两个空房子,找到两个空房子,交换两个房子里面的东西,而不是交换门牌号。
3.函数参数
3.1.形式参数(形参):
3.2.实际参数(实参):
像上面例子中的x,y,px,py都是形式参数,a,b是实际参数。所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
什么情况需要传递地址呢?
上面找两个数最大值的例子中,把a和b的值传过去,求a和b的较大值和求x和y的较大值的结果是一样的,所以不用传地址。在交换两个数的例子中,把a和b的值传过去给x和y,但这时形参与实参的交换不是一体的,形参的修改不会影响实参,在函数内部要改变外部的a和b的值,要进行联系,就需要传递地址,来改变外部a和b的值。
4.函数调用
4.1.传值调用
4.2.传址调用
5. 函数的嵌套调用和链式访问
5.1.嵌套调用
函数与函数之间可以根据实际情况进行组合,叫做嵌套调用。
函数的嵌套调用就好比制造一辆汽车, 分成几个模块来做,效率更高。
5.2.链式访问
一个题目:
我们可以先去上面介绍的网站查下printf函数返回的是什么。
也就是说打印多少个字符,就返回多少个。在题首先打印的是43,然后43是两个字符;接着打印的是2;最后2为一个字符,结果打印的是1;所以最终的答案为4321。注意:如果在每个%d后面都加上空格,答案就不一样了。空格也算一个字符,所以首先打印的是43空格;43空格有3个字符,所以接着打印的是3空格;3空格有2个字符,所以打印的是2空格;因此最终结果为43 3 2
6.函数的声明和定义
6.1.函数声明
6.2.函数定义
平时我们编写代码时,都是直接把定义放在前面,定义也是一种特殊的声明。
一般情况下,函数的声明是放在头文件的。
好处:未来在公司写代码需要团队协作,要分模块写,最后整合,总不能每个人都挤在一个文件里写代码。同时可以把代码的实现和声明分离,
7.函数递归
7.1什么是递归
一个简单的函数递归例子:
我们发现函数死递归了,导致栈溢出。如果要让函数的递归有意义,就要有限制条件。
7.2递归的两个必要条件
-
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
-
每次递归调用之后越来越接近这个限制条件。
练习1:接受一个整型值(无符号),按照顺序打印它的每一位,比如:输入1234,输出1 2 3 4
分析:
test(1234) ——把每一位打印出来
test(123) 4
test(12) 3
test(1) 2
代码示例:
练习2:编写函数不允许创建临时变量,求字符串的长度。
如果第一个字符就是\0,长度就为0,假设一个字符串abc,用递归的思想a不是\0,有1+test("bc");b不是\0,有1+1+test("c");c不是\0,有1+1+1+test("")
所以有1+1+1+0=3
代码示例:
7.3.递归与迭代
迭代是指重复一件事,循环也是迭代的一种。
练习3:求n的阶乘。(不考虑溢出)
练习4:求第n个斐波那契数。 (不考虑溢出)
斐波那契数:1 1 2 3 5 8 13 21 34 55......(前两个数的和是第三个数)
用递归的方式:
但是我们输入50时,发现计算很长时间还没算出结果,因为在其中有大量重复的计算。
在一些情况下,递归可能不太适合,用迭代效率更高一些。
前两个数赋值为1,当x=3时,计算1次;x=4时,计算两次;依次类推...所以计算的是x-2次
当我们输入50时,结果虽然是错误的,但速度很快。
递归或者迭代都是好方法,但关键是在适合的情景下实现才行。