写点什么

【C 语言】卍字通晓→函数+递归

作者:泽En
  • 2022 年 3 月 06 日
  • 本文字数:8815 字

    阅读完需:约 29 分钟

【C语言】卍字通晓→函数+递归

​一篇 infoQ 写作平台带你拿捏 C 语言当中的函数与递归详细讲解。

📢📢📢函数的概述

构成程序的基本单元是函数,函数中包含程序可执行代码。

函数是指一段可以直接被另一段程序或代码引用的程序或代码,也叫做子程序。

一个较大的程序一般应分为若干个程序块,每一个模块用来实现一个特定的功能。所有的高级语言中都有子程序这个概念,用子程序实现模块的功能。

在 C 语言中,子程序是由一个主函数和若干个函数构成的。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次。

函数可以提高软件的开发效率,在 main 函数当中调用其它函数这些函数执行完毕被调用函数执行完毕之后又回到 main 函数当中。通常把这些被调用的函数称为下层函数。函数调用发生时候,立即执行被调用函数,而调用者则进入等待状态,直到被调用函数执行完毕。函数可以又参数 (void&int) 和返回值。

举例说明→盖一栋房子,在这项工程当中,在工程师的指挥下,有工人搬运盖楼材料,有建筑工人造房子,还有工人在楼房外粉刷涂料。编写程序于盖这个楼房实际是一个道理,主函数就像是工程师一样,其功能是控制每一步程序的执行,其中定义的其他函数就好比盖楼中的每一道步骤,分别去完成自己所对应的特殊功能。



📢📢C 语言函数好处

降低复杂性! 用函数的最首要原因是为了降低程序的复杂性,可以使用函数来隐含信息,从而使你不必再考虑这些信息。

避免重复代码段! 如果在两个不同函数中的代码很相似,这往往意味着分解工作有误。这时,应该把两个函数中重复的代码都取出来,把公共代码放入一个新的通用函数中,然后再让这两个函数调用新的通用函数。通过使公共代码只出现一次,可以节约许多空间。

限制改动带来的影响: 由于在独立区域进行改动,因此,由此带来的影响也只限于一个或最多几个区域中。

隐含顺序: 如果程序通常先从用户那里读取数据,然后再从一个文件中读取辅助数据,在设计系统时编写一个函数,隐含哪一个首先执行的信息。

改进性能: 把代码段放入函数也使得用更快的算法或执行更快的语言(如汇编)来改进这段代码的工作变得容易些。

进行集中控制: 专门化的函数去读取和改变内部数据内容,也是一种集中的控制形式。

隐含数据结构: 可以把数据结构的实现细节隐含起来。

隐含指针操作: 指针操作可读性很差,而且很容易引发错误。通过把它们独立在函数中,可以把注意力集中到操作意图而不是集中到的指针操作本身。

隐含全局变量: 参数传递。


C 语言函数的分类👇

  1. 库函数

  2. 自定义函数



📢📢库函数

为什么在程序当中会存在有库函数?

C 语言在发布时已经为我们封装好了很多函数,它们被分门别类地放到了不同的头文件中,使用函数时引入对应的头文件即可。这些函数都是专家编写的,执行效率极高,并且考虑到了各种边界情况,可以在写代码时候放心使用。

  • 我们知道我们在学习 C 语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道编程的运行结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上 printf ~

  • 在编程的过程中,我们会频繁的做一些输入函数的工作 scanf ~

  • 在编程的时候我们也计算,总是会计算 n 的 k 次方这样的运算 pow ~

像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能会用的到,为了支持可移值性和提高程序的效率,所以 C 语言的基础库中有提供一系列的库函数,放别程序员进行软件的开发!

那么怎么学习库函数?


例如 strcpy() → 字符串拷贝函数。

📢语法原型

  • char strcpy(char strDestination, const char* strSource);**

参数说明:

  1. strDestination目的字符串。

  2. strSource源字符串。

  3. strcpy() 会把 strSource 指向的字符串复制到 strDestination。

  4. 必须保证 strDestination 足够大,能够容纳下 strSource,否则会导致溢出错误。

代码示例如下👇


#include<stdio.h>#include<string.h>int main(void){  char arr1[20] = { 0 };  char arr2[] = "hello C";  strcpy(arr1, arr2);  printf("%s\n", arr1);    return 0;}
复制代码


