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

本文深入浅出地介绍了C语言中的函数概念,包括函数的基本定义、库函数与自定义函数的区别,以及函数的参数、调用方式等内容。同时,还详细探讨了函数递归的原理及其应用。

 🐒博客名:平凡的小苏

 

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

目录

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. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
        
好了,小编分享到此结束,欲知后续,下篇精彩继续!!!

 

 

 

 

目录 1. C 语言中的指针和内存泄漏 5 2. C语言难点分析整理 10 3. C语言难点 18 4. C/C++实现冒泡排序算法 32 5. C++中指针和引用的区别 35 6. const char*, char const*, char*const的区别 36 7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. C语言复杂表达式的执行步骤 66 16. C语言字符串函数大全 68 17. C语言宏定义技巧 89 18. C语言实现动态数组 100 19. C语言笔试-运算符和表达式 104 20. C语言编程准则之稳定篇 107 21. C语言编程常见问题分析 108 22. C语言编程易犯毛病集合 112 23. C语言缺陷与陷阱(笔记) 119 24. C语言防止缓冲区溢出方法 126 25. C语言高效编程秘籍 128 26. C运算符优先级口诀 133 27. do/while(0)的妙用 134 28. exit()和return()的区别 140 29. exit子程序终止函数与return的差别 141 30. extern与static存储空间矛盾 145 31. PC-Lint与C\C++代码质量 147 32. spirntf函数使用大全 158 33. 二叉树的数据结构 167 34. 位运算应用口诀和实例 170 35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针 224 46. 标准C中字符串分割的方法 228 47. 汉诺塔源码 231 48. 洗牌算法 234 49. 深入理解C语言指针的奥秘 236 50. 游戏外挂的编写原理 254 51. 程序实例分析-为什么会陷入死循环 258 52. 空指针究竟指向了内存的哪个地方 260 53. 算术表达式的计算 265 54. 结构体对齐的具体含义 269 55. 连连看AI算法 274 56. 连连看寻路算法的思路 283 57. 重新认识:指向函数的指针 288 58. 链表的源码 291 59. 高质量的子程序 295 60. 高级C语言程序员测试必过的十六道最佳题目+答案详解 297 61. C语言常见错误 320 62. 超强的指针学习笔记 325 63. 程序员之路──关于代码风格 343 64. 指针、结构体、联合体的安全规范 346 65. C指针讲解 352 66. 关于指向指针的指针 368 67. C/C++ 误区一:void main() 373 68. C/C++ 误区二:fflush(stdin) 376 69. C/C++ 误区三:强制转换 malloc() 的返回值 380 70. C/C++ 误区四:char c = getchar(); 381 71. C/C++ 误区五:检查 new 的返回值 383 72. C 是 C++ 的子集吗? 384 73. C和C++的区别是什么? 387 74. 无条件循环 388 75. 产生随机数的方法 389 76. 顺序表及其操作 390 77. 单链表的实现及其操作 391 78. 双向链表 395 79. 程序员数据结构笔记 399 80. Hashtable和HashMap的区别 408 81. hash 表学习笔记 410 82. C程序设计常用算法源代码 412 83. C语言有头结点链表的经典实现 419 84. C语言惠通面试题 428 85. C语言常用宏定义 450
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平凡的小苏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值