C生万物 | 函数的讲解与剖析【内附众多案例详解】

一、函数是什么

首先我们来了解一下函数是什么
数学中我们常见到函数的概念。但是你了解C语言中的函数吗
百度百科中对函数的定义链接

  • 在计算机科学中子程序英语Subroutine, procedure, function, routine, method,
    subprogram, callable unit是一个大型程序中的某部分代码 由一个或多个语句块组
    成。它负责完成某项特定任务而且相较于其他代 码具备相对的独立性。
  • 一般会有输入参数并有返回值提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

二、C语言中函数的分类

1、库函数

为什么会有库函数

① 我们知道在我们学习C语言编程的时候总是在一个代码编写完成之后迫不及待的想知道结果想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能将信息按照一定的格式打印到屏幕上【printf】
② 在编程的过程中我们会频繁的做一些字符串的拷贝工作【strcpy】
③ 在编程是我们也计算总是会计算n的k次方这样的运算【pow】

  • 像上面我们描述的基础功能它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到
    为了支持可移植性和提高程序的效率所以C语言的基础库中提供了一系列类似的库函数方便程序员进行软件开发。
  • 那我们要如何去学习库函数呢给大家介绍一个网站www.cplusplus如果有遇到不懂的、不会使用的函数都可以到上面去寻找

在这里插入图片描述

我也在上面学习了很多的库函数总结一下C语言常用的库函数都有

  • IO函数
  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数

接下去我会参照文档给大家将两个常用的库函数来教会大家如何入阅读英文文档

strcpy

char * strcpy ( char * destination, const char * source );

在这里插入图片描述

  • 再到代码中来看看它的实际应用场景
//strcpy在拷贝的时候'\0'也会被拷贝过来
int main(void)
{
	char arr1[] = "############";
	char arr2[] = "hello bit";
	strcpy(arr1, arr2);

	printf("%s\n", arr1);
	return 0;
}
  • 再来看看运行结果

在这里插入图片描述
在这里插入图片描述


接下去再来看一库函数【memset】

memset

void * memset ( void * ptr, int value, size_t num );

在这里插入图片描述

  • 一样来看一下代码该如何书写
char arr[] = "hello bit";
memset(arr, 'x', 5);
printf("%s\n", arr);
  • 来看一下上面这段代码的执行结果。

在这里插入图片描述

  • 除此之外【memset】还有一个很重要的功能就是数组的初始化这个我们在后面的学习中会频繁使用到

在这里插入图片描述
在这里插入图片描述

但是库函数必须知道的一个秘密就是使用库函数必须包含 #include 对应的头文件。
这里对照文档来学习上面几个库函数目的是掌握库函数的使用方法。

再给大家介绍两个C语言的官网
en.cppreference.com【英文版】
zh.cppreference.com【中文版】


2、自定义函数【⭐⭐⭐】

  • 如果库函数能干所有的事情那还要程序员干什么
  • 所以更加重要的是【自定义函数
  • 自定义函数和库函数一样有函数名返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间

函数的组成

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

我们首先来举一个栗子🌰

写一个函数可以找出两个整数中的最大值。

int Get_Max(int x, int y)
{
	return (x > y ? x : y);
}
int main(void)
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);

	int max = Get_Max(a, b);
	printf("二者中的较大值为%d\n", max);
	return 0;
}
  • 然后给大家讲解一下这个函数

在这里插入图片描述

接下去的一个例子如果对函数的【实参】和【形参】不太了解的小伙伴可以先去看一下下一个模块而且还会涉及到【传值调用】和【传址调用】的概念如果有不懂的也先去看一下再来看这个例子。因为我这些都会涉及到

  • 接下去我们再来举一个例子也是函数这一块最经典的案例【数值交换】
  • 首先来看看代码我将交换两个数这个逻辑单独封装成了一个函数因为它是作为一个功能出现的
void swap(int x, int y)
{
	int t = x;
	x = y;
	y = t;
}
int main(void)
{
	int a = 10;
	int b = 20;
	
	printf("交换前a = %d, b = %d\n",a, b);
	swap(a, b);
	printf("交换后a = %d, b = %d\n", a, b);
	return 0;
}
  • 来看一下运行结果。可以看到两个数并没有发生交换

