C语言——函数详解——重难点函数递归

发布于:2022-11-01 ⋅ 阅读:(763) ⋅ 点赞:(0)

 🐒博客名:平凡的小苏

 

📚学习格言:别人可以拷贝我的模式,但不能拷贝我不断往前的激情

目录

1. 函数是什么?🌈

2. C语言中函数的分类:🎇

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函数声明与定义的正确使用🎇

7.函数递归📚

7.1 什么是递归?🌇

7.2函数递归的两个必要条件🐒

7.3递归演示🎇

 7.4例题讲解函数递归(画图讲解)🌇

 7.5递归与迭代🚀


1. 函数是什么?🌈

数学中我们常见到函数的概念。但是你了解C语言中的函数吗?

维基百科中对函数的定义:子程序

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

2. C语言中函数的分类:🎇

1. 库函数

2. 自定义函数

2.1 库函数:🚀

为什么会有库函数?

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

2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。

3. 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。 像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到, 为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员 进行软件开发。

那怎么学习库函数呢?

这里我们简单的看看:www.cplusplus.com

总结:
C 语言常用的库函数都有:
IO 函数  //input output函数,即输入输出函数
字符串操作函数  //strcmp,strcpy等等
字符操作函数  
内存操作函数   //memset等等
时间 / 日期函数  //time 等等
数学函数  //srand,rand等等
其他库函数

注:

1.我们使用的网站是新版的建议转换成旧版

2.我们这个网站可以自己学习标准库函数或者当我们忘了库函数的头文件时可以在这里查询到,并且可以学习到库函数的用法

3.很多人说自己英文也不用担心,在这个网页上右击鼠标后,可以使用百度翻译来翻译成中文,当然这里小编建议先看英文的实在不懂才去看中文。

 

注:在这个网页的右上角点击Legacy version转换成旧版

 我们参照文档,学习几个库函数:

代码例子:

#include <stdio.h>
#include <string.h>

int main()
{
	char str1[] = "Sample string";
	char str2[40];
		strcpy(str2, str1);
	printf("str1: %s\nstr2: %s\n", str1, str2);
	return 0;
}

 

 注:这里是一个字符一个字符的复制给str2的

2.2自定义函数 🛸

如果库函数能干所有的事情,那还要程序员干什么?
所有更加重要的是 自定义函数
自定义函数和库函数一样,有函数名,返回值类型和函数参数。
但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
函数的组成:
ret_type fun_name(para1, * )
{
 statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1    函数参数

代码演示:用函数交换两个数(错误版本

#include <stdio.h>
void Swap(int x, int y)
{
	int temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	printf("交换前:%d %d\n", a, b);
	Swap(a, b);
	printf("交换后:%d %d\n", a, b);
	return 0;
}

画图解释

 ​ 

注:

1.这里的x和y是形式参数,有自己的空间,他们与a和b的地址各不相同

2.当函数调用的时候,实参传递给形参,这时形参是实参的一份临时拷贝,当形参改变时是不影响实参的值的,因为他们的地址没有改变 

 代码演示:用函数交换两个数(正确版本

#include <stdio.h>
void Swap(int *x, int *y)
{
	int temp = *x;
	*x = *y;
	*y = temp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	printf("交换前:%d %d\n", a, b);
	Swap(&a, &b);
	printf("交换后:%d %d\n", a, b);
	return 0;
}

注:这时候将实参a和b进行取地址,把a的地址传给x,把b的地址传给y,*x就是a,*y就是b,这时候他们的地址相同,然后将他们的地址进行解引用的操作,那么就实现了函数数值的交换

3. 函数的参数🌈

3.1 实际参数(实参):🛸

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

3.2 形式参数(形参):🚀

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内
存单 元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有 效。
上面 Swap1 Swap2 函数中的参数 x y px py 都是 形式参数 。在 main 函数中传给 Swap1 num1 , num2 和传 给 Swap2 函数的 &num1 &num2 实际参数
这里我们对函数的实参和形参进行分析:

 4. 函数的调用:🌈

4.1 传值调用🚀

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

4.2 传址调用🎇

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

4.3例子练习🛸

1.写一个函数,实现一个整形有序数组的二分查找。
#include <stdio.h>
void find()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 8;
	int left = 0;
	int right = 9;
	int flag = 0;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (arr[mid] > k)
		{
			right = mid;
			right--;
		}
		if (arr[mid] < k)
		{
			left = mid;
			right++;
		}
		if (arr[mid] == k)
		{
			flag = 1;
			printf("找到了,该下标为:%d\n", mid);
			break;

		}

	}
	if (flag == 0)
	{
		printf("找不到\n");
	}
}
int main()
{
	find();
	return 0;
}

注:首先将二分查找的代码写在自定义函数里,然后在主函数进行调用就可以了,因为我设置的是无返回值的函数,所以不需要进行返回。

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

5.1 嵌套调用🐒

代码演示:

#include <stdio.h>
void new_line()
{
 printf("hehe\n");
}
void three_line()
{
    int i = 0;
 for(i=0; i<3; i++)
   {
        new_line();
   }
}
int main()
{
 three_line();
 return 0;
}

注:

1.这里我先定义了两个函数,一个函数打印hehe,利用three_line函数去嵌套调用new_line函数,并且利用for循环去调用new_ling()函数打印三次hehe

函数可以嵌套调用,但不能嵌套定义

这样定义的函数编译器会直接报错

 

5.2 链式访问🚀

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

代码演示:

#include <stdio.h>
#include <string.h>
int main()
{
	
	printf("%d\n", strlen("bit"));
	return 0;
}

注:这里是将strlen的返回值作为printf的参数,这就是链式访问。并且使用石头人冷需要包含#include<string.h>的头文件

经典链式访问:

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

	//printf函数的返回值是打印在屏幕上字符的个数
	return 0;
}

