【C语言初阶】之函数

【C语言初阶】之函数

  • 1. 函数是什么
  • 2. C语言中的函数
    • 2.1 库函数
      • 2.2.1 利用文档学习库函数
    • 2.2 自定义函数
  • 3. 函数参数
    • 3.1 实际参数(实参)
    • 3.2 形式参数(形参)
  • 4. 函数调用
    • 4.1 传值调用
    • 4.2 传址调用
    • 4.3 练习
  • 5. 函数的嵌套调用和链式访问
    • 5.1 嵌套调用
    • 5.2 链式访问
  • 6. 函数的声明和定义
    • 6.1 函数的声明
    • 6.2 函数的定义
    • 2.3 项目中函数的声明和定义的常见使用方式
      • 2.3.1 项目中分文件来写的目的
  • 7. 函数递归
    • 7.1 递归的定义
    • 7.2 递归的两个必要条件
      • 7.2.1 练习1
      • 7.2.2 练习2
    • 7.3 递归与迭代
      • 7.3.1 练习3
      • 7.3.2 练习4
      • 7.3.3 递归和迭代的区别

❤️博客主页: 小镇敲码人
🍏 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌞任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞

1. 函数是什么

数学中我们也了解过函数的概念,但是C语言中的函数你真的了解吗?

  • 美国人将函数称之为function,意思是功能的意思,到了我们中国将其译为函数,是因为C语言里面的函数与数学里面的函数有相似之处。
    • lenth = strlen(str)
    • y = f ( x ) y =f(x) y=f(x)
  • 看看他们是如此的相似,都是通过一份数据得到另一份数据,不过,严格来讲讲Function理解为功能似乎更加恰当,因为一个C语言函数所实现的功能常常是独立的,一个C语言程序由多个函数组成,可以理解为一个程序由多个小功能叠加。
  • C语言函数一般有函数名、返回值类型、函数参数。
                                                                       ----以上内容借鉴于C语言中文网

2. C语言中的函数

2.1 库函数

库函数就是C语言基础库里面提供的函数,但是C语言只规定了该函数的参数、返回值、还有基本功能,具体的底层实现不同的编译器可能会不同。

那C语言为什么会有库函数的存在呢?

  1. 这是因为我们在编写程序时,会有很多常见的功能会去使用,它们出现的频率很高,像打印功能(printf)、字符串拷贝功能(strcpy)等等。这种基础功能不是业务性的代码。我们在开发的过程中每个程序员都会使用到,为了提高可移植性和程序的效率,所以C语言的基础库提供了一系列类似的库函数,方便程序员进行软件开发。

2.2.1 利用文档学习库函数

那我们在学习C语言的过程中该如何学习库函数呢?我们可以借助下面的文档网站进行学习库函数cplusplus.com.
在这里插入图片描述
在这里插入图片描述
C语言常见的库函数有:

  • I/O函数(输入输出函数)
  • 字符串操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其它库函数

我们通过使用上述文档,来演示一下如何通过文档学习库函数:
进入那个网站,我们需要点击旧版,因为只有旧版才有搜索的功能,如图:
在这里插入图片描述
也就是右上角位置,当你点击之后,该界面会变成这样:

在这里插入图片描述

我们如果搜索字符串打印函数strcpy会出现这样的信息:
在这里插入图片描述
我们通过这个文档可以这样来学习函数strcpy

在这里插入图片描述
通过上面图片我们可以知道strcpy它的返回类型是char*类型,它有两个参数,第一个参数是一个char*类型的指针,表示目的地字符串的起始地址,第
二个参数是const char*类型的指针,表示源字符串的起始地址,且它不可修改,上面图片也是strcpy函数的一个定义。

再看下面的内容:

在这里插入图片描述
通过以上信息,我们可以知道strcpy函数的功能:进行字符串的拷贝,把源头字符串拷贝到目的地字符串数组中,包括终止字符\0也要拷贝。同时为了避免溢出,目的地字符数组的大小应该大于等于源字符串的大小(包括终止字符\0),它的内存和源字符串也应该不重叠。
当然你也可以使用edge浏览器的插件帮助你把英文翻译为中文,以此来帮助更好的去阅读文档,但它翻译出的信息可能会与本意有所偏差。

接着阅读信息:

在这里插入图片描述
上面图片主要介绍了strcpy函数的两个参数,destination是指向目的数组的地址,字符串将拷贝到这个字符数组中,source代表被拷贝的源字符串。

在这里插入图片描述
上面的内容主要是介绍了这个函数的返回值,目的地字符数组的起始地址将被返回。

我们通过下面代码来演示一下如何使用库函数strcpy:

