Bootstrap

18-函数

函数是什么?

数学中我们常见到函数的概念。但是你了解C语言中的函数吗?维基百科中对函数的定义:子程序

  • 在计算机科学中,子程序(英语:Subroutine,procedure,function,routine,method,subprogram, callable unit ),是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
  • 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

C语言中函数的分类:

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

库函数:

为什么会有库函数?

1.我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能∶将信息按照一定的格式打印到屏幕上( printf )。
2.在编程的过程中我们会频繁的做一些字符串的拷贝工作 ( strcpy ) .
3.在编程是我们也计算,总是会计算n的k次方这样的运算( pow )。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。

那怎么学习库函数呢?

这里我们简单看看:
www.cplusplus.com
zh.cppreference.com

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

  • IO函数 (主要是输入输出函数:printf、scanf、getchar、putchar)
  • 字符串操作函数 (strcmp、strlen)
  • 字符操作函数 (toupper)
  • 内存操作函数 (memcpy、memcmp、memset)
  • 时间/日期函数 (time)
  • 数学函数 (sqrt、pow)
  • 其他库函数

我们参照文档,学习几个库函数:(学会如何使用文档进行库函数的学习)
strcpy(链接
image.png

memset(链接)
image.png

自定义函数

自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。

函数的组成:

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

image.png

举个例子:

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

image.png

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

image.png
写完发现没交换
F10调试分析一下
image.png
image.png
然后F11,F11遇到函数会进入函数,F10遇到函数则不会进入
image.png
image.png
image.png
image.png
所以问题出在哪?
因为a和b的空间和上面x和y是两片独立的空间,所以对x和y的改变不会影响a和b
重新编写:
image.png

函数的参数

实际参数(实参):

真实传给函数的参数,叫实参。实参可以是∶常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

形式参数(形参):

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

image.png
image.png
形参的生命周期和局部变量一样,都是在函数的内部
函数调用是实参,函数定义是形参

函数的调用:

传值调用

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传值调用的时候形参是实参的一份临时拷贝
传值调用就是直接将实参的值传递给形参。这样形参和实参的值是一样的,但是函数的形参和实参分别占有不同的内存块,实参是一个独立的个体,形参也是一个独立的个体,只是形参的值与实参相同。
但实际两者之间并没有建立起真正的联系,对形参的修改不会影响实参。

传址调用

  • 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
  • 这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操作函数外部的变量。

练习

1.写一个函数可以判断一个数是不是素数

image.png

2.写一个函数判断一年是不是闰年

image.png
另一种方法
image.png

3.写一个函数,实现一个整形有序数组的二分查找

image.png

4.写一个函数,每调用一次这个函数,就会将num的值加1

image.png

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

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

嵌套调用

image.png

链式访问

image.png
image.png
image.png

函数的声明和定义

函数声明:

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

函数定义:

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

函数递归

什么是递归?

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

程序调用自身的编程技巧称为递归,是函数自己调用自己。
递归是一个函数在其定义中直接或间接调用自身的一种方法,它通常把一个大型的复杂的问题转化为一个与原问题相似的规模较小的问题来解决,递归的能力在于用有限的语句来定义对象的无限集合。

递归三要素:①确定终止条件;②确定返回值;③确定循环过程。

优点:代码更简洁清晰,可读性更好。
缺点:时间和空间消耗比较大。每一次函数调用都需要在内存栈中分配空间以保存参数,返回地址以及临时变量,而且往栈里面压入数据和弹出都需要时间。

另外递归会有重复的计算。递归本质是把一个问题分解为多个问题,如果这多个问题存在重复计算,有时候会随着n成指数增长。斐波那契的递归就是一个例子。
递归还有栈溢出的问题,每个进程的栈容量是有限的。由于递归需要系统堆栈,所以空间消耗要比非递归代码要大很多。而且,如果递归深度太大,可能系统撑不住。

递归的两个必要条件

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

最简单的递归 - 错误的案例
image.png
为什么会这样?
image.png
F10调试看一下
发现报错了
image.png

练习1

接受一个整型值(无符号),按照顺序打印它的每一位。例如:输入:1234,输出:1 2 3 4
image.png
利用递归解决
image.png
image.png

栈溢出

看下面这代码
image.png
哪里出了问题呢?
F10调试看一下
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
发现出现了栈溢出
image.png
所以!
写递归代码的时候:

  1. 不能死递归,必须有跳出条件,且每次递归逐渐趋近于跳出条件
  2. 递归不能太深,即使有跳出条件,递归层次也不能太深,否则也会造成栈溢出

推荐一个网站:https://stackoverflow.com/
相当于程序员的知乎!!!

练习2

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

直接利用strlen函数

image.png
不满足题目要求,没有创建函数解决

编写函数解题

image.png
image.png
但是依旧创建了临时变量count来统计数组的元素个数,不满足题目要求
image.png

利用递归解题的思路

my_strlen("bit");		//识别到第一个字符不是'\0',就剥离出去一个1
1 + my_strlen("it");	//继续识别,第二个字符不是'\0',再剥离一个1
1 + 1 + my_strlen("t");	//继续识别,第三个字符不是'\0',再剥离一个
1 + 1 + 1 + my_strlen("");	//三个字符都识别后没了,就剩'\0',所以是空,长度是0,所以就是1+1+1+0=3
//大事化小

image.png
image.png

递归与迭代

什么是迭代?

利用变量的原值推算出变量的一个新值。如果递归是自己调用自己的话,迭代就是A不停的调用B。
递归中一定有迭代,但是迭代中不一定有递归。大部分可以相互转换,能用迭代的不用递归。
迭代三要素:①确定迭代变量;②确定迭代关系;③确定迭代过程的控制。

优点:计算效率高,无额外内存开销。
缺点:代码不如递归简洁,有时不容易理解。

练习3

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

利用迭代解题

image.png

利用递归解题

image.png
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

练习4

求第n个斐波那契数(不考虑溢出)
斐波那契数就是:
1 1 2 3 5 8 13 21 34 55 ···
前两数之和等于第三个数

利用递归解题

image.png
image.png
因为效率太低了,重复大量的计算!
image.png
不光有重复的计算,第一次是21第二次是22一直递增下去,大概要到2^29才会全部算完
我们加个count看一下计算了多少次
image.png

另一种递归效率快的方法

image.png
image.png

作业

1.关于while(条件表达式)循环体,以下叙述正确的是(B)?(假设循环体里没有break,continue,return,goto等等语句)

A.循环体的执行次数总是比条件表达式的执行次数多一次
B.条件表达式的执行次数总是比循环体的执行次数多一次
c.条件表达式的执行次数与循环体的执行次数一样
D.条件表达式的执行次数与循环体的执行次数无关

2.有以下程序,最终a为?

#include <stdio.h>
int main()
{
	int a = 0, b = 0;
    for (a = 1, b = 1; a <= 100; a++)
    {
        if(b >= 20)
            break;
        if(b % 3 ==1)
        {
            b = b + 3;
            continue;
        }
        b = b - 5;
    }
    printf("%d\n", a);
    return 0;
}

a=1 a=2 a=3 a=4 a=5 a=6 a=7 a=7 a=8
b=1 b=4 b=7 b=10 b=13 b=16 b=19 b=21
a =8 时b=21>=20跳出循环
最终a = 8

3.编写程序数一下1到100的所有整数中出现多少个数字9

image.png

4.计算1/1-1/2+1/3-1/4+1/5…+1/99-1/100的值,打印出结果

image.png
image.png
image.png
image.png

5.求十个整数中最大的数

image.png
image.png
image.png

6. 在屏幕上输出9*9乘法口诀表

image.png
image.png

7.以下关于函数设计不正确的说法是(B)

A.函数设计应该追求高内聚低耦合
B.要尽可能多的使用全局变量
C.函数参数不易过多
D.设计函数时,尽量做到谁申请的资源就由谁来释放

8.关于C语言函数描述正确的是(C)

A.函数必须有参数和返回值
B.函数的实参只能是变量
C.库函数的使用必须要包含对应的头文件
D.有了库函数就不需要自定函数了

9.C语言规定,在一个源程序中,main函数的位置(C)


A.必须在最开始
B.必须在库函数的后面
C.可以任意
D.必须在最后
main函数只是程序的入口,在它前面或者后面可能还会有很多自定义函数

10.以下叙述中不正确的是:(D)

A.在不同的函数中可以使用相同名字的变量
B.函数中的形式参数是在栈中保存
C.在一个函数内定义的变量只在本函数范围内有效
D.在一个函数内复合语句中定义的变量在本函数范围内有效(复合语句指函数中的成对括号构成的代码)

;