初阶C语言-函数(下)

“追光的人,终会光芒万丈!” 今天我们继续一起来学习一下函数的相关知识点。

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

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

5.1嵌套调用

#define _CRT_SECURE_NO_WARNINGS 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;
}

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

//函数不能嵌套定义
void test()
{
    
    
	void new_line()
	{
    
    
		printf("haha\n");
	}
}

在这里插入图片描述

5.2链式访问

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

#include <stdio.h>
#include <string.h>
int main()
{
    
    
	printf("%d\n", strlen("abc"));//链式访问
	printf("%d\n", sizeof("abc"));
	return 0;
}

在这里插入图片描述

注:sizeof就是一个计算数据类型所占空间大小的单目运算符,在计算字符串的空间大小时,包含了结束符\0的位置;而strlen是一个计算字符串长度的函数,使用时需要引用头文件#include <string.h>,不包含\0,即计算\0之前的字符串长度。

#include <stdio.h>
#include <string.h>
int main()
{
    
    
	char arr1[20] = {
    
     0 };
	char arr2[] = "abc";
	printf("%d", strlen(strcpy(arr1, arr2)));
	return 0;
}

在这里插入图片描述

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

通过查阅资料,我们可以发现printf函数的返回值是打印在屏幕上字符的个数。
在这里插入图片描述
在这里插入图片描述

6.函数的声明和定义

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//写一个函数实现两个数相加
int main()
{
    
    
	int num1 = 0;
	int num2 = 0;
	scanf("%d%d", &num1, &num2);
	//函数使用之前需要添加函数的声明
	int add(int x, int y);
	//函数的调用,传值调用
	int ret = add(num1, num2);
	printf("%d\n", ret);
	return 0;
}
//函数的定义
int add(int x, int y)
{
    
    
	return x + y;
}