#include<stdio.h>
#include<string.h>int main()
{char arr1[] = "hello bit";//源头char arr2[] = "xxxxxxxxxxxxxxx";//目的地//对于数组,数组名表示首元素地址,它的类型是char*strcpy(arr2, arr1);//将字符串arr1的内容全部拷贝到arr2中,包括\0printf("%s\n", arr2);//打印目的地字符数组return 0;
}

运行结果为:

  • 使用库函数必须要包含#include对应的头文件。
    通过上述对利用文档学习库函数阐述,相信你已经对文档的使用有所了解,我们并不需要记住所有的库函数,只需要学会使用文档就行了,下面还有一些常见的查询工具:https://en.cppreference.com/w/(英文版)、https://zh.cppreference.com/w/(中文版)。

2.2 自定义函数

讲到这里大家可能就会有疑惑,既然库函数可以实现这么多功能,那还要程序员干什么呢?注意:库函数实现的功能一般都是较简单的,不足以满足所有的业务需求。
所以更加重要的是:自定义函数。

自定义函数和库函数一样,有函数名、返回值类型、函数参数。但是唯一不一样的地方是它需要我们自己去设计,而库函数给它传对应的参数就可以直接使用。

函数组成:

ret_type fun_name(paral,...)
{statement;//语句项
}
ret_type:函数的返回类型
fun_name:函数名
para1:函数参数

我们举一个例子:

写一个函数可以求两个数的最小值。

#include <stdio.h>// get_min函数用于比较两个整数a和b的大小,返回较小的数
int get_min(int a, int b)
{if (a > b) // 如果a大于b,则返回b,表示b是较小的数return b;else // 否则返回a,表示a是较小的数return a;
}int main()
{int a = 0; // 声明并初始化整数变量a,用于存储用户输入的第一个数int b = 0; // 声明并初始化整数变量b,用于存储用户输入的第二个数scanf("%d%d", &a, &b); // 从用户输入读取两个整数,并分别存储到a和b中printf("%d\n", get_min(a, b)); // 调用get_min函数比较a和b的大小,并输出较小的数return 0; // 程序正常结束,返回0表示成功
}

我们可以给这个函数传一个初始化的局部变量和常量,或者传一个函数调用,这都行,前提是表达式通过计算必须是一个确定的值。
在这里插入图片描述
可以看到如果你使用一个未初始化的局部变量去比较时,编译器是会报错的,其它几种方式都没有什么问题。

我们再举一个例子:

写一个函数,可以交换两个变量的内容。

#include <stdio.h>// swap1函数用于交换两个整数a和b的值,但实际上并不起作用,因为形式参数x和y是值传递
void swap1(int x, int y)
{int temp1 = x; // 声明临时变量temp1,并将x的值赋给temp1x = y; // 将y的值赋给x,但由于x和y是形式参数,不会影响到main函数中的a和by = temp1; // 将temp1的值赋给y,同样也不会影响到main函数中的x和y
}// swap2函数用于交换两个整数的值,通过指针传递来实现交换
1void swap2(int* px, int* py)
{int temp2 = *px; // 声明临时变量temp2,并将px指针所指向的值赋给temp2*px = *py; // 将px指针所指向的值赋给px指针所指向的位置,实现a和b的交换*py = temp2; // 将temp2的值赋给pb指针所指向的位置,实现b和a的交换
}int main()
{int a = 3; // 声明并初始化整数变量a,初始值为3int b = 4; // 声明并初始化整数变量b,初始值为4printf("交换前:a=%d b=%d\n", a, b); // 输出交换前a和b的值8swap1(a, b); // 调用swap1函数,但由于a和b是值传递,不会改变main函数中的a和b的值printf("交换后:a=%d b=%d\n", a, b); // 输出交换后a和b的值,实际上并未交换printf("交换前:a=%d b=%d\n", a, b); // 再次输出交换前a和b的值swap2(&a, &b); // 调用swap2函数,通过传递a和b的地址来实现交换printf("交换后:a=%d b=%d\n", a, b); // 输出交换后a和b的值,实际上已经交换成功return 0; // 程序正常结束,返回0表示成功
}

运行结果:

在这里插入图片描述
可以看到swap1函数是不能实现交换功能的,只有swap2函数实现了我们的交换功能,这里涉及到传值调用和传址调用,我们后续会讲。

3. 函数参数

3.1 实际参数(实参)

真实传给函数的参数叫做实际参数。
实参可以是:常量、变量、表达式、函数等。
无论实参是哪一种形式,它在函数传参时必须有确定的值,以便把这些值传给形参。

3.2 形式参数(形参)

