一、函数的嵌套调用和链式访问
1.嵌套调用
函数的嵌套调用就是一个函数被另一个函数调用,实现多重调用。
观察如下代码:
#include <stdio.h> void repeate() { printf("repeating!\n"); } void repeate_three() { int i = 0; for (i = 0; i < 3; i++) { repeate(); } } int main() { repeate_three(); return 0; }
我们看到,repeate_three函数中使用for循环调用了三次repeate函数,最后的结果也是打印三次“repeating!”,这就是函数的嵌套调用。
注意:函数虽然能嵌套调用,但却不能嵌套定义。
如下形式的代码就是错误的:
#include <stdio.h> int main() { int repeate(int a, int b) { return a + b; } return 0; }
上面的代码把一个返回类型为整形的函数定义在main函数内,这在语法上是不行的!!!
2.链式访问
链式访问就是把一个函数的返回值作为另一个函数的参数。
2.1strlen()函数
我们以strlen()函数为例:
#include <stdio.h> #include <string.h> int main() { int len = strlen("abcdef"); printf("%d\n", len); printf("%d\n", strlen("abcdef")); return 0; }
strlen()函数返回的是字符串的个数,我们定义一个整型变量len接收它的返回值,结果为6,直接打印strlen(“abcdef”)结果也是6。
像这种把strlen()函数的返回值作为printf()函数的参数就是函数的链式访问。
2.2printf()函数
我们再看一个经典例子:
如果printf()函数作为返回值又多次被printf()函数打印,那会是什么样的结果呢?
我们看下面的代码:
#include <stdio.h> int main() { printf("%d\n", printf("%d", printf("%d", 15))); return 0; }
要想知道这段代码是如何执行的,我们就要了解printf()函数到底是怎样返回的,返回的究竟是什么。
printf()的返回类型是一个整型:
int printf ( const char * format, … );
可以看到,作为参数成功的话,返回的是printf打印的字符或数字的总的个数。
接下来我们画图来分析这段代码的打印结果:
首先printf(“%d”,15)先把15打印出来:
而右边第一个printf打印了2个数字,所以它的返回值就是2,接着在屏幕上打印,就是152:
从右边第二个printf只打印了一个数字2,所以它的返回值就是1,接着在屏幕上打印,就是1521:
而最终的结果就是1521:
二、函数的声明和定义
1.函数声明和定义的介绍
函数的定义就是函数的实现,函数的声明就是告诉编译器存在这个函数。
我们先来观察下面这段代码:
#include <stdio.h> int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); int ret = Add(a, b); printf("%d\n", ret); return 0; } int Add(int m, int n) { return m + n; }
我们的编译器是从上往下按顺序读取代码的,当在main函数中读取到Add函数时会报一个警告,因为它没有找到Add函数的定义。
如果在main函数之前加上一串代码:
int Add(int m, int n);
这样警告就会消除了,而这段代码就是函数的声明,如果调用函数之前没有定义该函数,就需要提前对该函数进行声明,来消除编译器的警告。
一般情况,如果直接将函数的定义放在main函数之前,我们就不需要再对函数进行声明了。
2.函数声明和定义的使用
我们在编写程序的时候一般不会将主函数和其他自定义的函数写在同一个文件中,这时我们会用到函数的声明和定义,我们还以Add函数为例:
我们将主函数独立放在一个test.c文件中;
将Add函数的声明放在一个Add.h文件中;
将Add函数的定义放在一个Add.c文件中;
Add.c:
int Add(int m, int n) { return m + n; }
Add.h:
int Add(int m, int n);
test.c:
#include <stdio.h> #include "Add.h" int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); int ret = Add(a, b); printf("%d\n", ret); return 0; }
这就是函数的声明和定义的一般使用,我们一般是将函数的声明放在.h的文件中,在使用时需要包含这个.h文件,而我们在包含时用的是双引号,与库函数的引用尖括号有所区别。
这样写的好处是在我们在实现一个功能复杂的程序的时候,我们可以将这个功能拆开分成不同的步骤,把每个要实现的步骤交给不同的人,让多个人来完成一个程序的实现,这样就形成了一个模块化实现程序的样例,每个人都写一个.c和.h的文件来实现某一步的程序,最后将所有人的.h文件放在一个.h文件中,所有的.c文件放在一个.c文件中,最终一个复杂的程序被分为几个步骤由不同的人来完成。
三、变量的声明和定义
变量的声明和定义其实和函数是一样的,只是函数更加普遍一些。
如下一段代码:
#include <stdio.h> int main() { printf("%d\n", value); return 0; } int value = 2022;
这里同样会报一个警告,告诉我们value未定义,所以我们同样要将定义或声明放在main函数之前。
这里我们再扩展一个小知识:
如下代码:
#include <stdio.h> int value; int main() { printf("%d\n", value); return 0; }
这里我们将value作为全局变量,像这种声明但却没有定义的写法,就是将声明作为定义来看,它没有被赋值,默认为0。
四、函数递归
1.什么是递归
程序调用自身的编程技巧称为递归,递归就是自己调用自己,作为一种算法在程序设计语言中广泛应用,通常把一个大型复杂的问题转换成一个与原问题相似的规模较小的问题来求解,只需少量的程序就可描述出解题过程的多次计算,减少了程序的代码量,递归就是把大事化小。
递归:
递:递推
归:回归
我们观察如下简单递归代码:
#include <stdio.h> int main() { printf("hello!\n"); main(); return 0; }
可以看到,这是在main函数内又调用了main函数,而调用的main函数内又有main函数,如此重复调用下去,就成了一个递归函数,它会死循环的打印hello!,最终导致程序崩溃,所以它是个错误的递归。
而当程序崩溃后,我们通过监视可以看到下面的一条信息:
这里程序告诉我们的异常是Stack overflow,也就是栈溢出。
为什么会有栈溢出呢,我们要知道,在内存中,每调用一次函数,都会为本次函数,在内存的栈区上开辟一块内存空间,当栈区的内存空间被用完的时候,再想往栈区开辟空间,就会出现栈溢出的现象。
2.递归的两个必要条件
1.必须存在限制条件,当不满足这个条件的时候,递归结束
2.每次递归调用后都必须接近这个限制条件
2.1接收一个整型(无符号),按顺序打印它的每一位
如:输入1234
打印:1 2 3 4
#include <stdio.h> void Print(unsigned int m) { while (m) { printf("%d ", m % 10); m /= 10; } } int main() { unsigned int num = 0; scanf("%u", &num); Print(num); printf("\n"); return 0; }
结果为:
我们观察到,当输入的值为1234的时候,将1234%10得到的值为4,打印出来,然后再通过1234/10得到的值为123,将这两步放入while循环中,当传入的值为0的时候循环结束,但由此打印出来的结果是4 3 2 1,并不符合题意按顺序打印,所以我们需要改变一下思路,通过递归的方式如下:
#include <stdio.h> void Print(unsigned int m) { if (m > 9)//限制条件 { Print(m / 10);//打印123 } printf("%d ", m % 10);//打印4 } int main() { unsigned int num = 0; scanf("%u", &num); Print(num); printf("\n"); return 0; }
代码结果:
它的实现思路其实就是通过递归先完成高位的打印,然后一步步按顺序打印出来:
它的每次调用就是一次递推,而每次返回的值就是一次回归,这样看来递归确实用少量的代码就实现了复杂的问题。
2.2不创建临时变量,求字符串长度
我们知道有一个库函数可以实现求字符串长度,也就是strlen()函数,我们先忽略“不创建临时变量”,只看“求字符串长度”,这就是要求我们模拟实现strlen()函数,要想模拟实现strlen()函数,就要了解它是如何求出字符串长度的:
我们要知道在一个字符数组中,数组名就是字符数组首元素地址,而一个字符串是以/0结尾的,这时我们就可以定义一个整型变量count,先让一个指针指向字符串的首元素,解引用这个指针并判断是否为\0,如果不是\0,,count自增1,指针也自增1,当解引用为\0的时候,返回count的值就是字符串的长度。
模拟strlen()代码:
#include <stdio.h> int my_strlen(char* s) { int count = 0; while (*s != '\0') { count++; s++; } return count; } int main() { char arr[] = "hello"; int len = my_strlen(arr); printf("%d\n", len); return 0; }
我们虽然完成strlen()函数的模拟实现,但却用到了临时变量count,那么如何对这段代码进行改进,不用临时变量,也能实现求字符串的长度呢?
我们可以用到递归的思想,可以想到:
假设求一个字符串"abcd"
也就是my_strlen(“abcd”),只要第一个字符不是‘\0’,那么它的长度至少为1
于是:
len=1+my_strlen(“abc”)
len=1+1+my_strlen(“ab”)
len=1+1+1+my_strlen(“a”)
len=1+1+1+1+my_strlen(“\0”)
len=1+1+1+1+0=4
代码实现:
#include <stdio.h> int my_strlen(char* s) { if (*s != '\0') return 1 + my_strlen(s + 1); else return 0; } int main() { char arr[] = "hello"; int len = my_strlen(arr); printf("%d\n", len); return 0; }
在这里我们扩充一个小知识:
char类型的指针+1,每次跳过一个字符类型,也就是一个字节
int类型的指针+1,每次跳过一个整型,也就是四个字节
这里巧妙地用了递归的方式在不创建临时变量的前提下完成了求字符串长度。
两次的执行结果均一样:
3.递归与迭代
迭代就是使用非递归的方式
3.1求n的阶乘
递归方式:
#include <stdio.h> int Fac(int n) { if (n <= 1) { return 1; } else { return n * Fac(n - 1); } } int main() { int num = 0; scanf("%d", &num); int ret = Fac(num); printf("%d\n", ret); return 0; }
求n的阶乘,可以看成是求n乘以(n-1)的阶乘,而n-1的阶乘又可以看成n-1乘以(n-2)的阶乘,这就可以很好的运用递归思路,当n为1时返回1,否则就调用n*(n-1)的方式。
递归在有公式的时候是很容易想出来的,同样我们也可以通过非递归的方式来实现,
非递归方式:
#include <stdio.h> int Fac(int n) { int i = 0; int ret = 1; for (i = 1; i <= n; i++) { ret *= i; } return ret; } int main() { int num = 0; scanf("%d", &num); int ret = Fac(num); printf("%d\n", ret); return 0; }
可以看出,通过一段for循环也能很容易的实现阶乘的求法。
最终结果:
当num=3时,他们的结果均为6:
这里要注意一点:在使用递归和迭代的方式来实现时都会有一个缺陷,如果求比较大的数的阶乘,比如1000,在非递归方式中,由于1000的阶乘太大,整型变量ret接收不了这么大的数字,最终会返回0。在递归的方式中,最终结果也是0,并且程序会因为栈溢出而崩溃,这两种方式虽然最后结果都为0,但非递归方式并没有崩溃。
这样看来,我们在使用递归时还要谨慎使用,尽量能不用递归就不用递归。
3.2求第n个斐波那契数
1 1 2 3 5 8 13…
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)在现代物理、准晶体结构、化学等领域,斐波那契数列都有直接的应用,为此,美国数学会从 1963 年起出版了以《斐波那契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。
代码如下:
#include <stdio.h> int Fib(int n) { if (n <= 2) { return 1; } else { return Fib(n - 1) + Fib(n - 2); } } int main() { int num = 0; scanf("%d", &num); int ret = Fib(num); printf("%d\n", ret); return 0; }
利用递归的方式看起来非常简单,可是它的效率却不高,原因是它会有很多重复调用:
这样看来我们使用递归的方式来实现并不是最优的方式,我们对代码进一步优化:
我们使用循环的方式来实现:
新的思路是,我们先定义三个变量a,b,c
一组斐波那契数:1 1 2 3 5 8 13 21 34 55…
第一步:a=1,b=1,c=a+b
第二步:a=b,b=c,c=a+b
依此一直循环最后求出
最终代码:
#include <stdio.h> int Fib(int n) { int a = 1; int b = 1; int c = 1; while (n > 2) { c = a + b; a = b; b = c; n--; } return c; } int main() { int num = 0; scanf("%d", &num); int ret = Fib(num); printf("%d\n", ret); return 0; }
所以再次强调,能不用递归就不用递归,有时递归的方式会存在缺陷(栈溢出)和效率低的问题。
程序结果:
4.解决递归时的栈溢出问题
当我们用递归写出一个程序时,有时接受的数据较大,在执行递归的时候会出现栈溢出(Stack overflow)的现象,这时我们往往通过以下两种方式解决:
1.将递归改成非递归,有些代码用递归写起来可读性更高,但用非递归的写法也能够实现,在递归出现问题时,可以想出一个非递归的方式。
2.使用static对象替代局部对象,,这样不仅可以减少每次递归调用和返回时产生和释放局部对象的开销,而且static还可以保存递归调用的中间状态,并且可被各个调用层访问。