初阶C语言——函数【详解】(重点:函数的递归)

简介: 初阶C语言——函数【详解】(重点:函数的递归)

C语言中提供了可以随意滥用的 goto语句和标记跳转的标号。

从理论上 goto语句是没有必要的,实践中没有goto语句也可以很容易的写出代码。

但是某些场合下goto语句还是用得着的,最常见的用法就是终止程序在某些深度嵌套的结构的处理过程。

例如一次跳出两层或多层循环。

多层循环这种情况使用break是达不到目的的(break一次只能跳出一次循环)。它只能从最内层循环退出到上一层的循环。

goto语言真正适合的场景如下

for(...)
   for(...)
     {
        for(...)
           {
              if(disaster)
              goto error;
           }
      }     
error:
if(disaster)
// 处理错误情况


关机程序:

程序运行起来后,倒计时60秒后关机,如果输入我是猪,就取消关机

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
  char input[10] = { 0 };
  system("shutdown -s -t 60");
  //shutdown为Windows系统自带的关机操作程序
  //-s设置关机;-t设置时间关机;60关机倒计时
again:
  printf("电脑将在1分钟内关机,如果输入:我是猪,就取消关机!\n请输入:>");
  scanf("%s", input);
  if (0 == strcmp(input, "我是猪"))
  {
    system("shutdown -a");
    //-a取消关机
  }
  else
  {
    goto again;
  }
  return 0;
}


1.什么是函数

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

在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,

subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。

一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。


2. 函数分类

  1. 库函数
  2. 自定义函数


2.1 库函数:

在编写代码时我们会频繁的使用一个功能如:打印到屏幕上,字符串的拷贝,编程时的计算。

像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,

为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员

进行软件开发。

怎么学习库函数呢?

这里我们有一个网站可以学习: www.cplusplus.com

点开我们能看到各种函数相关的介绍,方便使用时进行查找。


库函数的学习不需要全部记住,需要学会查询工具的使用:

www.cplusplus.com

http://en.cppreference.com(英文版)

http://zh.cppreference.com(中文版)

总结: C语言常用的库函数都有

  • IO函数(输入输出函数)
  • 字符串操作函数(strlen函数,strcmp函数)
  • 字符操作函数(大小写转换,判断是数字还是字母)
  • 内存操作函数
  • 时间/日期函数(time函数)
  • 数学函数
  • 其他库函数
    注: 使用库函数,必须包含 #include 对应的头文件。


2.2 自定义函数

自定义函数和库函数一样,有函数名,返回值类型和函数参数。

但是不一样的是这些都是我们自己来设计。

函数的组成:

ret_type fun_name(para1, * )
{
      statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数


举例:

写一个函数可以找出两个整数中的最大值。

#include <stdio.h>
int get_max(int x,int y)
{
  if (x > y)
    return x;
  else
    return y;
}
int main()
{
  int a=0;
  int b=0;
  scanf("%d %d", &a, &b);
  int m = get_max(a, b);
  printf("%d", m);
  return 0;
}


运行结果

3.函数的参数

3.1 实际参数(实参)

真实传给函数的参数,叫实参。

实参可以是:常量、变量、表达式、函数等。

无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

3.2 形式参数(形参)

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

举例说明:

写一个函数可以交换两个整形变量的内容。

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


运行结果如下

我们发现编译后的结果并不交换,此时就需要我们考虑形参和实参的问题了


这里可以看到 Swap1 函数在调用的时候, x , y 拥有自己的空间,同时拥有了和实参一模一样的内容。

所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。

修改后

#include <stdio.h>
void Swap2(int* pa, int* pb)
{
  int tmp = *pa;//tmp = a;
  *pa = *pb;    //a=b;
  *pb = tmp;    //b=tmp
}
int main()
{
  int a = 0;
  int b = 0;
  scanf("%d%d", &a, &b);
  printf("交换前:a=%d b=%d\n", a, b);
  Swap2(&a, &b);
  printf("交换后:a=%d b=%d\n", a, b);
  return 0;
}

运行结果如下

给Swap传的是a,b的地址,通过pa远程找到a,通过pb远程找到b


什么时候传值什么时候传址?

当我们只是用这个值时,就传值,如果我们还需要对这个值进行修改,就传址

4. 函数的调用:

4.1 传值调用

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

4.2 传址调用

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。

这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操

作函数外部的变量。

传值调用: 把变量本身直接调过去,形参相当于实参的一份临时拷贝,对形参的修改不会影响实参。

传址调用: 把变量地址传过去,通过地址找到外部的变量a,b,从而通过指针来修改函数外部的变量a,b

4.3 练习

  1. 写一个函数可以判断一个数是不是素数。
#include <math.h>
#include <stdio.h>
//是素数返回1
//不是素数返回0
int is_prime(int i)
{
  int j = 0;
  for (j = 2; j <= sqrt(i); j++)
  {
    if (i % j == 0)
      return 0;
  }
  return 1;
}
int main()
{
  int i = 0;
  int count = 0;
  for (i = 100; i <= 200; i++)
  {
    //判断i是否为素数
    if (1 == is_prime(i))
    {
      count++;
      printf("%d ", i);
    }
  }
  printf("\ncount = %d\n", count);
  return 0;
}


  1. 写一个函数判断一年是不是闰年。
#include <stdio.h>
 int is_leap_year(int y)
{
  return ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0);
}
int main()
{
  int y = 0;
  for (y = 1000; y <= 2000; y++)
  {
    //判断y是否为闰年
    //如果是闰年返回1
    //不是闰年返回0
    if (is_leap_year(y))
    {
      printf("%d ", y);
    }
  }
  return 0;
}


  1. 写一个函数,实现一个整形有序数组的二分查找