在这里插入图片描述

  • 那有小伙伴就很诧异这是为什么呢我们一起来调试分析一下

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 上面是笼统地看我们通过地址再来看看

在这里插入图片描述

你可以这么认为形参实例化之后其实相当于实参的一份临时拷贝

  • 上面这一个叫做【传值调用】函数内部形参的修改是不会影响实参的接下来我们来讲讲【传址调用】
  • 虽然指针这一块我还没有讲到但是我在初始C语言那块儿已经有讲到指针这个内容了如果不了解的可以先去看看一下。因为真正地修改一个函数外部的实参值我们只能使用对指针进行解引用来进行修改然后如果你继续学下去的话还可以使用【二级指针】【C++引用】的方式也是可以做到的这里我就讲一级指针接收地址这一块
void swap(int* px, int* py)
{
	int t = *px;
	*px = *py;
	*py = t;
}
  • 先看一下代码然后我们再通过DeBug来展开分析一下

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 最后来看一下运行结果

在这里插入图片描述
讲到这里相信你对这个案例应该有了一个深刻的理解了

三、函数的参数

1、实际参数实参

真实传给函数的参数叫实参

  • 对于实参而言它可以是【常量】、【变量】、【表达式】、【函数】等。我们到VS里来测试一下

在这里插入图片描述

  • 无论实参是何种类型的量在进行函数调用时它们都必须有确定的值以便把这些值传送给形参

2、形式参数形参

  • 形式参数是指函数名后括号中的变量因为形式参数只有在函数被调用的过程中才实例化分配内存单元所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

所以我们可以总结出来一句话

【函数调用时实参传递给形参形参是实参的一份临时拷贝形参的改变不影响实参】

四、函数的调用

这里我们再来说一下函数的【传值调用】和【传址调用】这一块在上面的案例中也涉及到了

1、传值调用

函数的形参和实参分别占有不同内存块对形参的修改不会影响实参

在这里插入图片描述

2、传址调用

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系也就是函数内部可以直接操作函数外部的变量。

在这里插入图片描述

3、专项练习

看了这么多接下来就要来实践一下了我们通过四道练习题来进行训练

3.1 素数判断

  • 首先是对于素数的一个判断。【素数】又叫质数。素数指的是“大于1的整数中只能被1和这个数本身整除的数”
  • 知道了规则那代码就好写了。我们来输出一下1~100的所有素数。在外部写一个循环就可以获取1 - 100之间的数字了然后对于素数的求解可以单独封装为一个函数

  • 首先来思考需要传入哪些参数很明显只需要传入被判断的数字即可然后就要去思考这个函数的返回值可以这样设定在函数内部若是判断出其为素数那么就返回1否则就返回0然后在主函数外部进行一个判断即可
for (int i = 1; i <= 100; ++i)
{
	if (IsPrime(i) == 1)
	{
		printf("%d是素数\n",i);
	}
}
  • 接下去我们来看看函数的部分
/*素数判断*/
int IsPrime(int n)
{
	if (n <= 1)
		return 0;
	else if (n == 2)
		return 1;
	else 
	{
		for (int i = 2; i < sqrt(n) + 1; ++i)
		{
			if (n % i == 0)
			{
				return 0;
			}
		}
	}

	return 1;
}
  • 抛开其他部分我们先来看主体思考一下为什么这个循环的结束条件我要写成sqrt(n) + 1当然这里是开区间你也可以写成闭区间的形式<= sqrt(n) 设想一下一个数16 = 2 * 816还可以表示成16 = 4 * 4那我们就可以发现若是找到一个16的公因子【2】其实就可以不做判断了直接return 0即可sqrt(n) 其实就相当于这个【4】前面的2已经到了我们就可以这个算数平方根作为分解点不需要再往下找了
  • 再举一个例子9 = 3 * 3sqrt(9) = 3我们在进行判断的时候i为3时其实就不需要在向下判断了因为9对3取余为0因此【9】就不是一个素数了这么说你应该可能明白了素数判断的方法有很多种大家主要先记住这一种即可