简单总结,C 语言常用的库函数!

  • IO 函数(输入输出函数)

  • 字符串操作函数

  • 字符操作函数

  • 内存操作函数

  • 时间/日期操作函数

  • 数学函数



📢📢自定义函数

如果库函数能干所有的事情,那还要程序员干什么?

除了库函数,我们还可以编写自己的函数,拓展程序的功能。自己编写的函数称为自定义函数。所以更加重要的是自定义函数,如何去实现一个自定义函数。

自定义函数和库函数一样,有函数名,返回值类型 (int & void) 和函数参数 (有参无参) ,但是不一样的是这些都是我们自己来设计的,这从而就给了程序员一个很大的发挥空间。

那我们举出一个例子:写一个函数判断两个数字大小!


#include<stdio.h>int Max(int x, int y){  if (x > y)    return x;  else    return y;}int main(void){  int a = 10;  int b = 20;
int ret = Max(a, b); printf("ret = %d\n", ret); return 0;}
复制代码


🖊运行结果↓

ret = 20




那我们再举出一个无返回值类型例子:写一个函数交换两个值的变量!


#include<stdio.h>void swap(int *x, int *y){  int tep = *x;  *x = *y;  *y = tep;}int main(void){  int a = 10;  int b = 20;  printf("交换之前:a=%d,b=%d\n", a, b);  swap(&a, &b);  printf("------------------\n");  printf("交换之后:a=%d,b=%d\n", a, b);  return 0;}
复制代码


🖊运行结果↓

交换之前:a=10,b=20

交换之后:a=20,b=10

注意→在你交换值的时候需要取出它们的地址,因为相当于你以及改变它们的内存编号了!我们指望它能够把 a b 的值进行交换,也就是说我们在这个过程中会把 swap() 函数的值进行交换。所以,我们的外部函数和内部函数是必须要建立联系。那么我们就要把地址给传递过去,通过地址的内存单元的编号我们才能够找回来。



📢📢函数的参数

📢实际参数(实参)

真实传递给函数的参数,叫做实参。实参的参数可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须要有确定的值,以便把这些值传递给到形参当中去。

像上面示例当中 swap(&a,&b);     这里的 swap() 括号里面的变量就是实际参数。

📢形式参数(形参)

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元)调用一瞬间才会开辟内存空间,所以叫做形式参数。形式参数当函数调用完成之后就会自动销毁了。因此形式参数只是在函数当中有效!声明周期范围有限。

像上面的示例当中 void swap(int x, int y) 这里的无返回类型 swap 函数里面就是形式参数。**

形参和实参的功能是传递数据,发生函数调用时,实参的值会传递给形参。



📢📢调用函数

创建 C 函数时,会定义函数做什么,然后通过调用函数来完成已定义的任务。

当程序调用函数时,程序控制权会转移给被调用的函数。被调用的函数执行已定义的任务,当函数的返回语句被执行时,或到达函数的结束括号时,会把程序控制权交还给主程序。就像我们上面举例说明的代码一样!

说的简单点,就是让这个函数帮我们做事情,叫做函数调用!

C 语言中,函数的调用的一般形式为:

  • 函数名(实际参数列表)

函数的调用方式!!!

  1. 函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参与表达式的运算。这种方式要求函数是有返回值的。例如:z=max(x,y) 是一个赋值表达式,把 max 的返回值赋值给 z。

  2. 函数语句:函数调用的一般形式加上分号即构成函数语句。例如:printf("%d",a); scanf ("%d",&b); 都是以函数语句的方式调用函数。

  3. 函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为实参进行传送,因此要求该函数必须是有返回值的。例如: printf  函数的实参来使用的。在函数调用中还应该注意的一个问题是求值顺序的问题。所谓求值顺序是指对实 参表中各量是自左至右使用呢,还是自右至左使用。对此,各系统的规定不一定相同。 printf 函数时已提到过,这里从函数调用的角度再强调一下。


当调用函数时,有两种向函数传递参数的方式,如下↓

📢📢传值调用

向函数传递参数的传值调用方法,把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。

  • C 语言使用传值调用方法来传递参数。一般来说,这意味着函数内的代码不会改变用于调用函数的实际参数。

  • 函数 swap() 定义如下


void swap(int x, int y){   int temp;   temp = x; /* 保存 x 的值 */   x = y;    /* 把 y 赋值给 x */   y = temp; /* 把 temp 赋值给 y */}
复制代码

现在,让我们通过传递实际参数来调用函数 swap() ↓