#include <stdio.h>
int binary_search(int arr[], int k , int sz)
{
  int left = 0;
  int right = sz - 1;
  while (left<=right)
  {
    int mid = left + (right - left) / 2;
    if (arr[mid] < k)
    {
      left = mid + 1;
    }
    else if (arr[mid] > k)
    {
      right = mid - 1;
    }
    else
    {
      return mid;
    }
  }
  return -1;
}
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int k = 0;
  scanf("%d", &k);
  int sz = sizeof(arr) / sizeof(arr[0]);
  //找到了就返回下标,找不到就返回-1
  int ret = binary_search(arr, k, sz);
  if (ret == -1)
    printf("找不到\n");
  else
    printf("找到了,下标是:%d\n", ret);
  return 0;
}


  1. 写一个函数,每调用一次这个函数,就会将 num 的值增加1
#include <stdio.h>
void Add(int* p)
{
  //*p = *p+1;
  (*p)++;
}
int main()
{
  int num = 0;
  Add(&num);
  printf("%d\n", num);
  Add(&num);
  printf("%d\n", num);
  Add(&num);
  printf("%d\n", num);
  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;
}


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

5.2 链式访问

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

#include <stdio.h>
#include <string.h>
int main()
{
  printf("%d\n", strlen("abcdef"));
  return 0;
}
//打印结果为6,是字符串的长度


#include <stdio.h>
int main()
{
  printf("%d", printf("%d", printf("%d", 43)));
  //结果是啥?
  //结果为4321
  return 0;
}

这里我们可以利用 www.cplusplus.com来进行查询



查询后发现printf函数的返回值是打印在屏幕上字符的个数

6. 函数的声明和定义

6.1 函数声明:

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

6.2 函数定义:

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

#include <stdio.h>
//函数的声明
int Add(int x, int y);
int main()
{
  int a = 10;
  int b = 20;
  int c = Add(a, b);
  printf("%d\n", c);
  return 0;
}
//函数的定义
int Add(int x, int y)
{
  return x + y;
}


但是一般我们不会这样来写代码,这样写只是为了更方便的理解函数的定义与声明

正常写法

#include <stdio.h>
//函数的定义
int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int a = 10;
  int b = 20;
  int c = Add(a, b);
  printf("%d\n", c);
  return 0;
}

add.h的内容

放置函数的声明

//函数的声明
int Add(int x, int y);


add.c的内容

函数的定义

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


test.c的内容

函数的实现


7. 函数递归

7.1 什么是递归?


程序调用自身的编程技巧称为递归( recursion)。

递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接

调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解

递归策略

只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

递归的主要思考方式在于:把大事化小


7.2 递归的两个必要条件

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

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

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

当我们写了一个这样的函数之后发现函数打印结果有问题,那么问题是什么呢?

因为函数无限的调用导致栈溢出,至于函数的栈区堆区,我会在之后单独出一篇文章来讲解。


7.2.1 练习:

  • 练习1

接受一个整型值,按照顺序打印它的每一位。

例如:

输入:1234,输出 1 2 3 4


%u - 无符号的整数

%d - 有符号的整数

思路: 把1234转化成(123)4,(12)3 4,(1)2 3 4

Print(1234)

Print(123) 4

Print(12) 3 4

Print(1) 2 3 4

**void Print(unsigned int n)
{
  if (n > 9)//递归的限制条件
  {
    Print(n / 10);//每次递归后越来越接近这个限制条件。
  }
  printf("%d ", n % 10);
}
int main()
{
  unsigned int num = 0;
  scanf("%u", &num);
  Print(num);
  return 0;
}**


画图讲解如下:

  • 练习2

编写函数不允许创建临时变量,求字符串的长度。

思路: 把一个长的字符串转化为为几个小的相加,如图所示

#include <stdio.h>
int my_strlen(char* str)
{//strlrn统计的是\0之前出现的字符个数
  if (*str != '\0')
    return 1 + my_strlen(str+1);
  else
    return 0;
}
int main()
{
  char arr[10] = "abc";
  int len = my_strlen(arr);
  printf("%d\n", len);
  return 0;
}

图像说明:


7.3 递归与迭代

迭代:通过循环不断地重复一个过程,循环是迭代的一种。迭代和递归本质上是一种方法。

7.3.1 练习

1.求n的阶乘。(不考虑溢出)


迭代方法:

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


递归方法:

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


  1. 求第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);//40
  int ret = Fib(n);
  printf("%d\n", ret);
  //printf("count = %d\n", count);
  return 0;
}

但是我们发现有问题:在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。

我们以40举例说明


在这里我们可以发现如果要找40的斐波那契数,它下边类似35的这种小数会进行大量的重复计算,从而造成效率低的问题

修改后如下:

#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.但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。

3.当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。


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

热门文章

最新文章