【C语言初阶】之函数

news2025/7/27 10:11:58

【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中,包括\0
	printf("%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的值赋给temp1
    x = y; // 将y的值赋给x,但由于x和y是形式参数,不会影响到main函数中的a和b
    y = 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,初始值为3
    int b = 4; // 声明并初始化整数变量b,初始值为4
    printf("交换前:a=%d b=%d\n", a, b); // 输出交换前a和b的值8
    swap1(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,初始值为数组的第一个元素下标0
    int right = sz - 1; // 定义右边界right,初始值为数组的最后一个元素下标sz-1
    int 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 }; // 声明并初始化有序整数数组arr
    int k = 0; // 声明整数变量k,用于接收用户输入的要查找的元素
    scanf("%d", &k); // 输入要查找的元素k
    int sz = sizeof(arr) / sizeof(arr[0]); // 计算数组的大小
    // 调用binary_search函数在数组arr中查找元素k
    int 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"的长度,输出也为6

    return 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; // 若为空,返回长度 0
   else
       // 若不为空,递归调用 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,直接返回1
    if (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,直接返回1
    if (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; // 斐波拉契数列的第一项为1
    int b = 1; // 斐波拉契数列的第二项为1
    int 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.coloradmin.cn/o/1156787.html

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

相关文章

基于秃鹰算法的无人机航迹规划-附代码

基于秃鹰算法的无人机航迹规划 文章目录 基于秃鹰算法的无人机航迹规划1.秃鹰搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要&#xff1a;本文主要介绍利用秃鹰算法来优化无人机航迹规划。 1.秃鹰搜索算法 …

php实现关键词过滤

1. 构建关键词库 首先&#xff0c;你需要构建一个包含敏感关键词的库。你可以将这些敏感关键词存储在一个数组中&#xff0c;或者将它们存储在数据库中。 $keywords array(敏感词1,敏感词2,敏感词3,// 其他敏感词 ); 2. 实现关键词过滤函数 接下来&#xff0c;你需要实现一个…

对云计算的热爱,王坚院士为何从未减弱,而且更强烈了?

引言&#xff1a;云计算作为基础设施 未来将大放异彩吗&#xff1f; 【阿明观察 &#xff5c; 热点关注】 许久未见王坚院士的演讲&#xff0c;在2023杭州云栖大会&#xff0c;我看到他现场的分享指出&#xff0c;由于GPT的模型出现&#xff0c;计算对科技创新自身在产生革命…

有什么可以自动保存微信收到的图片和视频的方法么

8-1 在一些有外勤工作的公司里&#xff0c;经常会需要在外面工作的同事把工作情况的图片发到指定微信或者指定的微信群里&#xff0c;以记录工作进展等&#xff0c;或者打卡等&#xff0c;对于外勤人员来说&#xff0c;也就发个图片的事&#xff0c;但是对于在公司里收图片的人…

【ECharts】保姆级从空白项目到Echarts地图

前提 请先创建好一个空白的vue项目&#xff0c;准备好一个用于写Echarts地图的空白vue文件 准备好一个空白的vue页面后&#xff0c;只需要跟着我一步一步做 就可以达到我最后展示的效果&#xff01;&#xff01;&#xff01; 资源下载 ECharts npm install echarts地图 jso…

android查漏补缺(8)Binder framework架构和调用方法

1.Binder简介 Binder是android系统中实现进程间通信的主要组件&#xff0c;包括各种AMS,PMS,SMS等服务和APK的通信都是通过binder实现。但是调用过PMS的同学肯定会有疑问&#xff0c;既然是进程通信&#xff0c;怎么没有消息的发送和接收&#xff0c;为什么调用不同进程的服务的…

Git工作原理和常见问题处理方案

博客定位Git工作区域工作区域划分暂存区设计目的 Git基本操作核心操作初始化和配置指令 HEAD指针Git版本回滚指令介绍reset模式reset hard使用场景reset soft使用场景reset mixed使用场景reset使用注意事项checkout使用场景 Git分支管理什么是分支分支应用场景分支相关指令被合…

OS 死锁

资源问题 引起死锁的主要是需要采用互斥访问方法的、不可被抢占的资源 可重用资源和可消耗资源 可重用资源 定义&#xff1a;一种可供用户重复使用多次的资源 性质&#xff1a; 每个可重用资源中的单元&#xff0c;只能分配给一个进程使用&#xff0c;不允许多个进程共享…

NVME CMB原理和常规使用方案详解

什么是CMB 在NVMe Express 1.2 Spec中开始支持一个特性&#xff0c;那就是CMB&#xff08;Controller Memory Buffer&#xff09;&#xff0c;是指SSD控制器内部的读写存储缓冲区&#xff0c;与HMB&#xff08;Host Memory Buffer&#xff09;的不同处在于所使用的内存地址位于…

ffplay是做什么的

目录 ffplay是什么 一、ffplay的基本使用 二、ffplay播放控制 三、常用参数 ffplay是什么 ffplay 是FFmpeg提供的一个极为简单的音视频媒体播放器&#xff08;由ffmpeg库和SDL库开发&#xff09;&#xff0c;可以用于音视频播放、可视化分析 &#xff0c;提供音视频显示和播…

微信小程序面试题之理论篇

本文内容&#xff0c;来源于极客学院的分享&#xff0c;这里只做引用。 说说你对微信小程序的理解?优缺点? 背景 小程序与H5 优缺点 优点&#xff1a;缺点&#xff1a; 说说微信小程序的生命周期函数有哪些&#xff1f; 应用的生命周期页面的生命期组件的生命周期执行过程 应…

从零开始的目标检测和关键点检测(二):训练一个Glue的RTMDet模型

从零开始的目标检测和关键点检测&#xff08;二&#xff09;&#xff1a;训练一个Glue的RTMDet模型 一、config文件解读二、开始训练三、数据集分析四、ncnn部署 从零开始的目标检测和关键点检测&#xff08;一&#xff09;&#xff1a;用labelme标注数据集 从零开始的目标检测…

Jmeter 汉化中文语言

找到 bin -> jmeter.propertise 修改参数&#xff1a;languageen --> languagazh_CN OK&#xff01;

上位机底部栏 UI如何设置

上位机如果像设置个多页面切换&#xff1a; 位置&#xff1a; 代码如下&#xff1a; "tabBar": {"color": "black","selectedColor": "#d43c33","borderStyle":"black","backgroundColor": …

EVM6678L 开发教程: IBL-TFTP 引导 elf 文件

目录 EVM6678L 开发教程: IBL-TFTP 引导 elf 文件安装 Tftpd64测试工程测试说明 EVM6678L 开发教程: IBL-TFTP 引导 elf 文件 参考: "C:\ti\mcsdk_2_01_02_06\tools\boot_loader\examples\i2c\tftp\docs\README.txt" 此教程介绍如何在 EVM6678L 开发板上实现 IBL-…

【面试经典150 | 链表】旋转链表

文章目录 Tag题目来源题目解读解题思路方法一&#xff1a;遍历 其他语言python3 写在最后 Tag 【单向链表】 题目来源 61. 旋转链表 题目解读 旋转链表&#xff0c;将链表的每个节点向右移动 k 个位置。 解题思路 方法一&#xff1a;遍历 本题题目意思清晰&#xff0c;实现…

【Linux】jdk Tomcat MySql的安装及Linux后端接口部署

一&#xff0c;jdk安装 1.1 上传安装包到服务器 打开MobaXterm通过Linux地址连接到Linux并登入Linux&#xff0c;再将主机中的配置文件复制到MobaXterm 使用命令查看&#xff1a;ll 1.2 解压对应的安装包 解压jdk 解压命令&#xff1a;tar -xvf jdk 加键盘中Tab键即可…

企业级JAVA、数据库等编程规范之命名风格 —— 超详细准确无误

&#x1f9f8;欢迎来到dream_ready的博客&#xff0c;&#x1f4dc;相信你对这两篇博客也感兴趣o (ˉ▽ˉ&#xff1b;) &#x1f4dc; 表白墙/留言墙 —— 初级SpringBoot项目&#xff0c;练手项目前后端开发(带完整源码) 全方位全步骤手把手教学 &#x1f4dc; 用户登录前后端…

作为网工有必要了解一下什么是SRv6?

什么是SRv6&#xff1f; 【微|信|公|众|号&#xff1a;厦门微思网络】 【微思网络http://www.xmws.cn&#xff0c;成立于2002年&#xff0c;专业培训21年&#xff0c;思科、华为、红帽、ORACLE、VMware等厂商认证及考试&#xff0c;以及其他认证PMP、CISP、ITIL等】 SRv6&…