for (int i = 2; i < sqrt(n) + 1; ++i)
{
	if (n % i == 0)
	{
		return 0;
	}
}
  • 然后其他的部分就是对于特殊情况的判断也就是【1】和【2】这两个数进行一个单独的判断
if (n <= 1)
	return 0;
else if (n == 2)
	return 1;
  • 然后我们来看一下运行结果

在这里插入图片描述

3.2 闰年判断

  • 接下去再进行一个闰年判断的练习
  • 【闰年】公历年份是4的倍数且不是100的倍数为普通闰年。公历年份是整百数且必须是400的倍数才是世纪闰年
  • 一样我们先将主函数写好。需求是输出一下1000~2000之间的所有闰年
for (int i = 1000; i <= 2000; ++i)
{
	if (Isleap(i) == 1)
	{
		printf("%d ", i);
	}
}
  • 然后的各方面条件和上面是一样的便不做详解我们来看看函数体
  • 这一块我给出两种方法第一种就是直接了当一些若是能被4整数但是不能被100整数或者可以被400整数的数那就是一个闰年此时返回1即可反之返回0利用到了【逻辑操作符
  • 第二种方法就是一个if的嵌套判断这里要注意的一点是对400取余的条件外面要写成【if】不要写成【else if】否则两个判断条件就只会进去一个了那就回漏掉几个年份
/*闰年判断*/
int Isleap(int year)
{
//Way1
	//if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
	//{
	//	return 1;
	//}
	//return 0;
//Way2
	if (year % 4 == 0)
	{
		if (year % 100 != 0)
		{
			return 1;
		}
	}
	if (year % 400 == 0)
		return 1;
}
  • 来看一下运行结果

在这里插入图片描述

3.3 二分查找

  • 接下去来做一道很经典的题目 ——>【二分查找】
  • 首先我们需要设定一个有序数组因为二分查找只能在序列有序的情况下才可以进行