注:

 这里有4个printf,编译器首先打印43,然后返回值是打印在屏幕上字符的个数,所以返回2

 然后继续打印2,返回1,打印1,再返回1,然后再打印1;

6. 函数的声明和定义🥇

6.1 函数声明:🌇

1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数
声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用
3. 函数的声明一般要放在头文件中的
特殊的函数声明
这里我们先看没有被声明的函数:


int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	//求和
	int ret = Add(a, b);
	printf("%d\n", ret);

	return 0;
}



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

 

1.由于编译器从从上往下扫描的,当在main函数中调用Add的时候,编译器不知道有这个Add函数所以就会报警告,但不会报错。

2.所以当我们把函数定义在调用的后面时,我们需要在调用之前进行函数声明

3.如果我们直接把函数定义在使用之前,就不会出现这种警告了

声明后的代码:

#include<stdio.h>
int Add(int x, int y);

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	//求和
	int ret = Add(a, b);
	printf("%d\n", ret);

	return 0;
}


//函数的定义
//函数的定义也是一种特殊的声明
int Add(int x, int y)
{
	return x + y;
}

6.2函数声明与定义的正确使用🎇

 

注:这里是将函数的声明放在add.h的头文件中,然后函数的定义放在add.c原文件当中

 

好处:

1.模块化开发

 注:如果我们开发的项目比较复杂的话,就需要几个程序员进行模块化开发,几个程序员分工合作,当每个程序员开发好各自的项目后,只需要引入每个程序员的头文件和源文件就可以使用了。

 

2.代码的隐藏

当一个程序员开发好一个项目的时候,想要把这个源码拿去卖的时候,这个程序员不想别人知道他的代码是怎么写的,这时候这名程序员就可以将它的代码换成二进制的乱码,程序员只需要把头文件和这个乱码卖给另外一名程序员。这样即实现了代码隐藏,又赚到了钱。

 

 

7.函数递归📚

7.1 什么是递归?🌇

程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接
调用自身的 一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略 只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在于:把大事化小

7.2函数递归的两个必要条件🐒

1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。

2.每次递归调用之后越来越接近这个限制条件

7.3递归演示🎇

最简单的函数递归

 注意:这种虽然是最简单的递归,但是这种递归是错误的,自己调用自己,因为不满足于函数递归的两个必要条件,这个递归会陷入死循环,然后栈溢出程序会崩掉,

 

注:每次调用函数,都会为本次函数,在内存的栈区开辟一片空间,持续递归会将栈的内存耗干导致栈溢出

 7.4例题讲解函数递归(画图讲解)🌇

接受一个整型值(无符号),按照顺序打印它的每一位。
例如:
输入: 1234 ,输出 1 2 3 4
#include <stdio.h>
void print(int n)
{
 if(n>9)
 {
 print(n/10);
 }
 printf("%d ", n%10);
}
int main()
{
 int num = 1234;
 print(num);
 return 0;
}

 

注:

首先利用递归大事化小思想, 利用print函数将n除以10持续调用到个位数后,不满足if(n>9)

这个条件,现在剩下的就是个位数了,那么我么可以将个位数取模于10,把个位数打印出来,打印后又持续返回调用的那个函数中,就可以按照顺序打印出他的每一位了。

 7.5递归与迭代🚀

利用函数递归和迭代求第 n 个斐波那契数。(不考虑溢出)
斐波那契数: 1 1  2  3  5   8   13   21  34.......
斐波那契数从第三个开始,由前面两个数进行相加得到

利用函数递归求第n个斐波那契数。(不考虑溢出)

代码演示:

#include <stdio.h>

int fib(int n)
{
	if (n <= 2)
		return 1;
	else
		return fib(n - 1) + fib(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int num =fib(n);
	printf("%d\n", num);
	return 0;
}

 这里为什么讲函数递归和迭代讲解斐波那契呢?因为当利用递归求解斐波那契时,如果输入的数值过大,他会进行重复大量的计算,效率过于低下

 这里看一下当我们输入40的时候,他计算了多少次3

#include <stdio.h>
int count = 0;
int fib(int n)
{
	if (n== 3)
	{
		count++;
	}
	if (n <= 2)
		return 1;
	else
		return fib(n - 1) + fib(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int num =fib(n);
	printf("%d\n", num);
	printf("count= %d\n",count);
	return 0;
}

 

 这里的3被计算39088169次,这就可以看出他的效率极低。

那如何解决上述的问题:
1. 将递归改写成非递归。

利用迭代的方法进行求解斐波那契数

代码演示: 

#include <stdio.h>

int fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;

	while (n > 2)
	{
		a = b;//b赋值给a
		b = c;//c赋值给b
		c = a + b;//两数相加的斐波那契数
		n--;
	}
	return c;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int num =fib(n);
	printf("%d\n", num);

	return 0;
}

 

注:

1.当n<=2时,斐波那契数直接返回1

2. 当n>2时,这时候就是利用迭代思想,把b赋值给a,c赋值给b,两个数相加得到斐波那契数,然后输入的数需要进行减1,直到输入的数小于等于2

提示:
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
        
好了,小编分享到此结束,欲知后续,下篇精彩继续!!!

 

 

 

 

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到