形式参数是指函数名括号里面的变量,因为形式参数只有在函数调用的时候才会被分配内存单元,所以叫形式参数。当函数调用结束之后,形式参数的内存单元就会被销毁了。除非是static关键字修饰的形式参数,生存周期更长。

上面代码的swap1swap2中的函数里面的参数abpapb都是形式参数(形参),而在main函数里面传给函数swap1ab和传给swap2&a&b是实际参数(实参)。
这里我们对函数的实参和形参进行分析:

在这里插入图片描述

代码对应的内存分配如下:
在这里插入图片描述
这里我们可以看到xy有了自己的独立空间,所以我们可以简单的认为:此时的形参相当于实参的一份临时拷贝。

4. 函数调用

4.1 传值调用

形式参数和实际参数分别占有不同的内存块,改变形参并不会影响实参,因为实参相当于实参的一份临时拷贝。

4.2 传址调用

  • 传址调用是将函数外部创建的变量的内存地址给函数参数的一种调用方式。
  • 这种方式可以让函数和函数外面的变量建立起直接的联系,也就是函数内部通过对地址解引用操作可以直接改变函数外面变量的值。

4.3 练习

  1. 写一个函数可以判断一个数是不是素数。
#include<stdio.h>
//下面函数将实现判断一个数是否是素数的功能。int is_prime(int n){if (n == 1)return 0;else{int i = 2;for (i = 2; i * i <= n; i++){if (n % i == 0)return 0;}return 1;}
}int main()
{int i = 0;int count = 0;for (i = 100; i <= 200; i++){//判断i是否为素数if (is_prime(i)){printf("%d ", i);count++;}}printf("\n%d", count);return 0;
}

运行结果:

在这里插入图片描述

  1. 写一个函数判断一年是不是闰年。
include<stdio.h>
//写一个函数判断一个年份是否为闰年
//如果是闰年返回1
//如果不是返回0
//2. 实现函数int is_leap_year(int year)
{return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}int main()
{int year = 0;int count = 0;for (year = 2000; year <= 3000; year++){//1. 函数怎样使用//TDD// test driven development//测试驱动开发if (is_leap_year(year)){printf("%d ", year);count++;}}printf("\n%d", count);return 0;
}

运行结果:

在这里插入图片描述

  1. 写一个函数实现一个整形数组的二分查找。
#include <stdio.h>// binary_search函数用于在有序整数数组arr中查找元素k,并返回其下标
// 参数arr为有序整数数组的首地址,k为要查找的元素,sz为数组的大小
// 如果找到了元素k,则返回其在数组中的下标,否则返回-1表示没找到
int binary_search(int arr[], int k, int sz)
{int left = 0; // 定义左边界left,初始值为数组的第一个元素下标0int right = sz - 1; // 定义右边界right,初始值为数组的最后一个元素下标sz-1int mid = 0; // 定义中间位置mid// 使用循环进行二分查找while (left <= right){// 计算中间位置mid,避免整数溢出使用(right-left)/2的方式mid = left + (right - left) / 2;if (arr[mid] < k) // 若中间元素小于要查找的元素k{left = mid + 1; // 缩小查找范围,将左边界left更新为mid+1}else if (arr[mid] > k) // 若中间元素大于要查找的元素k{right = mid - 1; // 缩小查找范围,将右边界right更新为mid-1}else // 若中间元素等于要查找的元素k,表示找到了{return mid; // 返回中间位置mid,表示找到了元素k的下标}}// 若循环结束仍未找到元素k,则返回-1表示没找到return -1;
}int main()
{int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 声明并初始化有序整数数组arrint k = 0; // 声明整数变量k,用于接收用户输入的要查找的元素scanf("%d", &k); // 输入要查找的元素kint sz = sizeof(arr) / sizeof(arr[0]); // 计算数组的大小// 调用binary_search函数在数组arr中查找元素kint ret = binary_search(arr, k, sz);// 判断查找结果并输出相应信息if (ret == -1){printf("没找到\n"); // 若返回值为-1表示没找到,输出"没找到"}else{printf("找到了下标是:%d", ret); // 若返回值不为-1表示找到了,输出找到的元素k的下标}return 0; // 程序正常结束,返回0表示成功
}

代码分析:
在这里插入图片描述
运行结果:

在这里插入图片描述

  1. 写一个函数,每调用一次这个函数,就会将num的值增加1。

我们使用传值调用和传址调用两种方式都可以解决这个问题。

  • 传址调用
#include<stdio.h>
void test(int* p)
{(*p)++;
}int main()
{int num = 0;test(&num);test(&num);printf("%d\n", num);return 0;
}
  • 传值调用
#include<stdio.h>
int test(int num)
{return num + 1;
}int main()
{int num = 0;num = test(num);num = test(num);printf("%d\n", num);return 0;
}

5. 函数的嵌套调用和链式访问

5.1 嵌套调用

嵌套调用就是指一个函数体内可以调用其它的函数。

下面我们通过一段代码来演示一下函数的嵌套调用

#include<stdio.h>
int test()
{int a = 0;int b = 0;return a + b;
}
void fun()
{test();printf("hehe\n");
}
int main()
{fun();return 0;
}

运行结果:

在这里插入图片描述

5.2 链式访问

链式访问就是把一个函数的返回值作为另一个函数的参数。

下面我们通过几段代码来演示函数的链式访问,这段代码将使用字符串函数,如果你不太熟悉,请自己使用文档学习,或者看博主的这篇博文【C语言进阶技巧】探秘字符与字符串函数的奇妙世界。

#include <stdio.h>
#include <string.h>int main()
{int len = strlen("abcdef"); // 使用strlen函数获取字符串"abcdef"的长度,即字符个数(不包括空字符'\0')printf("%d\n", len); // 打印获取到的字符串长度,输出为6(字符串"abcdef"有6个字符)// 函数调用:使用strlen函数直接获取字符串"abcdef"的长度,并打印结果printf("%d\n", strlen("abcdef")); // 直接打印字符串"abcdef"的长度,输出也为6return 0;
}

运行截图:

在这里插入图片描述
这段代码中临时变量len接收了strlen函数的int类型的返回值,然后其作为printf函数的参数,这就是链式访问,或者不用临时变量接收,直接放入printf中,结果是一样的效果。
再看下面一段代码:

#include<stdio.h>int main()
{printf("%d ", printf("%d ", printf("%d ", 43)));return 0;
}

想知道这段代码的打印结果,我们就得清楚printf函数的返回值是什么,表示的是什么含义,这里我们查询文档,给出如下图片:

在这里插入图片描述
所以第三个printf会打印三个字符:43和空格,返回值为3,然后第二个printf函数以第三个printf函数的返回值3作为参数,打印两个字符:3和空格,返回值为2,接着第一个printf函数以第二个printf函数的返回值2为参数,打印两个字符:2和空格,返回值为2,所以最后屏幕上打印的应该是43 3 2

运行结果:

在这里插入图片描述

6. 函数的声明和定义

6.1 函数的声明

  • 函数的声明所实现的功能是告诉编译器一个函数的参数、返回值类型、名称。但函数的声明只是一个声明,它不能决定函数是否存在,也就是说,如果这个函数只存在声明,而没有定义在语法也是通过的。
  • 函数的声明一般放在函数的使用之前,要满足先声明再使用。
  • 函数的声明应该放在头文件中。

6.2 函数的定义

函数的定义是函数声明的一种特殊形式,它是指函数的具体实现,交代函数的功能实现。

下面我们通过具体的代码来介绍函数的声明与定义:

#include<stdio.h>
//函数声明
//函数声明中函数只需要包含函数参数的类型就可以了,但是加上变量名也可以
int Add(int, int);
//int Add(int a, int b);int main()
{int a = 0;int b = 0;//输入两个相加的数scanf("%d%d", &a, &b);//函数调用int c = Add(a, b);//打印printf("%d", c);
}//函数定义
int Add(int a, int b)
{return a + b;
}

我们也可以省略函数声明,直接在main函数的前面写Add函数的定义,因为定义是一种特殊的声明,在实现函数功能的同时它也包括了函数的声明:

#include<stdio.h>
//函数定义是一种特殊的声明
int Add(int a, int b)
{return a + b;
}
int main()
{int a = 0;int b = 0;//输入两个相加的数scanf("%d%d", &a, &b);//函数调用int c = Add(a, b);//打印printf("%d", c);
}

2.3 项目中函数的声明和定义的常见使用方式

实际项目开发中我们一般会这样去进行Add函数的声明和定义:

  1. Add函数的声明放在头文件Add.h

在这里插入图片描述

  1. Add函数的定义放在Add.c文件中

在这里插入图片描述
3. 在test.c或者其它文件中使用Add函数

在这里插入图片描述`

  • 当包含函数声明的头文件Add.h和包含函数定义的文件Add.c在同一个项目(解决方案)中时我们想要使用Add函数只需要包含一下头文件就行了,但因为这是我们自己创建的头文件,为了区分,用""包含头文件:
#include "Add.h"

运行结果:

2.3.1 项目中分文件来写的目的

  1. 在公司写代码是需要协作的,项目分文件来写有如下好处:
    • 分模块来写,方便协作,后期整合在一起。
    • 可以将代码的实现和声明分离。

下面一张图,希望能帮助你理解:
在这里插入图片描述
2. 将代码的实现与声明分离

还是以Add函数举例子,此时将Add.hAdd.c分开来写,如果程序员K想把这个项目卖了来获取收益,但是它不想让别人知道他是怎么写的,这个时候就可以把Add.c文件转换为Add.lib文件,这样就可以达到既让客户使用又不让客户知道代码是如何实现的目的,因为静态库是一种预先编译好的代码集合,我们是看不懂的。

下面我们来演示一下将Add.c转换为静态库文件并使用它:
3. 新建一个解决方案,并创建新的Add.hAdd.c
在这里插入图片描述

  1. 鼠标右键点击项目的属性。

在这里插入图片描述
在这里插入图片描述
.3. 将配置类型改为静态库。

在这里插入图片描述
4. 代码编译运行,生成静态库文件。

在这里插入图片描述
5. 鼠标右击Add.c,打开所在文件夹,并进入Debug(调试)文件中,可以看见Add.lib文件。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
6. 我们打开我们的test.c文件,并把刚刚生成的Add.lib和我们写好的代码声明Add.h放进这个项目的文件夹中。

在这里插入图片描述

  1. 只包含头文件Add.h,编译并运行。
    在这里插入图片描述
    如果仅仅只包含Add.h头文件,可以发现程序是报错了的,Add.lib静态库文件使用下面代码才能使用。
#pragma comment (lib,"Add.lib")
  1. 加入上述代码编译并运行。

7. 函数递归

7.1 递归的定义

程序调用自身的编程技巧叫做递归。
递归作为一种算法在程序设计语言中广泛的被应用。递归这种算法可以把一个复杂的问题层层转化为很多和原问题相似的子问题来求解,这种策略只需少量的程序就可描述出解题过程中所需要的多次重复计算,大大减少了代码量。

7.2 递归的两个必要条件

  • 递归必须要有限制条件,当不满足这个限制条件后,递归便不再调用。
  • 每次递归后必须要越来越接近这个限制条件。

最简单的递归:

#include<stdio.h>
int main()
{printf("hehe");main();return 0;
}

但是这个递归没有限制条件,会导致栈溢出。

在这里插入图片描述

7.2.1 练习1

接受一个整形值(无符号),按照顺序打印它的每一位。
例如:
输入:1234,输出 1 2 3 4

参考代码:

#include<stdio.h>
void print(unsigned int n)
{if (n > 9){print(n / 10);}printf("%u ", n % 10);
}
int main()
{unsigned int num = 0;scanf("%u", &num);print(num);return 0;
}
  • 注意%u是表示无符号十进制整数的格式化输入、输出说明符。

分析:
在这里插入图片描述

  • 注意,函数每调用一次,就会在栈区开辟一个栈帧,如果递归调用没有限制条件或者每一次调用后没有更接近这个条件,就不会不停的在栈区开辟栈帧造成栈溢出。

运行结果:

在这里插入图片描述

7.2.2 练习2

编写函数,不允许使用临时变量,求字符串长度。
这里我们使用递归来实现,但是如果你对更多方法感兴趣,可以看博主这篇文章【C语言进阶技巧】探秘字符与字符串函数的奇妙世界。

递归实现,参考代码:

#include <stdio.h>// 自定义函数 my_strlen,用于计算字符串的长度(不包括结尾的 '\0')
// 参数 str:指向待计算长度的字符串的指针
// 返回值:字符串的长度,以 size_t 类型表示
size_t my_strlen(char* str)
{// 判断字符串是否为空,即是否为 '\0' 结尾if (*str == '\0')return 0; // 若为空,返回长度 0else// 若不为空,递归调用 my_strlen 函数,计算剩余子串的长度,并加 1(包含当前字符)return 1 + my_strlen(str + 1);
}int main()
{char arr[] = "abc"; // 定义一个字符数组 arr,存储字符串 "abc"(包含结尾的 '\0')// 计算字符串的长度(不包括结尾的 '\0'),调用自定义的 my_strlen 函数size_t len = my_strlen(arr);printf("%d\n", len); // 输出字符串的长度,注意这里使用 %zu 作为格式化字符串,表示 size_t 类型return 0;
}

分析:在这里插入图片描述
运行结果:

在这里插入图片描述

7.3 递归与迭代

递归和迭代有着极其高的相似性,可以这样来理解递归:

递:迭代
归:回归

一般情况下:递归比迭代在性能上要差,但代码量比迭代要少,且如果一个程序你可以用迭代来实现,就相应的也可以使用递归来实现。

7.3.1 练习3

求n的阶乘。(不考虑溢出)

参考代码:

#include <stdio.h>// 自定义函数 factorial,用于计算 n 的阶乘
// 参数 n:需要计算阶乘的整数
// 返回值:n 的阶乘,以 int 类型表示
int factorial(int n)
{// 判断 n 是否为 1,如果是,则返回 1(1 的阶乘为 1)if (n == 1)return 1;else// 若 n 不是 1,则递归调用 factorial 函数,计算 n-1 的阶乘,并与 n 相乘return n * factorial(n - 1);
}int main()
{int n = 0; // 定义整数变量 n,用于存储用户输入的值scanf("%d", &n); // 从标准输入读取用户输入的整数,存入 n 中// 调用自定义的 factorial 函数,计算 n 的阶乘,并输出结果printf("%d\n", factorial(n));return 0;
}

分析:

在这里插入图片描述

运行结果:

在这里插入图片描述

7.3.2 练习4

求第n个斐波拉契数,不考虑溢出。

参考代码:

#include <stdio.h>// 定义递归函数Fib,用于计算斐波那契数列的第n项
// 参数n为要计算的斐波那契数列的项数
int Fib(int n)
{// 当n小于等于2时,斐波那契数列的第n项为1,直接返回1if (n <= 2){return 1;}else{// 当n大于2时,斐波那契数列的第n项为前两项之和// 调用Fib(n-1)和Fib(n-2)分别计算第n-1项和第n-2项// 然后将它们相加并返回return Fib(n - 1) + Fib(n - 2);}
}int main()
{int n = 0; // 声明整数变量n,用于接收用户输入的斐波那契数列项数scanf("%d", &n); // 输入要计算的斐波那契数列项数n// 调用Fib函数计算斐波那契数列的第n项并输出结果printf("%d", Fib(n));return 0; // 程序正常结束,返回0表示成功
}                                                     

分析过程:
在这里插入图片描述

7.3.3 递归和迭代的区别

我们可以发现上面两道题目,如果我们使用递归的思路来实现的话:

  • 使用Fib这个函数的话,我们发现计算第50个斐波拉契数要花费很多时间。
  • 使用factorial这个函数计算10000的阶乘(不考虑结果的正确性),程序会崩溃。

这是为什么呢?
我们可以对Fib函数做一下修改,计算一下Fib(3)被调用了多少次,假设此时 n = 15 。 n=15。 n=15

#include <stdio.h>//定义全局变量count来计数,F(3)重复计算的次数
int count = 0;
// 定义递归函数Fib,用于计算斐波那契数列的第n项
// 参数n为要计算的斐波那契数列的项数
int Fib(int n)
{// 当n小于等于2时,斐波那契数列的第n项为1,直接返回1if (n <= 2){return 1;}if (n == 3){count++;}else{// 当n大于2时,斐波那契数列的第n项为前两项之和// 调用Fib(n-1)和Fib(n-2)分别计算第n-1项和第n-2项// 然后将它们相加并返回return Fib(n - 1) + Fib(n - 2);}
}int main()
{int n = 0; // 声明整数变量n,用于接收用户输入的斐波那契数列项数scanf("%d", &n); // 输入要计算的斐波那契数列项数n// 调用Fib函数计算斐波那契数列的第n项并输出结果printf("%d", Fib(n));printf("\n%d", count);return 0; // 程序正常结束,返回0表示成功
}

运行结果:

在这里插入图片描述
可以看到虽然 n = 50 n = 50 n=50时的斐波拉契额数太大导致数据溢出,但是Fib(3)被调用的次数被成功计算出来了,为一个9位数,所以我们也可以知道,之所以计算第50个斐波拉契数这么慢,是因为有很多值是被重复计算了的。
另外我们可以对参数很大时候的factorial函数进行调试,程序会出现这样的问题:

在这里插入图片描述
这是因为n太大,所以函数栈帧开辟的太多,导致空间不够,造成了栈溢出(Stack overflow)的情况。因为系统分配给程序的栈空间是有限的,如果出现了死循环,或者死递归,这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称之为栈溢出。

那我们该如何解决这种问题呢?

  1. 将递归改为非递归。
  2. 给程序增加记忆功能,减少对很多中间过程的重复计算,如使用static对象代替局部变量(即栈对象)。

比如下面两个函数就采用了非递归的方法来实现求n的阶乘和求第n个斐波拉契数:

#include <stdio.h>// 求n的阶乘
// 参数n为要计算阶乘的数
// 返回值为n的阶乘结果
int factorial(int n)
{int i = 0;int ret = 1;// 使用for循环计算n的阶乘for (i = 1; i <= n; i++){ret *= i;}return ret;
}// 求第n个斐波拉契数
// 参数n为要计算的斐波拉契数列的项数
// 返回值为第n个斐波拉契数
int Fib(int n)
{int a = 1; // 斐波拉契数列的第一项为1int b = 1; // 斐波拉契数列的第二项为1int c = 1; // 用于保存第n个斐波拉契数// 使用for循环计算第n个斐波拉契数for (int i = 3; i <= n; i++){c = a + b; // 计算第i项的值,即前两项之和a = b; // 更新第i-2项为第i-1项的值b = c; // 更新第i-1项为第i项的值}return c;
}int main()
{int n = 0;printf("请输入一个整数n:");scanf("%d", &n);// 调用factorial函数计算n的阶乘,并输出结果printf("%d的阶乘结果为:%d\n", n, factorial(n));// 调用Fib函数计算第n个斐波拉契数,并输出结果printf("第%d个斐波拉契数为:%d\n", n, Fib(n));return 0;
}

运行结果:

在这里插入图片描述

  • 很多问题是以递归的形式来解释的,这只是由于它比非递归的形式更加清晰。
  • 一般情况下,如果使用迭代往往会比递归的效率更高,但是代码的可读性会稍微差一点。
  • 当一个问题很复杂,使用迭代难以实现的时候,可以使用递归实现,递归的简洁性可以弥补它所带来的运行开销。
  • 递归计算一个问题,往往是从结果开始计算,直到遇见已知值时就会停止递归,是从后往前算。而迭代是用已知量来推未知量,是从前往后计算。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/127398.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微服务架构之路1,服务如何拆分?使用微服务的注意事项?

目录 一、前言二、单体服务的弊端三、微服务化四、服务如何拆分&#xff1f;五、使用微服务的注意事项1、服务如何定义2、服务如何发布和订阅3、服务如何监控4、服务如何治理5、故障如何定位 大家好&#xff0c;我是哪吒。 一、前言 微服务已经是Java开发的必备技能&#xff…

Python画图之动态爱心

Python画出动态爱心&#xff08;有趣小游戏&#xff09; 一、效果图二、Python代码 一、效果图 二、Python代码 import random from math import sin, cos, pi, log from tkinter import *CANVAS_WIDTH 640 # 画布的宽 CANVAS_HEIGHT 480 # 画布的高 CANVAS_CENTER_X CANV…

线段树 区间赋值 + 区间加减 + 求区间最值

线段树好题&#xff1a;P1253 扶苏的问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 区间赋值 区间加减 求区间最大。 对于区间赋值和区间加减来说&#xff0c;需要两个懒标记&#xff0c;一个表示赋值cover&#xff0c;一个表示加减add。 区间赋值的优先级大于区间加…

大麦协议开发

1. 用户抢购请求处理&#xff1a; - 后端实现&#xff1a;在后端&#xff0c;您可以创建一个用于处理用户抢购请求的接口。当用户发起抢购请求时&#xff0c;后端会根据一定的算法和逻辑来处理请求。例如&#xff0c;可以使用分布式锁来保证只有一个用户能够成功抢购。示例后端…

VS2017制作安装包如何将整个文件夹添加进依赖项中

找到安装项目右键view-文件系统 找到Application Folder 右键Add-Folder 如Python38 选中创建的 Python38 在右侧的空白处粘贴要复制的文件即可。文件多&#xff0c;等待时间较长

玩转硬件之Micro:bit的玩法(三)——计步器

随着技术的发展&#xff0c;现在智能手机和智能手表已经走进千家万户&#xff0c;所以大家对于计步器可能不陌生&#xff0c;计步器是一种用于计算行走步数的装置。它通常是一个小型电子设备&#xff0c;可以佩戴在身体上&#xff0c;如腕带、腰带或口袋中。计步器通过感应人体…

树结构及其算法-二叉查找树

目录 树结构及其算法-二叉查找树 C代码 树结构及其算法-二叉查找树 二叉树在建立的过程中是根据“左子树 < 树根 < 右子树”的原则建立的&#xff0c;因此只需从树根出发比较键值即可&#xff0c;如果比树根大就往右&#xff0c;否则往左而下&#xff0c;直到相等就找…

浅谈安科瑞无线测温产品在南非某变电站的应用

摘要&#xff1a;随着电力工业的发展&#xff0c;对设备的安全性、可靠性要求越来越高。在这种条件下&#xff0c;高压设备的无线测温系统应运而生。这种技术是将内置电池或电流感应和无线发射模块的测温传感器安装于各测温点&#xff0c;由于其体积小&#xff0c;且无需任何接…

WordPress外链页面安全跳转插件

老白博客我参照csdn和腾讯云的外链跳转页面&#xff0c;写了一个WordPress外链安全跳转插件&#xff1a;给网站所有第三方链接添加nofollow标签和重定向功能&#xff0c;提高网站安全性。插件包括两个样式&#xff0c;由于涉及到的css不太一样&#xff0c;所以分别写了两个版本…

矢量图形编辑软件illustrator 2023 mac中文软件特点

illustrator 2023 mac是一款矢量图形编辑软件&#xff0c;用于创建和编辑排版、图标、标志、插图和其他类型的矢量图形。 illustrator 2023 mac软件特点 矢量图形&#xff1a;illustrator创建的图形是矢量图形&#xff0c;可以无限放大而不失真&#xff0c;这与像素图形编辑软…

赋能制造业高质量发展,释放采购数字化新活力——企企通亮相武汉2023国际智能制造创新论坛

摘要 “为应对成本上升、供应端不稳定、供应链上下游协同困难、决策无数据依据等问题&#xff0c;利用数字化手段降本增效、降低潜在风险十分关键。在AI等先进技术发展、供应链协同效应和降本诉求等机遇的驱动下&#xff0c;采购供应链数字化、协同化成为企业激烈竞争的优先选…

几种常见的接地类型详解

接地作为一种应用最为广泛的电气安全措施&#xff0c;是指电力系统和电气装置的中性点、电气设备的外露导电部分和装置外导电部分经由导体与大地相连。接地的作用主要是防止人身遭受电击、设备和线路遭受损坏、预防火灾和防止雷击、防止静电损害和保障电力系统正常运行。按其功…

【高光谱与多光谱:空间-光谱双优化模型驱动】

A Spatial–Spectral Dual-Optimization Model-Driven Deep Network for Hyperspectral and Multispectral Image Fusion &#xff08;一种用于高光谱与多光谱图像融合的空间-光谱双优化模型驱动深度网络&#xff09; 深度学习&#xff0c;特别是卷积神经网络&#xff08;CNN…

设计模式_观察者模式

观察者模式 介绍 设计模式定义案例问题堆积在哪里解决办法观察者是行为型设计模式 多个对象 观察 1个对象小强考试完 成绩公布了 家长/同学得知成绩后 做出不同反应一个一个通知很麻烦 先通知谁 也有讲究的 信息发布方 抽象出一个信息管理类 负责管理监听者 类图 代码 Obse…

如何搭建稳定独享的美国IP?优质美国静态住宅代理IP哪里有?

相信很多做跨境电商或外贸如TikTok shop、Facebook商店、Amazon、领英的玩家都需要搭建独享的美国IP环境来运营店铺&#xff0c;那么如何搭建稳定独享的IP环境呢&#xff1f;加下来为你详细介绍&#xff0c;并为你推荐优质的美国静态住宅代理IP&#xff0c;助力您的跨境业务。 …

ubuntu PX4 vscode stlink debug设置

硬件 stlink holybro debug板 pixhawk4 安装openocd 官方文档&#xff0c;但是第一步安装建议从源码安装&#xff0c;bug少很多 github链接 编译安装&#xff0c;参考 ./bootstrap (when building from the git repository)./configure [options]makesudo make install安装后…

Spring Cloud的ElasticSearch的进阶学习

目录 数据聚合 Bucket示例 Metric示例 RestAPI实现聚合 自动补全 使用拼音分词 自定义分词器 实现自动补全 RestAPI实现自动补全功能 数据同步 同步调用 异步通知 监听binlog 数据聚合 聚合可以实现对文档数据的统计、分析、运算。聚合常见的有三类&#xff1a; …

大模型之十九-对话机器人

大语言模型的最早应用是Chatbot&#xff0c;其实我最早接触语义理解在2014年&#xff0c;2014年做智能音箱的时候&#xff0c;那时也是国内第一批做智能音箱的&#xff0c;在现在看起来当时的智能音箱比较傻&#xff0c;很多问题无法回答&#xff0c;长下文效果也不好&#xff…

Linux安装sysv-rc-conf报错:出现NO_PUBKEY...问题,急需安装证书的情况

Linux下安装MySQL时&#xff0c;出现一个使用chkconfig命令&#xff0c;但无该命令的情况&#xff01; chkconfig --add mysql # 出现chkconfig command not found于是就展开了一次替换的行动&#xff0c;将chkconfig替换为sysv-rc-conf 第一步&#xff1a; 尝试直接安装&am…

飞桨国际化应用案例:挪威广告企业Adevinta应用PaddleOCR提质增效

Adevinta&#xff0c;位于挪威奥斯陆的跨国在线分类广告公司&#xff0c;以其全球市场的图像处理API为特色。Adevinta的主要使命是构建全球买家和卖家之间的桥梁&#xff0c;其在线市场运营覆盖11个国家&#xff0c;拥有众多备受信任的品牌&#xff0c;如荷兰的marktplaats、德…