#include <stdio.h>void swap(int x, int y){   int temp;   temp = x; /* 保存 x 的值 */   x = y;    /* 把 y 赋值给 x */   y = temp; /* 把 temp 赋值给 y */  }
int main (void){ 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;}
复制代码


🖊运行结果↓ 

交换前,a 的值: 100
交换前,b 的值: 200
交换后,a 的值: 100
交换后,b 的值: 200

可以从上面的代码发现程序并没有达到交换的效果!

📢📢传址(引用)调用

通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。

  • 通过引用传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。

  • 传递指针可以让多个函数访问指针所引用的对象,而不用把对象声明为全局可访问。

  • 函数 swap() 定义如下:


void swap(int *x, int *y){   int temp;   temp = *x;    /* 保存地址 x 的值 */   *x = *y;      /* 把 y 赋值给 x */   *y = temp;    /* 把 temp 赋值给 y */}
复制代码


现在,让我们通过传递实际参数来调用函数 swap()


#include <stdio.h>
void swap(int *x, int *y){ int temp; temp = *x; /* 保存地址 x 的值 */ *x = *y; /* 把 y 赋值给 x */ *y = temp; /* 把 temp 赋值给 y */} int main (void){ int a = 100; int b = 200; printf("交换前,a 的值: %d\n", a ); printf("交换前,b 的值: %d\n", b ); /* 调用函数来交换值 * &a 表示指向 a 的指针,即变量 a 的地址 * &b 表示指向 b 的指针,即变量 b 的地址 */ swap(&a, &b); printf("交换后,a 的值: %d\n", a ); printf("交换后,b 的值: %d\n", b ); return 0;}
复制代码


🖊运行结果↓

交换前,a 的值: 100
交换前,b 的值: 200
交换后,a 的值: 200
交换后,b 的值: 100

在讲以上知识点的时候,希望大家再写自定义函数的时候,我们自定义函数只需要完成我们的功能即可,不需要进行很多无谓的操作,这样只会显得多此一举。

在这里拓展一个知识点,假设你实参里面有数组名,那么我们把数组名传递给到形参里去的话。注意:我们传递的仅仅是数组首元素的地址!

📢📢📢函数的声明和定义

函数的声明就是告诉编译器我这里是有一个函数的,它的参数和返回类型也要告诉编译器,那么这就够了。这个时候编译器就知道你这个函数已经声明了,就不会再不知道你没有这个函数。因为我们已经告知编译器我们这个函数是实实在在是存在的。所以,编译器就不会报错!


📢函数的声明组成


  • 函数的返回值类型,返回值可以是某个 C 的数据类型

  • 函数名,函数名也就是函数的标识符,函数名在程序中必须是唯一的。因为标识符,所以函数名也要遵守表示符的一个命名规程。

  • 参数列表,参数列表可以没有变量也可以有多个变量,在进行函数调用时候,实际参数将被复制到这些变量当中。

  • 分号 ④部分组成其形式如下:

  • 返回值类型    函数名(参数列表) ;

  • 此处要注意的是:声明的最后要用到分号";"作为语句的结束标志!


  1. 函数的定义就是在创建这个函数!

  2. 函数的声明就是在告知编译器我有这个函数!

  3. 注意:声明只是告诉你有没有这个函数,真正取决于是函数的定义!


下面的程序为大家用代码举个例子如下所示↓


#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>int main(void){  int a = 10;  int b = 20;    Add(a, b);     return 0;}
void Add(int x, int y){ int z = 0; z = x + y; printf("ret = %d\n", z);}
复制代码

代码编译运行结果:"Add"重定义,不同类型报错!


#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>void Add(int x, int y);int main(void){  int a = 10;  int b = 20;    Add(a, b);     return 0;}
void Add(int x, int y){ int z = 0; z = x + y; printf("ret = %d\n", z);}
复制代码

🖊运行结果↓

ret = 30


从上面代码不同的编译结果我们就可以看出有函数的声明和无函数声明的一个区别!

说明:当然如果你把函数的定义放在主函数前面这个时候我们函数的声明其实都是无所谓的,因为我们的编译器都是从前往后开始的执行的。因为我编译器在前面已经见过了你这个函数是存在的。那么在主函数调用你这个函数自然而然是一点问题都是没有的。