int arr[] = { 1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int k = 7;
  • 可以看到上面除了设定数组以外我还求出这个数组的元素个数因为在进行函数传参的时候需要使用到
int pos = BinarySearch(arr, sz, k);
  • 来看如何传值以及接收返回值首先我们需要将这个数组的【首元素地址】传入因为对于一个数组而言你无法将整个数组都传入函数体中只能传入这个函数的首元素的地址那既然是地址就可以使用指针来进行接收当然最具有可读性的还是数组接收数组若是你不知道传入的这个是数组的首元素地址那可能用指针来接受就有点晦涩难懂了
  • 除了要传入数组的首元素地址之外还需要传入我在上面求出来的【sz】也就是数组的个数这一点的话很重要对于数组的大小一定要在外面求解完再传进去若是你在函数内部求解数组的大小时因为传入的是数组的首元素地址因此sizeof(arr)sizeof(arr[0]) 就都是一样的4个字节因为整型元素都是4个字节的大小所以求出的sz就为【1】了此时就会发生问题。最后的话需要查找的值肯定也需要进行一个传入
//int BinarySearch(int* a, int n, int k)   //指针接收地址
int BinarySearch(int a[], int n, int k)	   //数组接收数组
{
	int left = 0;
	int right = n - 1;
	while (left <= right)
	{
		int mid = left + (right - left) / 2;
		if (k > a[mid])
			left = mid + 1;
		else if (k < a[mid])
			right = mid - 1;
		else
			return mid;
	}
	return -1;
}

3.4 修改数值

  • 接下去一个练习就是通过函数内部的修改带动函数外部数组的修改这里就要使用到我们上面所说的【传址调用】。先来看看代码
void change(int* px)
{
	(*px)++;
}
int num = 0;
change(&num);
printf("num = %d\n", num);
  • 在函数外部我传入了num值的地址然后在函数体的形参中使用指针来进行接收此时对这个指针进行解引用就可以获取到外部的实参就可以直接对其进行修改了这里主要写成(*px)++不能写成*px++因为【++】的优先级比【*】来得高所以会先对这个指针变量进行一个++然后再对其进行一个解引用的操作但是当这个指针后移的时候就已经变成了野指针此时再去访问这个野指针就会出现问题后面给出运行结果
  • 可以其进行一次的修改那一定也可以进行多次的修改我们通过运行结果来看看

在这里插入图片描述

  • 但是我们上*px外的括号去掉再看看

在这里插入图片描述

五、函数的嵌套调用和链式访问

1、嵌套调用

函数和函数之间可以根据实际的需求进行组合的也就是互相调用的

  • 首先来看第一个例子。非常好理解可以自己看看
void print()
{
	printf("haha\n");
}

void three()
{
	for (int i = 0; i < 3; ++i)
	{
		print();
	}
}
int main(void)
{
	three();
	return 0;
}

在这里插入图片描述
函数可以嵌套调用但是不能嵌套定义

2、链式访问

把一个函数的返回值作为另外一个函数的参数

  • 这里我先介绍一下【strlen】这个函数

在这里插入图片描述

  • 对于strlen()sizeof()我会在后面的【面试题】专栏中进行细致讲解
  • 然后我们到代码里面来用一下
char a[] = "hello world";
int len = strlen(a);
printf("len = %d\n", len);

在这里插入图片描述
这里要注意一点的是strlen()去求字符串长度的时候是不算’\0’的

  • 接着再来介绍一个函数【strcat

在这里插入图片描述

  • 好接下去我们来看看如何做到链式访问
char a[10] = "hello";
int len = strlen(strcat(a, "bit"));
printf("len = %d\n", len);

在这里插入图片描述

  • 接下去再来看一下链式访问的例子。你知道这句代码输出的结果是多少吗
printf("%d", printf("%d", printf("43")));
  • 来揭晓一下结果吧

在这里插入图片描述

  • 那有同学就很诧异为什么这句代码的输出是【4321】呢我们来看一下【printf】这个函数

在这里插入图片描述

  • 可以看到对于printf的返回值是输出字符的个数那这就可以解释通了对于内层的 printf("43")输出的字符个数有2个然后再看外层的printf("%d", printf("43"))便会在输出一个2它的返回值就是输出了一个字符那么整体的这个printf("%d", printf("%d", printf("43")))就会在输出一个1作为返回值

在这里插入图片描述

六、函数的声明和定义

1、函数声明

  1. 告诉编译器有一个函数叫什么参数是什么返回类型是什么。但是具体是不是存在函数
    声明决定不了。
  2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用
  3. 函数的声明一般要放在头文件中的
  • 来看一下代码
int Add(int x, int y);

int main(void)
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);

	int sum = Add(a, b);
	printf("sum = %d\n", sum);
	return 0;
}

int Add(int x, int y)
{
	return x + y;
}

在这里插入图片描述

2、函数定义

函数的定义是指函数的具体实现交待函数的功能实现

  • 对于上面的代码我们可以对其做一个分解将【add】这个功能单独放到一个.c的文件中然后再写一个.h的文件去声明一下这个函数。因为在日常的项目工程中是有很多程序员一起写代码的但是他们不可能在一个.c文件中一起书写也不能等一个人写好之后另一个人再接着写这样就会降低开发效率
  • 所以我们就有一个分模块编写的说法也就是在一个头文件中先写好函数的声明和各种库函数的引用和定义然后每个程序员都可以创立自己的.c文件然后去书写自己的那段逻辑最后将大家的代码进行一个整合就完成了整个项目

在这里插入图片描述

  • 上面就是对【add】整个功能做了一个分模块的编写。在后面学到三子棋和扫雷的时候还会教大家用这种分模块编写

七、函数递归

接下去我们来说说函数递归这也是函数这一块最难理解的内容

1、什么是函数递归

程序调用自身的编程技巧称为递归 recursion
只需少量的程序就可描述出解题过程所需要的多次重复计算大大地减少了程序的代码量
递归的主要思考方式在于把大事化小

