【C语言】函数 ---- 函数的嵌套调用和链式访问、函数的声明和定义、变量的声明和定义、函数递归与迭代、递归时的栈溢出问题

简介: 【C语言】函数 ---- 函数的嵌套调用和链式访问、函数的声明和定义、变量的声明和定义、函数递归与迭代、递归时的栈溢出问题

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

1.嵌套调用

函数的嵌套调用就是一个函数被另一个函数调用,实现多重调用。

观察如下代码:

#include <stdio.h>
void repeate()
{
  printf("repeating!\n");
}
void repeate_three()
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    repeate();
  }
}
int main()
{
  repeate_three();
  return 0;
}

我们看到,repeate_three函数中使用for循环调用了三次repeate函数,最后的结果也是打印三次“repeating!”,这就是函数的嵌套调用。

注意:函数虽然能嵌套调用,但却不能嵌套定义

如下形式的代码就是错误的:

#include <stdio.h>
int main()
{
  int repeate(int a, int b)
  {
    return a + b;
  }
  return 0;
}

上面的代码把一个返回类型为整形的函数定义在main函数内,这在语法上是不行的!!!

2.链式访问

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

2.1strlen()函数

我们以strlen()函数为例:

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

strlen()函数返回的是字符串的个数,我们定义一个整型变量len接收它的返回值,结果为6,直接打印strlen(“abcdef”)结果也是6。

像这种把strlen()函数的返回值作为printf()函数的参数就是函数的链式访问。

2.2printf()函数

我们再看一个经典例子:

如果printf()函数作为返回值又多次被printf()函数打印,那会是什么样的结果呢?

我们看下面的代码:

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

要想知道这段代码是如何执行的,我们就要了解printf()函数到底是怎样返回的,返回的究竟是什么。

printf()的返回类型是一个整型:

int printf ( const char * format, … );

可以看到,作为参数成功的话,返回的是printf打印的字符或数字的总的个数。

接下来我们画图来分析这段代码的打印结果:

首先printf(“%d”,15)先把15打印出来:

而右边第一个printf打印了2个数字,所以它的返回值就是2,接着在屏幕上打印,就是152:

从右边第二个printf只打印了一个数字2,所以它的返回值就是1,接着在屏幕上打印,就是1521:

而最终的结果就是1521:

二、函数的声明和定义

1.函数声明和定义的介绍

函数的定义就是函数的实现,函数的声明就是告诉编译器存在这个函数。

我们先来观察下面这段代码:

#include <stdio.h>
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 m, int n)
{
  return m + n;
}

我们的编译器是从上往下按顺序读取代码的,当在main函数中读取到Add函数时会报一个警告,因为它没有找到Add函数的定义。

如果在main函数之前加上一串代码:

int Add(int m, int n);

这样警告就会消除了,而这段代码就是函数的声明,如果调用函数之前没有定义该函数,就需要提前对该函数进行声明,来消除编译器的警告。

一般情况,如果直接将函数的定义放在main函数之前,我们就不需要再对函数进行声明了。

2.函数声明和定义的使用

我们在编写程序的时候一般不会将主函数和其他自定义的函数写在同一个文件中,这时我们会用到函数的声明和定义,我们还以Add函数为例:

我们将主函数独立放在一个test.c文件中;

将Add函数的声明放在一个Add.h文件中;

将Add函数的定义放在一个Add.c文件中;

Add.c:

int Add(int m, int n)
{
  return m + n;
}

Add.h:

int Add(int m, int n);

test.c:

#include <stdio.h>
#include "Add.h"
int main()
{
  int a = 0;
  int b = 0;
  scanf("%d %d", &a, &b);
  int ret = Add(a, b);
  printf("%d\n", ret);
  return 0;
}

这就是函数的声明和定义的一般使用,我们一般是将函数的声明放在.h的文件中,在使用时需要包含这个.h文件,而我们在包含时用的是双引号,与库函数的引用尖括号有所区别。

这样写的好处是在我们在实现一个功能复杂的程序的时候,我们可以将这个功能拆开分成不同的步骤,把每个要实现的步骤交给不同的人,让多个人来完成一个程序的实现,这样就形成了一个模块化实现程序的样例,每个人都写一个.c和.h的文件来实现某一步的程序,最后将所有人的.h文件放在一个.h文件中,所有的.c文件放在一个.c文件中,最终一个复杂的程序被分为几个步骤由不同的人来完成。

三、变量的声明和定义

变量的声明和定义其实和函数是一样的,只是函数更加普遍一些。

如下一段代码:

#include <stdio.h>
int main()
{
  printf("%d\n", value);
  return 0;
}
int value = 2022;

这里同样会报一个警告,告诉我们value未定义,所以我们同样要将定义或声明放在main函数之前。

这里我们再扩展一个小知识:

如下代码:

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

这里我们将value作为全局变量,像这种声明但却没有定义的写法,就是将声明作为定义来看,它没有被赋值,默认为0。