📢📢函数的嵌套

  • 函数不能嵌套定义,但可以嵌套调用,也就是在一个函数的定义或调用过程中允许出现对另外一个函数的调用。

  • 在 C 语言中,函数的定义都是相互平行的,独立的。也就是说,在定义函数的时候,一个函数体内不能包含另一个函数的定义,这个有些语言是不同的。因为,有些语言是允许在定义函数的时候,在其中函数体包含另一个函数的形式,而这种形式称之为函数的嵌套。


例如:示例代码如下↓


#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>int main(void){  void Display()  //错误不能在函数内定义函数  {    printf("Hello C!\n");  }  return 0;}
复制代码

从上面的代码中可以看到,在主函数 main 中定义了一个 Display 函数,目的是输出语句(Hello C!)。但是,C 语言是不允许进行嵌套定义的。因此就会提示错误!❌




虽然,C 语言不允许嵌套定义,但是可以嵌套调用函数,也就是说,在一个函数体内可以调用另外一个函数。例如:下面代码进行函数的嵌套调用↓


#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>void nesting(){  printf("hello C!\n");}void Display()  {  nesting();}int main(void){  Display();  return 0;}
复制代码

上述代码是正确的,在函数体内进行函数的嵌套调用形式

🖊运行结果↓

hello C


📢📢📢函数的递归

概述:一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层,当最内层的函数执行完毕后,再一层一层地由里到外退出。

📢📢递归是什么?

程序调用自身的编程技巧称为递归。递归作为一种算法在程序设计语言中是广泛应用的。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题转换位一个与原问题相似的规模较小的问题来去进行求解,递归策略只需少了的程序就可以描述出解题的过程所需要的多次重复计算,大大减少了程序的代码量。递归的人主要思考方式就在于:把大事化小。递归之所以能够实现,是因为函数的每个执行过程在栈中都有自己的形参和局部变量的副本,这些副本和和该函数的其它执行过程是不会发生关系的。

注意→当递归进入到最内层的时候,递归就结束了,就开始逐层退出了,也就是逐层执行 return 语句。当执行最后层的 return 之后,就执行上面一层的递归!



📢📢递归两个必要条件

每一个递归函数都应该只进行有限次的递归调用,否则它就会进入死胡同,永远也不能退出了,这样的程序是没有意义的。


  • 存在限制条件,当满足这个限制条件之后的时候,递归便会不再继续。

  • 每次递归调用之后都会越来越接近这个限制条件

  • 这两个条件是必须要知道的,这样你才知道递归怎么去使用。


那么下面举出一个最简单的例子,示例代码如下↓


#include<stdio.h>int main(void){  printf("hello C!\n");  main();  return 0;}
复制代码

C 语言的递归是什么?不就是函数体内自身调用自己称之为递归吗。

如上述代码中可以看到,这里主函数里面有个打印库函数,其语句 hello C。那么后面有个 main();函数。那么当我们执行完语句,执行 main();函数,此时执行由回到主函数开头又执行打印库函数,再执行 main();函数进行调用。这样就构成了一个死循环,直到系统自己退出中这个死循环为止。不然就会一直存在这个死递归

  • 注意:递归不能这样写,但是它确实是递归。

  • 上面情况,如果你调试了,它会出现栈溢出的情况。



🔥习题①→接收一个整型值(无符号),顺序打印出每一位。例如 1234,输出 1 2 3 4!

解题思路🖊

  • 1234 % 10 = 4

  • 1234 / 10 = 123    123 % 10 = 3

  • 123 / 10 = 12        12 % 10 = 2

  • 12 / 10 = 1            1   % 10 = 1

%u → 无符号 10 进制整数。

最后:1 / 10 = 0,当我这个数为 0 的时候,就得到了所有输出的数字。但是你会发现,我们这里得到数字都是倒着打印的,当然用数组也可以实现正向打印。不过麻烦,所以我们这里带大家实现用递归如何输出上述程序!

注意:当函数递归完之后是会继续再次从递归的函数开始执行,直到你满足限制条件!


#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>void print(unsigned int number){  if (number > 9)             //限制条件  {    print(number/10);       //调用这个函数,直到表达式为假 执行下面语句  }  printf("%d ", number % 10);}int main(void){  unsigned int number = 0;  printf("请输入数字:");  scanf("%u", &number);
print(number);
return 0;}
复制代码

简单说明:栈(stack) 又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

