c语言之函数的本质和使用及递归函数
前言
从今天开始,给大家分享c语言里面的函数本质及其使用;我估计大多读者看到这个,都认为c语言函数里面有啥可讲的,其实在学习过程中千万不要小看每一个知识点,因为每一个小的知识点都是给你在做项目之前打牢基础,很多人肯定会遇到过这种情况,在做项目写代码的时候,诶!用什么方法才能实现我要的功能以及这种写法怎样表示,甚至一些基础的语法错误都会有(严重的话,一些最为基本的错误都解决不了,发现不了。),归根到底还是基础不牢,其实这样做起项目来比较痛苦的(不过这会让你注视到c语言功底的重要性了)。好了,废话就不多说了,开始今天的主题分享!
(1)整个程序分成多个源文件,一个文件分成多个函数,一个函数分成多个语句,这就是整个程序的组织形式。这样组织的好处在于:分化问题、便于编写程序、便于分工。
(2)函数的出现是人(程序员和架构师)的需要,而不是机器(编译器、CPU)的需要。
(3)函数的目的就是实现模块化编程(类似于画pcb里面的模块化布局一样,就是说一个模块一个模块的来进行摆放和走线,你不按照这个规则来做的话,后面会让你画的怀疑人生,特别是在画高速板的时候;写函数也一样,一个功能一个功能的来写,不能这个功能没写完,又跑去写另外一个函数功能)。)。说白了就是为了提供程序的可移植性。
第一:遵循一定格式。函数的返回类型、函数名、参数列表等。
第二:一个函数只做一件事:函数不能太长也不宜太短,原则是一个函数只做一件事情。
第三:传参不宜过多:在ARM体系下,传参不宜超过4个。如果传参确实需要多则考虑结构体打包(之前的结构体专题里面有讲过结构体作为函数参数来传参!)。
第四:尽量少碰全局变量:函数最好用传参返回值来和外部交换数据,不要用全局变量(因为全局变量它是直到程序结束时,它的“寿命”才结束,因此你把作为函数传参,当在函数里面对它进行操作完毕后,在这个操作函数外面,全局变量还是原来的样子,对这个没注意的话在做项目中,你还以为当函数操作后,全局变量发生了改变了呢;不像局部变量那样更灵活,在函数里面使用完毕后就消亡了,就不会有这个歧义了。),下面是我举得一个例子:
#include <stdio.h>
int a;
void fun1( int b)
{
b++;
printf("the b is %d\n",b);
}
void fun2(int c)
{
c++;
printf("the c is %d\n",c);
}
int main(void)
{
fun1(a);
fun2(a);
printf("now the a is %d\n",a);
return 0;
}
the b is 1
the c is 1
now the a is 0
(1)函数将来被编译成可执行代码段,变量(主要指全局变量)经过编译后变成数据或者在运行时变成数据。一个程序的运行需要代码和数据两方向的结合才能完成。
(2)代码和数据需要彼此配合,代码是为了加工数据,数据必须借助代码来起作用。拿现实中的工厂来比喻:数据是原材料,代码是加工流水线。名词性的数据必须经过动词性的加工才能变成最终我们需要的产出的数据。这个加工的过程就是程序的执行过程。
(1)程序的主体是数据,也就是说程序运行的主要目标是生成目标数据,我们写代码也是为了目标数据。我们如何得到目标数据?必须2个因素:原材料+加工算法。原材料就是程序的输入数据,加工算法就是程序。
(2)程序的编写和运行就是为了把原数据加工成目标数据,所以程序的实质就是一个数据处理器。
(3)函数就是程序的一个缩影,函数的参数列表其实就是为了给函数输入原材料数据,函数的返回值和输出型参数就是为了向外部输出目标数据,函数的函数体里的那些代码就是加工算法。
(4)函数在静止没有执行(乖乖的躺在硬盘里)的时候就好象一台没有开动的机器,此时只占一些存储空间但是并不占用资源(CPU+内存);函数的每一次运行就好像机器的每一次开机运行,运行时需要耗费资源(CPU+内存),运行时可以对数据加工生成目标数据;函数运行完毕会释放占用的资源。
(5)整个程序的运行其实就是很多个函数相继运行的连续过程。
(1)函数的定义就是函数体,函数声明是函数原型,函数调用就是使用函数。
(3)函数声明的主要作用是告诉编译器函数的原型。
(4)函数调用就是调用执行一个函数。
#include <stdio.h>
int add(int a, int b);
// 函数声明
int main(void)
{
int a = 3, b = 5;
int sum = add(a, b);
// 典型的函数调用
printf("3+5=%d.\n", add(3, 5)); // add函数的返回值作为printf函数的一个参数
return 0;
}
// 函数定义
int add(int a, int b) // 函数名、参数列表、返回值
{
return a + b; //函数体
}
演示结果:
3+5=8.
(1)函数原型就是函数的声明,说白了就是函数的函数名、返回值类型、参数列表。
(2)函数原型的主要作用就是给编译器提供原型,让编译器在编译程序时帮我们进行参数的静态类型检查。
(3)必须明白:编译器在编译程序时是以单个源文件为单位的(所以一定要在哪里调用在哪里声明),而且编译器工作时已经经过预处理处理了,最最重要的是编译器编译文件时是按照文件中语句的先后顺序执行的。
(4)编译器从源文件的第一行开始编译,遇到函数声明时就会收到编译器的函数声明表中,然后继续向后。当遇到一个函数调用时,就在我的本文件的函数声明表中去查这个函数,看有没有原型相对应的一个函数(这个相对应的函数有且只能有一个)。如果没有或者只有部分匹配则会报错或报警告;如果发现多个则会报错或报警告(函数重复了,C语言中不允许2个函数原型完全一样,这个过程其实是在编译器遇到函数定义时完成的。所以函数可以重复声明但是不能重复定义)。
(1)递归函数就是函数中调用了自己本身这个函数的函数。
(2)递归函数和循环的区别。递归不等于循环。
(3)递归函数解决问题的典型就是:求阶乘、求斐波那契数列。(这个在算法里面会遇到这个,其实还是要掌握递归函数的基本概念,要真正理解它)。
(1)实际上递归函数是在栈内存上递归执行的,每次递归执行一次就需要耗费一些栈内存。
(2)栈内存的大小是限制递归深度的重要因素。
// 用递归函数来计算阶乘
#include <stdio.h>
int jiecheng(int n); // 函数声明
void digui(int n);
int main(void)
{
digui(5);
int a = 5;
printf("%d的阶乘是:%d.\n", a, jiecheng(a));
return 0;
}
// 函数定义
int jiecheng(int n)
{
// 传参错误校验
if (n < 1)
{
printf("n必须大于等于1.\n");
return -1;
}
if (n == 1)
{
return 1;
}
else
{
return (n * jiecheng(n-1));
}
}
void digui(int n)
{
printf("递归前:n = %d.\n", n);
if (n > 1)
{
digui(n-1);
}
else
{
printf("结束递归,n = %d.\n", n);
}
printf("递归后:n = %d.\n", n);
}
演示结果:
递归前:n = 5.
递归前:n = 4.
递归前:n = 3.
递归前:n = 2.
递归前:n = 1.
结束递归,n = 1.
递归后:n = 1.
递归后:n = 2.
递归后:n = 3.
递归后:n = 4.
递归后:n = 5.
5的阶乘是:120.
(1)收敛性就是说:递归函数必须有一个终止递归的条件。当每次这个函数被执行时,我们判断一个条件决定是否继续递归,这个条件最终必须能够被满足。如果没有递归终止条件或者这个条件永远不能被满足,则这个递归没有收敛性,这个递归最终要失败。
(2)因为递归是占用栈内存的,每次递归调用都会消耗一些栈内存。因此必须在栈内存耗尽之前递归收敛(终止),否则就会栈溢出。
(3)递归函数的使用是有一定风险的,必须把握好。