作者主页:
lovewold_的博客_CSDN博客-C语言猎杀时刻,Github领域博主
本章内容:
函数的理解以及使用细节,函数还有很多叫法,比如方法、子例程或程序,等等。
目录
前言:
一、C语言函数是什么
二、函数定义
三、函数声明
四、函数的参数
传值调用
传址调用
五、递归
5、1为什么我们要使用递归?
5、2递归使用的优缺点
总结
前言:
本章主要讲述了函数部分内容,通过样例和图文描述来具体讲述函数那些事。
一、C语言函数是什么
C的函数是规划好的一起执行一个任务的语句总和。每一个C程序都至少有一份函数,既主函数main(),同时其他可以执行某一个任务的语句可以单独独立出来,划分到不同函数中。这便是程序员的作用,设计自定义函数。
函数的声明告诉编译器函数的名称,返回类型和参数。函数的定义提供了函数的主体部分,既用来实现逻辑的代码快。C标准库提供了大量库函数可以调用的内置函数,如初学者最常用的printf用来格式化打印和scanf格式化输出函数。
函数也有很多叫法,比如方法,子例程或子程序,等等
二、函数定义
函数的定义就是函数体的逻辑功能实现,用来完成单个的任务。函数体就是一个代码块,它在函数被调用的时候执行。与函数的定义相反,函数的声明出现在函数被调用的地方,通过函数的声明告诉编译器函数的信息,用于确保函数被正常的调用。
C语言中函数的定义形式一般如下:
返回值类型 函数明(函数参数)
{
代码块;
}
在C语言中,函数由一个函数头和一个函数主体组成。
- 返回类型:一个函数可以返回一个值,在函数名前面定义数据类型即可规定返回值数据类型。而有些函数的操作不需要返回值,这个时候在数据类型位置使用关键字void即可。
- 函数名称:函数的名称,定义名称最好具有实际意义,与参数列表构成函数签名。
- 参数:参数类似与占位符,同时确定了传递参数的类型。当函数被调用的时候,可以向函数传递制定需要的参数。同时,当不需要传递参数的时候只需要使用关键字void或者不填写即可。
- 函数主体:用来实现程序任务的语句。
实例
int MAX(int x,int y)//创造用来实现找出两者较大值的函数 {int max = 0;//创建变量if(x>y){max = x;}else if(x<y){max = y; }else{max=x;//相等返回最大值相等}return max;//返回指定类型的返回值 }
此函数,有两个int类型的参数,x和y,会返回两个数中的较大数字。
三、函数声明
当主函数的使用过程中需要调用函数,编译器需要知道函数的信息。那么函数的身份证名片是需要怎么实现呢?
我们有两种实现方式:
第一种:如果主函数所在源文件前面已经出现了函数的定义,编译器就会记住函数的参数数量,类型,以及返回值类型。在后续调用的过程中,编译器便会主动检测函数,确保调用是正确的。
第二种:在主函数前向编译器主动提供函数原型既不包括函数实现部分的整个函数部分。
int MAX(int x,int y);
函数原型提供了函数的返回值类型,函数名,函数参数部分的定义。如上述比较较大值的函数原型
在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:
int MAX(int, int);
当您在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。
四、函数的参数
如果函数要使用参数,就必须接受声明好的符合接受类型的变量。这些变量称为函数的形式参数。
形式参数就像你创建的局部变量,进入的时候创建,出去的时候销毁。
在传递参数的时候,有两种传值方式。
调用类型 描述 传值调用 这个方法把参数的实际值复制给了函数的形式参数,也就是形式参赛获得了实际参赛的一份临时拷贝,因此修改函数中的形式参数值不改变其实际参数大小 传址调用 这种方法直接把实际参数的地址传递给了形式参数,因此,在同一份地址下的参数大小是固定且独有的,因此改变了所在地址下的元素就会改变实际参数。 默认情况之下,C语言使用传值调用来传参。在使用函数过程中,要实现确定好你需要改变的参数,来确定使用什么调用类型。
实例:
传值调用
向函数传递参数的传值调用方法,把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
默认情况下,C 语言使用传值调用方法来传递参数。一般来说,这意味着函数内的代码不会改变用于调用函数的实际参数。
定义一个函数用来交换参数位置,实现互换
void swap(int x,int y) {int temp;temp=x;x=y;y=x;}
此时我们来调用函数
#include <stdio.h>void swap(int x, int y);//声明函数int main () {/* 局部变量定义 */int a = 100;int b = 200;printf("交换前,a 的值: %d\n", a );printf("交换前,b 的值: %d\n", b );/* 调用函数来交换值 */swap(a, b);printf("交换后,a 的值: %d\n", a );printf("交换后,b 的值: %d\n", b );return 0; } void swap(int x,int y) {int temp;temp=x;x=y;y=temp;}
编译结果
不难发现,函数在内部虽然改变了x和y的值,但是没有影响到实际参数。
传址调用
通过引用传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。
传递指针可以让多个函数访问指针所引用的对象,而不用把对象声明为全局可访问
传址调用完善代码:
#include <stdio.h>void swap(int* x, int* y);//声明函数int main() {/* 局部变量定义 */int a = 100;int b = 200;printf("交换前,a 的值: %d\n", a);printf("交换前,b 的值: %d\n", b);/* 调用函数来交换值 */swap(&a, &b);//传递参数地址printf("交换后,a 的值: %d\n", a);printf("交换后,b 的值: %d\n", b);return 0; } void swap(int* x, int* y)//参数定义应该规定传递的是地址 {int temp;//中间量temp = *x;//交换改变地址下元素值*x = *y;*y =temp;}
编译结果:
五、递归
C语言运行时堆栈支持递归函数的实现,通俗来讲,递归函数就是自己调用自己自身的函数。递归的使用必须限制递归条件,否则就会陷入同循环一样的死递归现象。因此我们通常需要if,while等语句作为控制阀门来规定递归的执行。每一次递归都应该接近其目标值以达到及时跳出递归,完成任务。
5、1为什么我们要使用递归?
递归的实现是一种算法思维,体现在大事化小,小事化了的算法思维。递归分两步:由外到内部的递,和由内部到外部的归。
这里举三个例子:
按次序依次打印数字的每一位(由高位到低位)
思维过程,我们知道一个数去%10便可以得到个位,再通过循环以此除10降低位数,继续获取个位就能实现由低位到高位的数字获取。这时我们打印就能实现由个位到高位的印。此时我们不难发现,实际需求的打印次序反了。
那么我们可以通过递归的方式实现,
#include<stdio.h>void PRINTF(int x) {if (x > 9)//递归执行终止条件{PRINTF(x/10); //递归调用,每一次除10,传递其参数到下一次递归内部}printf("%d ", x%10);//打印每一位数据} int main() {int n = 0;scanf("%d", &n);PRINTF(n);//函数调用return 0; }
不难发现,我们的调用一直持续到除10到小于等于9的时候才开始返回后续代码执行。因此能比较简单的观察调用和返回的过程。
执行结果
打印每一位数字的位数之和;
递归实现:
#include<stdio.h>int DigitSum(int x) {if (x > 9){return x % 10 + DigitSum(x / 10);//传递每一次/10后参数到下一次递归。下次递归就会返回//return x%10+(x/10)%10+DigitSum(x/10/10)}else if (x < 9)//递归终止条件{return x;//到个位递归终止,直接返回个位值与之前的return的值相加} } int main() {int n = 0;scanf("%d", &n);int ret = DigitSum(n);printf("%d", ret);return 0; }
编译结果:
不创建变量创建strlen函数(计算字符串长度函数);
#include<stdio.h>int my_strlen1(char* x) {if (*x != '\0')//每一次访问数组元素,当达到'\0'字符串结束,停止递归{return 1 + my_strlen(x+1);//每一次下标访问地址加一,访问下一个元素}elsereturn 0; } //非递归 int my_strlen(char* x) {int count = 0;while(*x != '\0'){count++;x++;}return count; } int main() {char arr[]="adbbh";int ret = my_strlen(arr);int ter = my_strlen1(arr);printf("%d %d", ret,ter);return 0; }
编译结果:
5、2递归使用的优缺点
递归与迭代
递归是一种强有力的技巧,和许多技巧一样,在考虑不周到情况下会被误用。阶乘的定义往往是以递归的形式表示,既5!=5*4!
当n<=0时:1
jiecheng(n)
当n>0 : n*jiecheng(n-1)
递归实现:
int jiecheng1(int x) {if (x <= 1){return 1;}else if (x > 1){return x * jiecheng1(x - 1);} }
非递归,迭代实现:
int jiecheng2(int y) {int i = 1;int sum = 1;for (i = 1; i <= y; i++){sum *= i;}return sum; }
函数调用:
#include<stdio.h>//在此忽略函数定义部分,仅做演示 int main() {int a = 0;scanf("%d", &a);int ret1 = jiecheng1(a);int ret2 = jiecheng2(a);printf("%d\n%d", ret1, ret2);return 0; }
编译结果:
优缺点分析:
递归函数调用将涉及到运行时候都开销——参数必须压到堆栈中、为局部变量分配内存空间、寄存器的值必须保持等,当递归函数返回时,上述操作必须还原,基于这些开销,是否对于程序有简化的作用。
比如计算了n!,实际上并没有迭代这么高效。基于这道题,迭代(循环)的方式虽然代码的可读性不如递归直接拿数学定义公式实现的可读性高,但它能更为有效的计算出相同结果。它不需要反复堆栈等操作。
这里使用一个极端例子,斐波那契数列的实现。
斐波那契数列:前两数和为第三个数字——1,1,2,3,5,8······
n<=1 : 1;
feibo(n) n=2 : 1;
n>2 ;feibo(n-1)+feibo(n-2);
递归形式的定义便可以直接思考到递归实现。
递归实现:
int feibo(int x) {if (x <= 2){return 1;}else if (x > 2){return feibo(x - 1) + feibo(x - 2);} }
非递归实现:
//非递归实现 int feibo1(int x) {int a = 1;int b = 1;int c = 0;int n = x;if (n <= 2){return 1;}while (n > 2){c = a + b;a = b;b = c;n--;}return c; }
可见递归的实现方式非常巧妙,可读性高;但是,feibo(n-1)的计算时也会递归进入feibo(n-2)
feibo(n-2)的计算会递归计算feibo(n-3)因此这样的一个计算会重复多次。
而且不单单只是一次重复计算,每一次递归都会触发两次递归,成指数爆炸的方式增长,不难想象你的电脑是否承担的起如此多的计算,电脑为函数划分的栈区是否能承受得住。例如:在递归计算feibo(10)时,feibo(3)被计算21次;在计算feibo(30)时feibo(3)被递归进行了317811次计算。这些计算是多余的,额外的开销就是如此恐怖。
总结
希望大家读完本章后能学到点东西,函数的声明,函数的定义,函数参数的两种方式,递归实现的思维,递归迭代优缺点等。同时,笔者不才,对于文章的细枝末节可能关注不是太多,希望大家能指正。