拓展,栈:当你递归调用的次数多了,那么每一次都要开辟一次内存空间,当开辟到空间存储不够的时候,那么就会发生栈溢出的情况!所以我们在写递归的时候需要注意下:

  1. 绝对不能够进行死递归,死递归就必然栈溢出。因为,它没有跳出这个循环。

  2. 递归每次就必须要逼近这个跳出条件。

  3. 递归不能够"太深",也就是层次不能太深。这样也会有栈溢出。



🔥习题②→模拟实现字符串函数,用递归的形式,不能创建临时变量。

解题思路🖊

这个题目是求字符串长度,那我们要求一个字符串函数,不就是模拟 strlen 吗?就是实现这个字符串函数的功能,让我们自己创建一个自定义函数来去实现。

  • 用 My_strlen 求字符串函数!那么字符串就是:

  • My_strlen("Cnb");的长度!形参字符型指针变量 str 指向的不就是这个字符串吗。那么这个拿到字符串的第一个长度是很容易的,因为我们一开始 str 就是从第一个字符拿到的不是吗?刚好可以进行判断它是不是 '\0' ,如果不是就继续执行!

  • 1+My_strlen("nb");就可以变成这种形式。这不就是上面的长度吗?因为我发现我的第一个字符串长度并不是 '0', 所以就可以变成这种形式。

  • 1+1+My_strlen("b");

  • 1+1+1My_strlen("");     在往后就是 '\0' 。最终结果运行结果为→3

代码示例如下↓


#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>int My_strlen(char *str){  if (*str != '\0')    return 1 + My_strlen(1 + str);  else    return 0;}int main(void){  char arr[20] = {0};  printf("请输入字符:");  scanf("%s", &arr);
printf("str = %d\n", My_strlen(arr));
return 0;}
复制代码

🖊运行结果↓

请输入字符:123456

str = 6



🔥习题③→求斐波那契数!

  • 这个数列从第 3 项开始,每一项都等于前两项之和,这就是斐波那契数。

  • 公式:F(n - 1)+F(n - 2)、

  • 注意:这个代码不适合用递归,如果你输入的数字是 50 以上的话就要递归好久层次太深,所以不适合用递归,递归固然有好处但是也要分场合使用。程序效率低下!

代码示例如下↓


#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>int Fib(n){  int a = 1;  int b = 1;  int c = 1;  int count = 0;  //计数  while (n > 2)  {    c = a + b; // 1 1 2 3 5 8 13 21 34 55    a = b;     // 1 2 3 4 5 6 7  8  9  10    b = c;          n--;    count = count + 1;  }  printf("count = %d\n", count);  return c;}int main(void){  int n = 0;  printf("请输入数字:");  scanf("%d", &n);  int c = Fib(n);  printf("c = %d\n", c);    return 0;}
复制代码

那么这里我再把递归的代码给大家看看!来用递归的形式做斐波那契数列,大家可以看下区别,但是如上所说这里使用递归做不合适。

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>int Fib(int n){  if (n <= 2)    return 1;  else    return Fib(n - 1) + Fib(n - 2);}int main(void){  int n = 0;  printf("请输入数字:");  scanf("%d", &n);  int ret = Fib(n);  printf("ret = %d\n", ret);
return 0;}
复制代码

那么这里我们需要知道为什么用递归的方法不合适做斐波那契数列。

从上面的代码可以看出用递归的方法代码比前面的方法简短了不少,直接将斐波那契数列的递推公式带入即可实现。但是,用递归实现的话会有一个非常大的缺点:效率低下(算法的运行速度比较慢),尤其是当我们输入的 n 较大时。那么程序计算的时候需要的时间很长,这是因为程序在每次递归调用自己时都需要算一遍(n-1)和(n-2)项,存在很多重复计算。不考虑栈溢出的情况。


🎄最后🎄

看到这里你是否对函数 &递归有了一个了解,实际上这些东西都需要自己上手自己敲代码去实践的。这样对你理解的帮助很大,尤其是在这里所说的递归。不懂的话多去 F10 进行调试,调试对你学习帮助是非常大的(o=^•ェ•)o┏━┓


发布于: 刚刚阅读数: 2
用户头像

泽En

关注

好像没有😅 2022.01.29 加入

CSDN嵌入式领域新星创作者、2021年度博客之星物联网与嵌入式开发TOP5、2022博客之星TOP100 掘金创作者

评论

发布
暂无评论
【C语言】卍字通晓→函数+递归_递归_泽En_InfoQ写作平台