目录
七、函数的定义与声明
7.1 函数声明
经过函数(一)部分的学习,我们应该基本了解了C语言中函数的基本用法。因此,我们可以写一个程序,使用函数计算两个输入值的和,代码如下:
#include<stdio.h>
int main()
{
int num1 = 0;
int num2 = 0;
scanf("%d %d", &num1, &num2);
int ret = Add(num1, num2);
printf("%d\n", ret);
return 0;
}
int Add(int x, int y)
{
return x + y;
}
可是,这样书写并运行以后发现,出现了警告,内容是“Add未定义”。
实际上,在对代码进行编译时,代码是从前往后扫描的。在扫描时看到主函数main后,程序会直接进入主函数中,在遇到Add函数调用时出现了问题,因为编译器在从上往下扫描时并没有扫描到Add函数的定义。虽然事实上后边有对Add函数的定义,强行运行也可以得到想要的结果,可是在调用之前确实没有告诉编译器Add函数,所以编译器报错。
因此,需要在自定义函数调用之前对其进行声明。如图所示,在1、2位置处均可。
函数声明就是把函数的名字、函数类型以及形参类型、个数和顺序通知给编译器,至于函数是如何定义的,则通过3位置获取。
函数的定义本来就是一种特殊的声明,整体要满足先声明后使用的原则,因此,代码就改为我们最常见的形式:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int num1 = 0;
int num2 = 0;
scanf("%d %d", &num1, &num2);
int ret = Add(num1, num2);
printf("%d\n", ret);
return 0;
}
综上所述,函数声明:
1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么,但具体内容靠函数声明是决定不了的。
2. 一般在函数使用之前,要满足先声明后使用的原则。
3.函数声明一般要放在头文件中。
7.2 函数定义
函数的定义是指函数的具体实现,交代函数的功能实现,比如上图程序中3表示函数定义。
未来在工程中,代码是比较多的,函数一般是放在.h文件中声明,在.c文件中定义。
函数是具有外部链接属性的。
其中,需要注意的是:自定义函数的头文件使用双引号“”,标准库头文件使用尖括号<>。
八、函数递归
8.1 递归
程序调用自身的编程技巧称为递归。递归作为一种算法,在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或简介调用自身的一种方法,通常把一个大型复杂问题层层转化为一个与原问题相似的规模较小的问题来求解。递归策略只需要少量的程序就可描述出解题过程所需要的多次重复计算,大大减少了程序代码量。
递归的主要思考方式在于:把大事化小。
函数递归的例子如下:
#include<stdio.h>
int main()
{
printf("hehe\n");
main();//自己调用自己 // 但代码最终会崩掉
return 0;
}
我们通过调试发现,程序崩溃的原因是栈溢出。
8.2 递归的两个必要条件
递归在书写时有两个必要条件:
1. 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2. 每次递归调用之后越来越接近这个限制条件。
8.3 关于递归的举例认识
通过字面意思,我们大致了解了什么是递归,以及递归的两个必要条件,接下来我们将通过举例子进一步认识递归。
举例1:
接收一个整型值,按照顺序打印它的每一位,如1234,打印1 2 3 4。
比如接收整数位1234,那么肯定是个位数字4最容易获取:
1234 % 10 = 4;
1234 / 10 = 123;
123 % 10 = 3;
123 / 10 = 12;
12 % 10 = 2;
12 / 10 = 1。
如果我们采用循环语句的形式:
#include<stdio.h>
int main()
{
int num = 0;
scanf("%d", &num);
while(num)
{
printf("%d ", num % 10);
num = num / 10;
}
return 0;
}
运行结果为倒叙打印,输出4 3 2 1。
由上述分析可知,我们将1234通过运算逐步化简,从而属于把大事化小的范畴,那么我们可以采用递归的思路进行代码编写。
#include<stdio.h>
/*void Print(int num)
{
if(num > 0 && num < 9)
{
printf("%d ", num);
return ;
}
Print(num / 10);
printf("%d ", num % 10);
}*/
void Print(int n)
{
if (n > 9)
{
Print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int num = 0;
scanf("%d", &num);
Print(num);
return 0;
}
那么,这个代码是怎样执行的呢?
首先我们知道,递归的字面意思就是递推+回归。因此,我们可以画图来便于理解。
函数之所以能被调用,能实现递归,是因为函数在调用的时候会维护一个函数栈帧(内存上的一块区域)。函数调用开始,函数栈帧创建,函数调用结束后,栈帧销毁。
举例2:
编写函数,不创建临时变量,求字符串长度。
求字符串长度,我们常有strlen函数。
#include<stdio.h>
int main()
{
char arr[] = "abc";
size_t len = strlen(arr);
printf("%zd\n", len);//3
return 0;
}
1. 库函数strlen的返回类型是size_t,size_t 是一种类型,是无符号整型的。
2. size_t就是为sizeof设计的,因为大小不为负数。
3. size_t类型的数据打印的时候,VS指定格式使用%zd。
模拟实现strlen(strlen统计的是\0之前的字符个数):
由上图可知,如果*str指向的内容不是\0,那么字符串长度就是1加上后边字符串的长度,依次类推。
#include<stdio.h>
size_t my_strlen(char* str)//传过来的是首字符的地址,因此用char*
{
if (*str == '\0')
{
return 0;
}
return 1 + my_strlen(str + 1);//str++不行,因为str++是先使用str,把str传递过去以后再自加1。虽然++str可以,但在递归中给函数传参时,尽量不要用带复合作用的代码。
}
int main()
{
char arr[] = "abc";
size_t len = my_strlen(arr);
printf("%zd\n", len);
return 0;
}
九、递归与迭代
举例1:
我们写一个程序,计算n的阶乘。
5! = 1*2*3*4*5
4! = 1*2*3*4
...
5! = 5*4!
...
n! = n*(n-1)!
#include<stdio.h>
int my_Fac(int x)
{
if (x = 1)
{
return 1;
}
else
{
return x * my_Fac(x - 1);
}
}
int main()
{
int N = 0;
scanf("%d", &N);
int r = my_Fac(N);
printf("%d\n", r);
return 0;
}
栈的空间是有限的,而每一次调用,都会在栈上开辟一块空间。如果递归层次太深,程序会一直往下递推,而不会向上逐层返回释放栈空间,因此就有可能在递归的过程中出现栈空间不足的情况,在这种情况下,程序运行就会出现问题,这种现象称为栈溢出(stack overflow)。
为避免这种现象,我们将采用迭代的方式。迭代就是重复执行运算步骤,每重复一次过程就是一次迭代,而迭代的结果就是下一次迭代的初始值,类似于循环,因此,上述程序可以改写为:
#include<stdio.h>
int my_Fac(int x)
{
int i = 0;
int r = 1;
for(i = 1; i <= x; i++)
{
r = r * i;
}
return r;
}
int main()
{
int N = 0;
scanf("%d", &N);
int r = my_Fac(N);
printf("%d\n", r);
return 0;
}
因此可以说,递归与迭代之间是可以相互转化的。
举例2:
求第n个斐波那契数。
1 1 2 3 5 8 13 21 34 55 ...
看到斐波那契数的表达式以后,我们很容易想到,这样表达式简直就是明示递归了吧,代码如下:
#include<stdio.h>
int Fib(int n)
{
if(n <= 2)
{
return 1;
}
return Fib(n-1) + Fib(n-2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d", ret);
return 0;
}
当输入n=10时,输出结果为55,答案是正确的。那我们就想看看后边计算的斐波那契数是多少,于是,我们输入n=50,此时运行代码会发现,光标一直闪烁,计算所花费的时间明显变长了许多,效率变的很低。
究其原因,我们发现,当前代码在运行过程中,虽然递归的层次不深,n=50,递归深度为50层,但是要经历大量的重复运算,比如:
所以,看似使用递归写法的斐波那契数列并不适合采用递归的程序进行计算,因此,我们转用迭代的形式。
#include<stdio.h>
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;//满足了当n = 1, n = 2时,直接输出1
while (n >= 3)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int r = Fib(n);
printf("%d\n", r);
return 0;
}
采用迭代的方式,效率明显增加了。
综上所述:
1. 递归和迭代之间是存在相互转化的可能性。
2. 如果使用递归很容易想到,写出的代码没有明显的缺陷,那就可以使用递归
3. 但是如果写的递归代码有明显的问题,比如栈溢出、效率底下等,那我们还是要使用迭代的方式来解决问题