2、函数递归的两个必要条件

  • 存在限制条件当满足这个限制条件的时候递归便不再继续。
  • 每次递归调用之后越来越接近这个限制条件

3、题目的展开与剖析

3.1 例题1打印数字

【需求】输入1234打印1 2 3 4

  • 在之前我们学习分支和循环的时候有做过类似的题目那个时候我们是利用取余【%】和整除【/】倒着打印一个数字的每个数位也就是下面这段代码
void print1(int num)
{
	while (num > 0)
	{
		printf("%d ", num % 10);
		num /= 10;
	}
}

运行结果如下

在这里插入图片描述

  • 但是呢现在我们要顺着将这个数的每一个数位进行一个打印这该怎么去做呢我们来分析一下
  • 我们在屏幕上通过scanf输入一个数然后通过封装好的一个函数传入print(num)print(1234)那既然上面讲到了这个分割数字的思路这里我们也可以使用这个思路来完成
  • 对于print(1234)我们可以拆成print(123) 4
  • 对于print(123)又可以拆成print(12) 3
  • 对于print(12)又可以拆成print(1) 2
  • print(1234)就被拆成了print(1) 2 3 4也就是当这个num < 10为一个个位数时就做一个打印否则的话就不断将其以十分之一倍得进行缩小。我们将其转化为代码的形式就【一目了然】了
  • 可以看到这里是使用到了一个递归的逻辑上面说到递归都是具有结束条件的若是不符合的话就层层递归下去知道当前传入的num值为个位数为止才进行一个回调
void print2(int num)
{
	if (num > 9)
	{
		print2(num / 10);
	}
	printf("%d ", num % 10);
}

运行结果如下

在这里插入图片描述

  • 可能还是有同学对于递归有点难理解没关系我们通过递归展开图再来看看

在这里插入图片描述

  • 通过这么一幅递归展开图的剖析相信你对函数递归的整体流程应该有了一个了解。但是还是觉得难以有这个思维没事我们再来看一道例题

3.2 例题2求字符串长度

【需求】输入abc输出其长度为3

  • 上面有讲到过strlen()这个函数其可以求出一个字符串的长度我们首先通过这个函数来试试。运行结果后面再给出
int main(void)
{
	char str[] = "abc";

	int len = strlen(str);		//利用库函数进行求解
	printf("len = %d\n", len);
	return 0;
}
  • 可以看到这个很简单但是不要忘了包含头文件【string.h】
  • 接下去的话我们将这个strlen()函数改成my_strlen()我们自己来实现一下这个底层的逻辑。对于int len = my_strlen(str);我们传入了字符数组str的首元素地址那上面有讲到过对于地址要使用指针来进行接收最后还要返回求出的长度所以对于函数我们可以定义成这样
int my_strlen(char* str)
  • 接下去考虑该如何去实现和这个函数。对于str这个字符指针现在是指向传入字符数组的首元素地址也就是【a】那你就要搞懂对于一个字符串来说以什么作为结束标志没错就是【\0】因此可以让这个字符指针不断后移每一次对其进行解引用看看是否为【\0】即可有了思路我们就可以写出代码了
  • 在这里的话还需要设置计数器我们我们要去求解这个字符串的长度因此判断到它不为【\0】的时候就count++然后让这个字符指针进行后移即可。
int my_strlen(char* str)
{
	int count = 0;
	while ((*str) != '\0')
	{
		count++;	//计数器累加
		str++;		//字符指针后移
	}
	return count;
}
  • 可是呢现在我要改变一下题目的要求不可以使用计数器进行求解字符串的长度那有小伙伴就很诧异了你让我去求长度但是连计数器都不让用这怎么求呢别急我们用递归来试试
  • 一样我们可以使用到上面的分割思想
  • 对于my_strlen(abc)我们可以拆成1 + my_strlen(bc)
  • 对于my_strlen(bc)我们可以拆成1 + my_strlen(c)
  • 对于my_strlen(c)我们可以拆成1 + my_strlen('\0')
  • my_strlen(abc)就相当于是1 + 1 + 1 + 0。也是使用字符指针去进行一个后移若其不为【\0】时就不断对这个字符串进行拆分然后直到遇到【\0】时便return 0。接下去就可以写出代码了