四、函数递归

1.什么是递归

程序调用自身的编程技巧称为递归,递归就是自己调用自己,作为一种算法在程序设计语言中广泛应用,通常把一个大型复杂的问题转换成一个与原问题相似的规模较小的问题来求解,只需少量的程序就可描述出解题过程的多次计算,减少了程序的代码量,递归就是把大事化小。

递归:

递:递推

归:回归

我们观察如下简单递归代码:

#include <stdio.h>
int main()
{
  printf("hello!\n");
  main();
  return 0;
}

可以看到,这是在main函数内又调用了main函数,而调用的main函数内又有main函数,如此重复调用下去,就成了一个递归函数,它会死循环的打印hello!,最终导致程序崩溃,所以它是个错误的递归。

而当程序崩溃后,我们通过监视可以看到下面的一条信息:

这里程序告诉我们的异常是Stack overflow,也就是栈溢出

为什么会有栈溢出呢,我们要知道,在内存中,每调用一次函数,都会为本次函数,在内存的栈区上开辟一块内存空间,当栈区的内存空间被用完的时候,再想往栈区开辟空间,就会出现栈溢出的现象。


2.递归的两个必要条件

1.必须存在限制条件,当不满足这个条件的时候,递归结束

2.每次递归调用后都必须接近这个限制条件

2.1接收一个整型(无符号),按顺序打印它的每一位

如:输入1234

打印:1 2 3 4

#include <stdio.h>
void Print(unsigned int m)
{
  while (m)
  {
    printf("%d ", m % 10);
    m /= 10;
  }
}
int main()
{
  unsigned int num = 0;
  scanf("%u", &num);
  Print(num);
  printf("\n");
  return 0;
}

结果为:

我们观察到,当输入的值为1234的时候,将1234%10得到的值为4,打印出来,然后再通过1234/10得到的值为123,将这两步放入while循环中,当传入的值为0的时候循环结束,但由此打印出来的结果是4 3 2 1,并不符合题意按顺序打印,所以我们需要改变一下思路,通过递归的方式如下:

#include <stdio.h>
void Print(unsigned int m)
{
  if (m > 9)//限制条件
  {
    Print(m / 10);//打印123
  }
  printf("%d ", m % 10);//打印4
}
int main()
{
  unsigned int num = 0;
  scanf("%u", &num);
  Print(num);
  printf("\n");
  return 0;
}

代码结果:

它的实现思路其实就是通过递归先完成高位的打印,然后一步步按顺序打印出来:

它的每次调用就是一次递推,而每次返回的值就是一次回归,这样看来递归确实用少量的代码就实现了复杂的问题。


2.2不创建临时变量,求字符串长度

我们知道有一个库函数可以实现求字符串长度,也就是strlen()函数,我们先忽略“不创建临时变量”,只看“求字符串长度”,这就是要求我们模拟实现strlen()函数,要想模拟实现strlen()函数,就要了解它是如何求出字符串长度的:

我们要知道在一个字符数组中,数组名就是字符数组首元素地址,而一个字符串是以/0结尾的,这时我们就可以定义一个整型变量count,先让一个指针指向字符串的首元素,解引用这个指针并判断是否为\0,如果不是\0,,count自增1,指针也自增1,当解引用为\0的时候,返回count的值就是字符串的长度。

模拟strlen()代码:

#include <stdio.h>
int my_strlen(char* s)
{
  int count = 0;
  while (*s != '\0')
  {
    count++;
    s++;
  }
  return count;
}
int main()
{
  char arr[] = "hello";
  int len = my_strlen(arr);
  printf("%d\n", len);
  return 0;
}

我们虽然完成strlen()函数的模拟实现,但却用到了临时变量count,那么如何对这段代码进行改进,不用临时变量,也能实现求字符串的长度呢?

我们可以用到递归的思想,可以想到:

假设求一个字符串"abcd"

也就是my_strlen(“abcd”),只要第一个字符不是‘\0’,那么它的长度至少为1

于是:

len=1+my_strlen(“abc”)

len=1+1+my_strlen(“ab”)

len=1+1+1+my_strlen(“a”)

len=1+1+1+1+my_strlen(“\0”)

len=1+1+1+1+0=4

代码实现:

#include <stdio.h>
int my_strlen(char* s)
{
  if (*s != '\0')
    return 1 + my_strlen(s + 1);
  else
    return 0;
}
int main()
{
  char arr[] = "hello";
  int len = my_strlen(arr);
  printf("%d\n", len);
  return 0;
}

在这里我们扩充一个小知识:

char类型的指针+1,每次跳过一个字符类型,也就是一个字节

int类型的指针+1,每次跳过一个整型,也就是四个字节

这里巧妙地用了递归的方式在不创建临时变量的前提下完成了求字符串长度。

两次的执行结果均一样:


3.递归与迭代

