C语言基础-函数的故事

维系C世界框架的英雄
  • 上一章我们刚讲完C世界中函数的调用方式,而接下来我们将进一步了解函数的嵌套调用和链式访问等函数的其他内容

  • 如果你想要想知道函数之前的故事,可以通过观看C语言基础-函数的故事(1)来进行了解\(^ ^)/

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

函数和函数之间可以有机的组合

函数的嵌套调用

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

例子1:
#include <stdio.h>
void print()
{
    printf("禁止套娃!");
}
int test()
{
    print();
    return 0;
}
int main()
{
    test();
    return 0;
}

该例子就是通过在主函数调用了一个test函数,又在test函数里调用了一个print函数

函数的链式访问

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

例子2:将arr2的字符串拷贝到arr1,并打印arr1
#include <stdio.h>
#include <string.h>
int main()
{
    char arr1[20]={0};
    char arr2[]="Hello World";
    
    //不用链式访问
    strcpy(arr1,arr2);
    printf("%s\n",arr1);
    
    //链式访问
    printf("%s\n",strcpy(arr1,arr2);
}
  • 不用链式访问,就是直接用strcpy函数将arr2的值传给arr1后,再将arr1打印
  • 用链式访问就是将strcpy的返回值作了printf的参数

所以我们要知道strcpy的返回值是什么,可以通过上章讲的网站,查询。结果如下:

  • Each of these functions returns the destination string. No return value is reserved to indicate an error
  • 意思就是该函数返回目标字符串,没有的话则指示错误
例子3:printf套娃打印(思考题哦)
#include <stdio.h>
int main()
{
	printf("%d",printf("%d",printf("%d",43)));
	return 0;
}
  • 最终打印的是434343吗?还是43?或者还是其他值呢?
  • 按照例2的解释,我们第一步应该要清楚,这是函数的链式访问
  • 第二步,就要找到最内层函数的返回值是是么
  • 第三步就是将返回值作为参数给到外层函数,开始套娃啦!
  1. 我们可以确定这是链式访问,并且只有函数printf

  2. 通过资料找到printf的返回值:

    Each of these functions returns the number of characters printed, or a negative value if an error occurs

    翻译就是:这些函数中的每一个函数都返回打印的字符数,如果发生错误,则返回负值

  3. 故最内层printf先打印43,而他的返回值就是打印的字符数2,并且作为其外层printf的参数。中间的printf就打印2,而他则返回1再作为最外层printf的参数,最后最外面的printf则打印1

  4. 得到最后的结果就是4321

函数的声明和定义
函数的声明
  • 函数声明的作用则是把函数的名字、函数类型以及形参类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查
  • 在书写形式上,函数声明可以把函数头部复制过来,在后面加一个分号;而且在参数表中可以只写各个参数的类型名,而不必写参数名
  • 函数的声明一般出现在函数的使用之前。要满足先声明后使用
  • 函数的声明一般要放在头文件中的
函数的定义
  • 函数定义是指对函数功能的确立,包括指定函数名,函数值类型、形参类型、函数体等,它是一个完整的、独立的函数单位
个人理解
  • 函数声明其实是无关紧要的,如果我在调用函数之前,就定义了函数,就不需要函数声明了
  • 函数定义就像你发明了一个东西,但是你不告诉别人这是什么,别人也不会用,不知道。而函数声明就是去告诉别人你这个发明的出现。
函数的递归
什么是递归
  • 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法
  • 递归的主要思考方式:大事化小,小事化了
递归的两个必要条件
  1. 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
  2. 每次递归调用之后越来越接近这个限制条件
例子5:最简单的递归(main函数)
#include <stdio.h>
int main()
{
    printf("我会死循环呀!\n");
    main();
}
  • 上面循环大家可以通过调试,得到一个报错:Stack overflow,这就是栈溢出

  • 至于为什么会出现栈溢出呢?大家可以发现,例5其实不满足递归的两个必要条件,所以这个递归是错的,甚至不能称为递归

栈溢出
  • 栈溢出就是缓冲区溢出的一种
  • 缓冲区:程序在运行过程中,为了临时存取数据的需要,一般都要分配一些内存空间,通常称这些空间为缓冲区
  • 如果向缓冲区中写入超过其本身长度的数据,以致于缓冲区无法容纳,就会造成缓冲区以外的存储单元被改写,这种现象就称为缓冲区溢出
  • 缓冲区长度一般与用户自己定义的缓冲变量的类型有关
例子6:顺序打印一个整型值(用递归)
#include <stdio.h>
void print(int n)
{
    if(n>9)
    {
        print(n/10);
    }
    printf("%d ",n%10);
}
int main()
{
    int n;
    scanf("%d",&n);
    print(n);
    return 0;
}
  • 顺序打印一个数,可以通过模10,除10,得到该数各个位上的数(最先得到最低位数字),然后通过数组接收,逆序输出就可以
  • 而如果利用递归,我们的思路就要变一下:假如我们print(123)。我们其实就可以打印print(12)和printf(3);而print(12),其实就是print(1)和printf(2)最后print(1)我们就可以直接printf(1)

通过上面递归的思路,化成代码的思路就是:(例子6)

  1. 判断这个数是不是个位数,即n<9。
  2. 若判断的数字不小于9,则拆解成前面一部分和最后一位数,前面一部分先继续判断,判断后,后面这个数再接着打印
  3. 若判断的数字小于9,则直接打印
例子7:创建一个函数,模拟strlen,不能创建临时变量
#include <stdio.h>
int my_strlen(char* str)
{
	if (*str != '\0')
	{
		return 1 + my_strlen(str+1);
	}
	else
	{
		return 0;
	}
}
int main()
{
	char arr[] = "hello World";
	printf("%d", my_strlen(arr));
	return 0;
}
  • 这题如果能创建临时变量,就可以定义一个count来计数。但是要求,所以可以思考一下递归
递归与迭代
  • 迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值
例子7:求n的阶乘
#include <stdio.h>
int fac(int n)
{
    if(n<=1)
        return 1;
    else
        return n*fac(n-1);
}
int main()
{
    int n;
    scanf("%d",&n);
    int ret=fac(n);
    printf("%d",ret);
    return 0;
}

例7就是递归与迭代的结合。当n>1时,会反复执行n*fac(n-1)这个语句;而n<=1时,就返回1

例子8:求斐波那契数列
#include <stdio.h>
int fib(int n)
{
    if(n<=2)
        return 1;
    else
        return fib(n-1)+fib(n-2);
}
int main()
{
    int n;
    scanf("%d",&n);
    printf("%d",fib(n));
    return 0;
}
  • 斐波那契数列第一个和第二个数为1,从第三个开始都等于前两个数的和
  • 所以只需要,判断当n<=2时,返回1,n>2时返回前两个数的和,即递归和迭代的结合
注意:
  • 当n较大时,如50。最终结果要等一段时间才能输出。因为递归的效率很低,比如fib(50)=fib(49)+fib(48),fib(49)=fib(48)+fib(47)…一个数后面会分散成很多分支,而且会出现很多已经重复出现的值,故要花时间大,效率低
  • 当n很大时,如10000000。则最后程序可能会崩溃。因为每调用一次函数,都会在栈区开辟一份内存,而当n=10000000时,是需要开辟很多空间的,导致栈溢出
当n=50时,可作图如下:

686CCA17-EB9F-6A43-6576-35A385A8E6BD.png

  • 通过上图可以知道,当n=50时,递归调用函数需要很多次
  • 并且中间会出现很多重复的值,需要再次计算
解决递归效率问题
  1. 将递归改写成非递归。

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

例子9:斐波那契数列(非递归,不考虑溢出)
#include <stdio.h>
int fib(int n)
{
    int a=1;
    int b=1;
    int c=0;
    while(n>2)
    {
        c=a+b;
        a=b;
        b=c;
        n--;
    }
    return c;
}
int main()
{
    int n;
    scanf("%d",&n);
    printf("%d",fib(n));
    return 0;
}
递归总结
  1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
  2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
  3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开 销。
递归思考题(这里是我的思考与题解,大家可以借鉴 C语言基础实现青蛙跳台阶和汉诺塔问题
  1. 汉诺塔问题
  2. 青蛙跳台阶问题
递归总结
  1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
  2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
  3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
结语

最后函数的故事到此就告一段落了,以后他将经常出现在C语言的学习中,希望我的讲解你们能喜欢!

收藏 (0)
评论列表
正在载入评论列表...
我是有底线的
为您推荐
    暂时没有数据