int my_strlen(char* str)
{
	if (*str != '\0')
	{
		return 1 + my_strlen(str + 1);
	}
	return 0;
}
  • 然后来看一下结果吧上面三段代码的结果都是一样的所以一起展示了

在这里插入图片描述

  • 一样我们来画一下递归展开图

在这里插入图片描述

  • 看了这个相信你对递归的概念又进一步有了了解。但还是感觉有所欠缺我们再来看一道对于递归来说很经典的题目——阶乘求解

3.3 例题3阶乘求解

【需求】输入一个数输出其求阶乘后的结果

  • 对于阶乘来说可能有小伙伴没听过的我稍作分析然后先列出一个公式。
  • 阶乘就是从从一个数开始乘慢慢减少一直乘到1为止举个例子3! = 3 * (3 - 1) * (3 - 2) = 3 * 2 * 1 = 6。再举两个特殊一点的 0! = 11=1 知道了这些我来列出一个公式你看看

在这里插入图片描述

  • 也就是当【n <= 1】时即0的阶乘和1的阶乘最后都是1当【n >= 2】时最后的结果就是n去乘以它减1的阶乘于是我们就可以得出递归的代码
/*阶乘——递归*/
int Func1(int n)
{
	if (n <= 1)
		return 1;
	else
		return n * Func1(n - 1);
}
  • 对于阶乘来说不仅可以使用递归实现还可以使用循环的方式来是实现这个我们在前面学习分支和循环语句的时候就有讲到过这里就不作详解了
/*阶乘——循环*/
int Func2(int n)
{
	int ret = 1;
	for (int i = 1; i <= n; ++i)
	{
		ret *= i;
	}
	return ret;
}

这里再对上面的递归实现做一个递归展开图的分析

在这里插入图片描述

  • 看完这三道例题相信你对递归的代码写法有了一个初步的认识。接下去我们再通过三道练习题来巩固一下

3.4 练习1斐波那契数列

【需求】输入一个数输出从1到这个数的斐波那契数

  • 如果有同学对斐波那契数列不太了解的可以先去看看——> 斐波那契数列。这里给出从1~10的斐波那契数列 【1 1 2 3 5 8 13 21 34 55】可以看出对于12两个数来说都是①后面就是数就是前两个数之和因此我们可以列出下面的公式
    在这里插入图片描述
  • 然后根据这个公式写出代码
/*斐波那契数列——递归*/
int Fib1(int n)
{
	if (n <= 2)
		return 1;
	else
		return Fib1(n - 2) + Fib1(n - 1);
}
  • 然后一样通过递归展开图来分析一下不过对于斐波那契数列的递归展开图可能有些复杂因为每次递归存在两个分支这个结构如果学过数据结构的同学应该清楚是二叉树没学过也没有关系因为这是C语言的文章。下面是我的讲解希望你可以看得懂😄

在这里插入图片描述

  • 仔细观察其实可以发现光是求5这么小的数字就有一些重复计算也就是【1】和【2】这两个数字。包括【3】也计算了两次那在这里其实我们可以去打印一下若是求50的阶乘话【3】会计算几次呢

3.5 练习2青蛙跳台阶

接下去我们再来做一个很有意思的小练习这个和斐波那契数列很像

① 首先来快速了解一下青蛙的跳法

📚青蛙每次可以跳一个台阶或者两个跳台阶

在这里插入图片描述

  • 需求是算出当青蛙跳到第n个台阶的时候有几种跳法

③ 通过画算法图来分析一下

在这里插入图片描述

  • 可以看出对于第n个台阶青蛙的跳法变化规律为1——>2——>3——>5——>8很明显呈现的就是我们上面所讲到过的【斐波那契数列】那这就好办了

④ 来看看代码如何书写

  • 首先来看看递归版的写法