在这里插入图片描述
注:函数在使用之前一定要先声明,不然会出现警告!
在这里插入图片描述
当然,有的同学可能会想到之前我们在使用函数的时候,也没有声明,为什么没有出现警告呢?

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//写一个函数实现两个数相加
//函数的定义
int add(int x, int y)
{
    
    
	return x + y;
}
int main()
{
    
    
	int num1 = 0;
	int num2 = 0;
	scanf("%d%d", &num1, &num2);
	//函数的调用,传值调用
	int ret = add(num1, num2);
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述
我们可以发现这时函数的定义是在函数的使用之前,我们需要注意的是函数的定义也是一种特殊的声明。

6.1函数声明

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

6.2函数定义

函数的定义是指函数的具体实现,交代函数的功能实现。

未来在工程中代码比较多,函数一般放在.h文件中声明,在.c文件中实现。

//add.h文件
#pragma once
//函数的声明
int add(int x, int y);
//add.c文件
#define _CRT_SECURE_NO_WARNINGS 1
//函数的定义
//函数具有外部链接属性
int add(int x, int y)
{
    
    
	return x + y;
}
//test.c文件
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include "add.h"//通过#include" "引入自定义.h头文件
int main()
{
    
    
	int num1 = 0;
	int num2 = 0;
	scanf("%d%d", &num1, &num2);
	//函数的调用,传值调用
	int ret = add(num1, num2);
	printf("%d\n", ret);
	return 0;
}

分文件书写形式的优点:
1.方便多人协作
2.保护代码

7.函数递归

7.1什么是递归?

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

7.2递归的两个必要条件

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

7.2.1 练习1

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

//递归
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void Print(int n)
{
    
    
	if (n > 9)
	{
    
    
		Print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
    
    
	int num = 0;
	scanf("%d", &num);
	Print(num);
	return 0;
}

在这里插入图片描述
递归的本质就是递推+回归,那么,这段代码是如何实现的呢?
在这里插入图片描述

函数之所以能实现调用,能实现递归都是因为函数在调用的时候会维护一个函数栈帧(内存上的一块区域)。函数调用开始,函数栈帧创建,函数调用结束后,栈帧销毁。

这里假设n=123(数值小好画图)给大家演示一下栈区存储情况:
在这里插入图片描述

7.2.2 练习2

练习2:编写函数不允许创建临时变量,求字符串的长度。
如果允许用临时变量,相信我们大家都会想到strlen函数来求字符串长度。

#include <stdio.h>
#include <string.h>
int main()
{
    
    
	char arr1[] = "abc";
	int len = strlen(arr1);
	printf("%d\n", len);
	return 0;
}

在这里插入图片描述
包括下面这段代码,也是用到了临时变量,不符合题目要求。

#include <stdio.h>
size_t my_strlen(char* str)
{
    
    
	size_t count = 0;//创建了临时变量,不满足题目要求
	while (*str != '\0')
	{
    
    
		str++;
		count++;
	}
	return count;
}
int main()
{
    
    
	char arr[] = "abc";
	size_t len = my_strlen(arr);
	printf("%zd\n", len);
	return 0;
}

那么,现在题目中不允许使用临时变量,我们该怎么编写这段代码呢?这里,我们可以利用递归来解决这个问题。

#include <stdio.h>
size_t my_strlen(char* str)
{
    
    
	if (*str == '\0')
		return 0;
	else
		return 1 + my_strlen(str + 1);
}
int main()
{
    
    
	char arr[] = "abc";
	size_t len = my_strlen(arr);//传递的是数组元素的首地址
	printf("%zd\n", len);
	return 0;
}

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

7.3递归和迭代

7.3.1练习三

练习三:求n的阶乘(不考虑溢出)

//求n的阶乘
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int Fac(int n)
{
    
    
	if (n <= 1)
		return 1;
	else
		return n*Fac(n - 1);
}
int main()
{
    
    
	int n = 0;
	scanf("%d", &n);
	int ret = Fac(n);
	printf("%d\n", ret);
	return 0;
}

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

但是,这里存在着栈溢出的问题,如果我们按照下面这种方法编写程序就可以避免这个问题。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int Fac(int n)//将递归代码改为循环的方式
{
    
    
	int i = 0;
	int r = 1;
	for (i = 1; i <= n; i++)
	{
    
    
		r = r * i;
	}
	return r;
}
int main()
{
    
    
	int n = 0;
	scanf("%d", &n);
	int ret = Fac(n);
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述

7.3.2练习四

练习四:求第n个斐波那契数。(不考虑溢出)
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因意大利数学家莱昂纳多·斐波那契(Leonardo Fibonacci)1202年以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:
在这里插入图片描述
这个数列从第 3项开始,每一项都等于前两项之和。

//斐波那契数列
#define _CRT_SECURE_NO_WARNINGS 1
#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 ret = Fib(n);
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述

但是,我们发现这样有问题:

  • 在使用Fib这个函数的时候,如果我们要计算第50个斐波那契数字的时候特别耗费时间。
  • 使用factorial函数求10000的阶乘(不考虑程序的正确性),程序会崩溃。

为什么呢?我们发现Fib函数在调用的过程中很多计算其实在一直重复。
那么我们该如何改进呢?

  • 在调试factorial函数的时候,如果你的参数比较大,那就会报错:stack overflow(栈溢出)这样的信息。系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者四递归,这样有可能导致一直开辟空间,最终产生栈空间耗尽的情况,这样的现象我们就称为栈溢出

这里,我们就可以将递归改为非递归:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int Fib(int n)
{
    
    
	int a = 1;
	int b = 1;
	int c = 1;
	while (n >= 3)
	{
    
    
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}
int main()
{
    
    
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述

注:如果使用递归很容易想到,写出来的代码没有明显的缺陷,那就可以使用递归。但是如果写出的递归代码,有明显的问题,比如:栈溢出、效率低等,我们还是要使用迭代(循环)的方式来解决。

解决栈溢出的问题的方法:

  1. 将递归写成非递归。
  2. 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

好啦,关于函数的知识点到这里就结束啦,后期会继续更新C语言相关知识点,欢迎大家持续点赞、关注和评论!大家多多支持团子呀!

猜你喜欢

转载自blog.csdn.net/qq_73121173/article/details/132008119
今日推荐