迭代就是使用非递归的方式

3.1求n的阶乘

递归方式:

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

求n的阶乘,可以看成是求n乘以(n-1)的阶乘,而n-1的阶乘又可以看成n-1乘以(n-2)的阶乘,这就可以很好的运用递归思路,当n为1时返回1,否则就调用n*(n-1)的方式。

递归在有公式的时候是很容易想出来的,同样我们也可以通过非递归的方式来实现,

非递归方式:

#include <stdio.h>
int Fac(int n)
{
  int i = 0;
  int ret = 1;
  for (i = 1; i <= n; i++)
  {
    ret *= i;
  }
  return ret;
}
int main()
{
  int num = 0;
  scanf("%d", &num);
  int ret = Fac(num);
  printf("%d\n", ret);
  return 0;
}

可以看出,通过一段for循环也能很容易的实现阶乘的求法。

最终结果:

当num=3时,他们的结果均为6:

这里要注意一点:在使用递归和迭代的方式来实现时都会有一个缺陷,如果求比较大的数的阶乘,比如1000,在非递归方式中,由于1000的阶乘太大,整型变量ret接收不了这么大的数字,最终会返回0。在递归的方式中,最终结果也是0,并且程序会因为栈溢出而崩溃,这两种方式虽然最后结果都为0,但非递归方式并没有崩溃。

这样看来,我们在使用递归时还要谨慎使用,尽量能不用递归就不用递归。


3.2求第n个斐波那契数

1 1 2 3 5 8 13…

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)在现代物理、准晶体结构、化学等领域,斐波那契数列都有直接的应用,为此,美国数学会从 1963 年起出版了以《斐波那契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

代码如下:

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

利用递归的方式看起来非常简单,可是它的效率却不高,原因是它会有很多重复调用:

这样看来我们使用递归的方式来实现并不是最优的方式,我们对代码进一步优化:

我们使用循环的方式来实现:

新的思路是,我们先定义三个变量a,b,c

一组斐波那契数:1 1 2 3 5 8 13 21 34 55…

第一步:a=1,b=1,c=a+b

第二步:a=b,b=c,c=a+b

依此一直循环最后求出

最终代码:

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

所以再次强调,能不用递归就不用递归,有时递归的方式会存在缺陷(栈溢出)和效率低的问题。

程序结果:


4.解决递归时的栈溢出问题

当我们用递归写出一个程序时,有时接受的数据较大,在执行递归的时候会出现栈溢出(Stack overflow)的现象,这时我们往往通过以下两种方式解决:

1.将递归改成非递归,有些代码用递归写起来可读性更高,但用非递归的写法也能够实现,在递归出现问题时,可以想出一个非递归的方式。

2.使用static对象替代局部对象,,这样不仅可以减少每次递归调用和返回时产生和释放局部对象的开销,而且static还可以保存递归调用的中间状态,并且可被各个调用层访问。

相关文章
|
1天前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
36 16
|
1天前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
11 3
|
1天前
|
存储 算法 安全
【C语言程序设计——函数】分数数列求和1(头歌实践教学平台习题)【合集】
if 语句是最基础的形式,当条件为真时执行其内部的语句块;switch 语句则适用于针对一个表达式的多个固定值进行判断,根据表达式的值与各个 case 后的常量值匹配情况,执行相应 case 分支下的语句,直到遇到 break 语句跳出 switch 结构,若没有匹配值则执行 default 分支(可选)。例如,在判断一个数是否大于 10 的场景中,条件表达式为 “num> 10”,这里的 “num” 是程序中的变量,通过比较其值与 10 的大小关系来确定条件的真假。常量的值必须是唯一的,且在同一个。
7 2
|
5天前
|
存储 编译器 C语言
【C语言程序设计——函数】回文数判定(头歌实践教学平台习题)【合集】
算术运算于 C 语言仿若精密 “齿轮组”,驱动着数值处理流程。编写函数求区间[100,500]中所有的回文数,要求每行打印10个数。根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码。如果操作数是浮点数,在 C 语言中是不允许直接进行。的结果是 -1,因为 -7 除以 3 商为 -2,余数为 -1;注意:每一个数据输出格式为 printf("%4d", i);的结果是 1,因为 7 除以 -3 商为 -2,余数为 1。取余运算要求两个操作数必须是整数类型,包括。开始你的任务吧,祝你成功!
28 1
|
C语言
C语言及程序设计初步例程-35 问题求解方法——迭代
贺老师教学链接  C语言及程序设计初步 本课讲解 例:求Fibonacci数列前40个数 #include &lt;stdio.h&gt; int main() { long f1,f2,fn; int i; f1=f2=1; printf("%ld\t%ld\t",f1,f2); for(i=3; i&lt;=40; i++) {
967 0
|
5天前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
42 23
|
5天前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
31 15
|
5天前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
41 24
|
1月前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
75 10
|
1月前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
60 9