【C语言】函数(2)
函数的嵌套使用和链式访问
嵌套使用
1 | //嵌套使用 |
链式访问
把一个函数的返回值作为另一个函数的参数
1 | //链式访问 |
请问以下代码打印输出什么?
1 | printf("%d",printf("%d", printf("%d",43))); |
答案:
因为printf()
函数返回值是打印在屏幕上字符的个数。
函数先打印 43 到屏幕上,返回值为打印在屏幕上字符的个数,是2
然后下一个函数打印 2 到屏幕上,返回值为1,然后函数打印输出 1
函数的声明和定义
函数声明:
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
函数定义:
函数的定义是指函数的具体实现,交待函数的功能实现。
先看代码
1 | int main() |
这个函数能否运行?
答案:
在编译器执行代码的时候,是从上到下的
它先执行了主函数main
,在里面发现了一个Add
函数,这个Add
函数在之前并没有见过
所以他会报一个警告

所以我们需要告诉编译器,有Add
这个函数,这就是函数的声明
1 | int main() |
当然,如果你的函数定义,放在你的函数调用前面,就不会有这种问题了
因为编译器在调用你这个函数前,就已经见过这个函数了
换一种说法,函数的定义也是一种声明
用函数写一个加减乘除的计算器
前面我们说到函数的声明和定义,但在函数的声明中有一点值得注意
函数的声明一般要放在头文件中的
所以在正常的代码编写的情况下
我们应该新建一个头文件,把加减乘除函数的声明放在头文件里,加减乘除函数的实现和主函数放在源文件里
这里演示一下加法函数的代码编写
先新建一个头文件add.h
,声明加法函数
1 | int Add(int x, int y); |
再新建一个add.c
的源文件,实现加法函数
1 | int Add(int x, int y) |
然后在主函数源文件下,引用头文件add.h
,并调用加法函数
1 |
|
这样就完成了加法代码的编写
函数递归
什么是递归
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
递归的两个必要条件:
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
例子:
1 | int main() //错误例子 |
函数自己调用自己,这就是一个递归,不过以上这个递归是错误的,是一个死递归 ,他会导致栈溢出。
扩展:
前面在讲
static
的时候,有提到栈区里面存储了局部变量,函数的参数而堆区是用于动态内存分配,还有静态区存储全局变量,static修饰的静态变量
那什么是栈溢出呢?
当我们执行代码时,每调用一个函数,就要给函数分配一个空间,这个空间就叫做栈帧空间
栈帧空间内,用于放置该函数的变量
当你递归函数,一次又一次的进行调用函数,就会一直给函数分配空间,直到栈空间不够,就会导致栈溢出
所以请注意:
1.在写递归的时候,要有跳出条件,每次递归逼近跳出条件,不能死递归
2.递归层次不能太深,不然会导致栈溢出
例题一
接受一个整型值(无符号),按照顺序打印它的每一位。
例如: 输入:1234,输出 1 2 3 4.
1 | //请注意print为自定义函数,不是库函数打印输出 |
1234 % 10 = 4
1234 / 10 % 10 = 3
123 / 10 % 10 = 2
12 / 10 % 10 = 1
我们输入一个数1234
之后,调用了函数print
这时n = 1234,n>9进入判断语句,调用自身函数print
进入到了print(1)
中,这时n= 123 ,n>9,再次调用自身函数print
进入到print(2)
中,这时n=12,n>9,再次调用自身函数print
进入到print(3)
中,这时n = 1,n<9,直接打印输出 1 ,结束后返回上次调用函数的位置,即print(2)
然后执行print(2)
中的打印输出 2,结束后返回上次调用函数的位置
执行print(1)
中的打印输出 3,结束后返回上次调用函数的位置
执行print
中的打印输出 4,结束函数
例题二
编写函数不允许创建临时变量,求字符串的长度。
不使用strlen()
函数,模拟strlen()
的实现
1 | int string(char* a) |
我们定义一个字符数组,当数组传参时,传过去的是一个首元素的地址
所以,函数形参使用指针来存储这个地址
要从指针中获得这个数组的字符,需要用到指针变量*a
,而*a
是获得字符数组的首个字符的
那么如何拿到下一个字符呢,用地址+1
就可以拿到下一个字符的地址,即a+1
因为要计算的是数组的长度,所以1 + string(a + 1)
,前面这个1
就是表示当前字符的长度为 1
而数组的结束标志是一个\0
,当*a == '\0'
的时候,也就说明数组结束,而这个\0
并不是数组的元素,所以返回值为 0
最后就是 1+1+1+0 = 3,返回给主函数中调用的string(arr)
,打印输出 3
递归与迭代
例题一
求n的阶乘。(不考虑溢出)
递归:
1 | int factorial(int n) |
迭代:
1 | //迭代 |
例题二
求第n个斐波那契数。(不考虑溢出)
递归:
1 | //递归 |
迭代:
1 | //迭代 |
我们可以发现,在使用递归求第n个斐波那契数时,如果数值过大,比如40,它的计算速度是非常慢的,效率很低,因为它需要大量的调用函数,并且还容易导致栈溢出
而使用迭代的方法,计算速度会快很多,因为它只需要循环就可以了
所以在解决问题是,如果可以递归实现功能,且递归代码简洁,无缺陷(栈溢出或代码运行效率低下),就使用递归,反之,使用非递归的方法。