//递归
int Jump(int n)
{
	if (n <= 2)
		return n;
	else
		return Jump(n - 1) + Jump(n - 2);
}
int main(void)
{
	int n = 0;
	printf("请输入台阶的数量");
	scanf("%d", &n);

	for (int i = 0; i < n; ++i)
	{
		printf("跳到第%d个台阶时有%d种跳法\n", i + 1, Jump(i + 1));
	}
	return 0;
}

运行结果

在这里插入图片描述

  • 接下去再来看看迭代版的写法
//迭代
int Jump2(int n)
{
	int a = 1;
	int b = 2;
	int c = 1;
	int t = n;

	while (n > 2)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}

	if (t <= 2)
		return n;
	else
		return c;
}

运行结果

在这里插入图片描述

  • 对于青蛙跳台阶和斐波那契数列很是类似因此不做过多分析可以自己去谢谢代码画图感受一下

3.6 练习3汉诺塔问题

汉诺塔是很经典的递归问题我们一起来看一下

① 首先来了解一下【汉诺塔】是什么东西

  • 汉诺塔Tower of Hanoi又称河内塔是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定在小圆盘上不能放大圆盘在三根柱子之间一次只能移动一个圆盘

② 接下去快速了解一下它的规则

📚规则一每次只能移动一片盘片
📚规则二每次移动完后要保证小盘片在大盘片的上面

③ 然后通过动画来看看盘片的移动过程

在这里插入图片描述
④ 再来看看代码如何书写

/*汉诺塔*/
void move(char x, char y)
{
	printf("%c-->%c\n", x, y);
}

void Hanoi(int n, char a, char b, char c)
{
	if (n == 1)
	{
		move(a, c);
	}
	else
	{
		Hanoi(n - 1, a, c, b);
		move(a, c);
		Hanoi(n - 1, b, a, c);
	}
}

int main(void)
{
	int n = 0;
	int a = 'A';
	int b = 'B';
	int c = 'C';

	scanf("%d", &n);
	Hanoi(n, a, b, c);

	return 0;
}

【运行结果】

在这里插入图片描述

⑤ 马上来讲讲代码的逻辑

  • 可以看到在主函数中我们以三个盘片为例然后将汉诺塔判断移动的过程封装成了一个函数的形式因为它要进行递归展开所以这一块是毋庸置疑的肯定是一个单独的函数逻辑
  • 然后我们来看看移动盘片函数的内部逻辑若是当前盘片数量不为1个的时候那就需要移动否则的话就要去执行一个递归的逻辑具体的递归逻辑我化成了一个递归展开图如下图所示

⑥ 最后通过代码来画出递归展开图
在这里插入图片描述

  • 相信在看完这幅图后的你一定对汉诺塔的移动原理有了一个深入的理解

八、函数栈帧的创建的销毁

本内容请看这篇文章——> 反汇编深挖【函数栈帧】的创建和销毁

九、总结与提炼

好我们来总结一下本文所学习的内容

  • 在本文中我们首先了解了什么是函数然后看了C语言中的函数分为哪两哪类除了给大家详细介绍几个库函数之外还说明了如何去自己写一个自定义函数在这其中我们讲到了函数调用的一个非常经典案例——【交换数值】
  • 对于这个案例也需要使用到后面形参实参的概念以及传值调用传址调用的区别想要在一个函数中真正通过形参的修改使得外部的实参得到一个修改就需要传入实参的地址此时就可以与外界建立起一个真正的连接
  • 知道了如何去自定义函数我们通过几个专项练习进行了巩固加深了对函数传参以及调用的认知和理解。
  • 在说完函数的嵌套调用和链式访问之后知道了原来函数还可以进行套娃😍有了嵌套调用的基本概念后我们就正式进入了函数最难的一节——函数递归在这一模块中我们了解了什么是函数递归知道了如何使用递归去巧妙地求解实际问题在【题目的展开与剖析】中还带着大家做了许多的例题与练习对函数递归来说最重要的还是画递归展开图可以更好地帮助理解

以上就是本文所要展示的所有内容感谢您的阅读如有问题请于评论区留言或者私信我🍀

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

“C生万物 | 函数的讲解与剖析【内附众多案例详解】” 的相关文章