浅谈函数(下)
6. 函数的声明和定义
6.1 函数声明: 1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数 声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3. 函数的声明一般要放在头文件中的。
这里说add未定义
为什么说add未定义,这里我们就牵扯到代码的编译
例如这个test.c的文件到最后如何变成test.exe文件,中间要进行两个过程,先编译(从前往后一一扫描代码)但是这个代码中,从上到下扫描,扫到add的时候不知道add是什么,所以说add未定义。那我们想要正常使用add函数,就要告诉有这个add函数,所以就在函数使用之前声明,只告诉返回值类型,函数名,参数就行,之后函数无论在哪都可以识别到。之后再链接,得到.exe。函数的声明中,可以不带参数。
但是真正的企业化应用函数是分模块化来写
然后再创建头文件
这两个统称为加法模块
引入自己的模块化头文件用双引号。之后就能正常使用。
这样写可以多人协作,代码能更好的开发。
如果代码愿意让别人使用,不愿意让别人看见源码,可以这样实现。(因为有函数的声明,函数的返回类型,函数名,函数的参数,就可以知道函数怎么使用)但是因为头文件和源文件的分开,导致只知道怎么使用,不知道源码是什么。
如果不愿意暴露代码,就可以编译成静态库。
假设这是其它程序员写的add函数,现在你想使用add函数,但是别人让你看见源代码
点击属性
这里有静态库。之后点击应用,完了点击确定
之后对我们的代码进行生成
点击生成解决方案
之后生成了add.lib文件.
点击文件的debug查看
现在就出现了lib。这里使用记事本打卡.lib文件
看到的都是乱码,这里实际是二进制的信息,我们看不懂,这里编译的add.lib其实就是函数的具体实现,这时我们就可以把add.h和add.lib这两个文件给别人用
这里把add.lib跟add.h给别人
这时我们刚刚的test模块就得到了这个add函数
这时候打开我们原有的函数
导入现有项,之后输入#pragma comment (lib,“add.lib”)这句话的意思是导入一个静态库,同时也导入add.h头文件
这里也能正常导入,正常使用,很好的隐藏核心源代码
比如我们经常使用的printf函数
这里也能看出来静态库,上面我们也自己创造出一个add的静态库
7. 函数递归
7.1 什么是递归?
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接
调用自身的
一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,
递归策略
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
int main()
{
printf("hehe\n");
main();
return 0;
}
这是最简单的函数递归。但是这个程序会挂掉
这里是stack overflow意思是栈溢出.
下面就用函数递归来写一个案例
- 用函数的递归来将一个数拆开
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;
}
我们可以简单的分析一下这个函数
比如我们输入123,那么函数就调用了三次
当123>9的时候调用一次函数(大于9代表它是一个两位数)
n/10之后剩12依旧>9
再调用一次n/10剩余1<9完了打印1%10==1,之后轮第二张图的函数了,12%10==2,之后就是第一张图的函数123%10==3,这样1 2 3就打印完成了
+
所谓递推就是先递归再回推
每一次递归的这些数值都会被保存下来的,当我们彻底返回到主函数的时候这些值才没有。在我们的内存空间上有一个栈空间,每一个函数调用都会在栈上开辟一个空间,包括main函数的调用也会在栈上开辟一个空间以上三次调用,开辟了三次空间,保留了数据,函数调用结束一次数据销毁一次
7.2 递归的两个必要条件
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。
#include<string.h>
//编写函数不允许创建临时变量,求字符串的长度
int main()
{
char arr[] = "abcdef";//a b c d e f \0,strlen计算字符串\0之前的字符的个数
int num = strlen(arr);
printf("%d", num);
return 0;
}
//现在要自己编写跟strlen函数功能一样的函数
int my_strlen(char* str)//char*存一个字符的地址,把a的地址传给str,*str就找到这个元素了
{//str是a的地址 *str是a
int count = 0;//统计字符的个数
while (* str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);//数组传参的时候arr是数组第一个元素的地址,a的地址是第一个字符的地址
printf("%d", len);
return 0;
}
进入函数以后创建的局部变量就是临时变量。这里的count就是临时变量
想要完成目标,利用函数的递归
int my_strlen(char* str)
{
if (*str != '\0')
{
return 1 + my_strlen(str + 1);
}
else
{
return 0;
}
}
int main()
{
char arr[] = "abcd";
int len = my_strlen(arr);
printf("%d", len);
return 0;
}
下面来分析分析
a b c d \0
这里是a!=0
Return一个从b开始
b !=\0 return 一个cd
C !=0,return一个d
之后剩余一个\0
这时候return 0
7.3 递归与迭代
循环是一种迭代
求n的阶乘
int fanc(int m)
{
if (m <= 1)
{
return 1;
}
else
{
return m * fanc(m - 1);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fanc(n);
printf("%d", ret);
return 0;
}
//求第n个斐波那契数
int fib(int n)
{
if (n <= 2)
{
return 1;
}
else
{
return fib(n - 1) + fib(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret=fib(n);
printf("%d", ret);
return 0;
}
但是我们发现有问题;
在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
为什么呢? 我们发现 fib 函数在调用的过程中很多计算其实在一直重复。 如果我们把代码修改一下:
int count = 0;//全局变量
int fib(int n)
{
if(n == 3)
count++;
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
最后我们输出看看count,是一个很大很大的值。 那我们如何改进呢?
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出) 这样的信息。
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一 直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
那如何解决上述的问题: 1. 将递归改写成非递归。 2. 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不 仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保 存递归调用的中间状态,并且可为 各个调用层所访问。
比如,下面代码就采用了,非递归的方式来实现:
//求n的阶乘 int factorial(int n)
{
int result = 1;
while (n > 1)
{
result *= n ;
n -= 1;
}
return result;
}
//求第n个斐波那契数
int fib(int n)
{
int result;
int pre_result;
int next_older_result; r
esult = pre_result = 1;
while (n > 2)
{ n -= 1;
next_older_result = pre_result;
pre_result = result;
result = pre_result + next_older_result;
}
return result;
}
- 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。 2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。 3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。