函数的认识
如果我们想要实现一台智能ATM机,我们该怎么办?
我们是不是应该从智能ATM机需要什么功能出发,比如说:存钱,取钱,查看余额,修改密码,转账等等。
就以存钱功能来看,当我们想要存钱的时候,是不是需要插入银行卡,并且需要验证这张卡是不是属于我们的,需要输入密码,然后我们就可以把钱放进去,在最后还需要再次确认我们存多少钱。
我们来看,在存钱这个功能下,我们还可以划分出这么多更细的功能。
这就是C语言中的代码模块化,而函数就是实现模块化的好方法。
当我们需要编写C语言来实现一个功能,我们可以对这个功能进行拆分,自顶而下,逐步求精。拆到你可以写出这些小功能,再把这些小功能整合,来实现你需要实现的大功能。
函数设计原则:
单一职责
高内聚 低耦合
函数传递数据:
全局变量 --- 不建议用,会破坏的c语言结构化编程
形参变量 --- 推荐
函数的实现
函数的定义
类型标识符 函数名(形式参数) //函数头 --head
{
//声明部分
//语句部分
函数体//body
return 表达式; //返回函数处理的结果
}
函数头:
(1)类型标识符 --- int/short/long ...
返回值类型 //说明函数返回的结果的数据类型
注意:
1.返回值结果 和 设计的 类型标识符 可能不一致
此时,返回值结果 会被 转换成 类型标识符 指定的类型
2.如果返回值类型省略不写
默认 int类型
3.如果不需要带出返回值
void --- 空类型
对应的 return 后面不能有值
return;
(2)函数名 --- 标识符
体现函数功能的
见名知意
注意:
1.形参 与 实参
类型匹配
个数相同
2.形参
int a,b,c; //普通的定义变量的书写方式
int a,int b,int c //形参变量,每个变量都必须明确的指定类型
3.如果函数功能,不需要传入数据 --- 形参一般设计为 void
4.函数名 --代表 的是 函数的入口地址
void menu(void)
{
printf("------mp3----\n");
}
(3)形式参数 --- 本质是是变量
作用 --用来接受 实际的参数
语法:
类型 形参变量名1, 类型 形参变量名2, 类型 形参变量名3,...
函数体:
//函数对应功能 具体代码
函数定义的位置
C语言中所有 标识符 都必须先定义,后使用
main函数是整个程序的入口函数
定义的位置
main之前
main之后:
必须在使用前,做一下函数声明
函数声明,是指将函数头复制,然后加上分号
函数的使用
函数调用:
函数名(实际参数)
函数嵌套调用
特殊的嵌套调用 --- 自己调用到自己 ,称为递归
递归:
1.本质上是个循环
2.这种循环 一定会结束
因为栈的空间有限
3.递归的效率并不高
因为有些问题的解决,
使用递归将变得非常简单
递归的理解:
递归分为两个过程:递推和回归。
递推过程为,从要解决的问题N往前以N与N-1之间相同的关系推断,直到结束标志,这时在从递推结束的地方不断回归,从一个个的嵌套中返回值,直到返回问题N,这时,就使用递归的方法解决了问题N。
递归解决问题:
需要是一个函数
1.递推关系
问题n 与 问题n-1 之间的递推关系
2.递归结束的条件
返回值类型 递归函数(形参 )
{
if (是否到达结束条件)
{
//是 回归
return 表达式;
}else
{ //假 递推
return 问题n和n-1 的 递推结果
}
}
例如使用递归,求1~100个数的和:
我们需要理解,求1~100的和,是不是求1~99的和再+100,那求1~99的和是不是求出1~98的和再+99,那这样一层层的往下,求1~2的和是不是1+2,1就是等于1,那么随着以这种N与N-1之间的关系不断递推,我们到达了问题N的末尾,这时,我们就开始返回了,把1返回到1+2,把1+2返回到sum(2)+3,我们不断的从一个个嵌套中返回,最后我们返回sum(99)的值到sum(99)+100,这时,我们就得到了1~100的累加和,然后把和返回,函数调用完毕,求1~100的和的问题解决了。
在递归中,最重要的就是每一个问题间的关系,和递推结束的条件了。
如果我们不断的递推,但是没有结束标志,那么就会导致栈溢出,产生段错误。
为什么会产生栈溢出,这就是我们下面要讲的,函数嵌套调用的本质,函数为什么可以嵌套调用。
递归的应用——汉诺塔递归:
1. 将 n-1 个盘子从 A(起始)->B(辅助)
2. 将 剩下的那个盘子从 A(起始)->C(目标)
3. 将 n-1 个盘子从 B(辅助)->C(目标)
函数嵌套调用的本质
当我们在一个函数中调用另一个函数,再在这个函数中调用其他的函数,因为每一个函数名都是一个地址,而我们在函数内部编写的代码都存储在这个以地址为开始的空间,所以函数的不断调用其实就是在不同的地址空间中执行程序。那为什么在一个函数调用结束后,系统能准确的找到函数结束后应该继续执行的程序呢?
那是因为系统在内存里开辟了一个专门用来存放相关数据的空间,这个空间具备栈结构的特点,先进后出,所以第一个调用函数的相关数据就存放在栈底,而最后一个调用数据就存放在栈顶,那么当最后一个函数调用结束后,栈顶的数据进行出栈,系统回到最后一个函数调用前的位置继续执行程序,当函数一个个结束,栈中的数据一个个按顺序出栈,由于先进后出,所以系统能准确的找到接下来要执行的程序。栈的开辟无需申请,栈的空间自动释放,所以我们才不需要在程序里对栈进行操作。
但我们能不断的调用函数吗?
当然是不能的,栈开辟在内存里,栈的空间是有限的,Linux系统给每一个栈分配的默认大小是8M(但是栈的大小是可以被改变的)。
数组作为函数参数
数组:
整型一维
字符型一维
整型二维
字符型二维
int a[10] = {1,2,3,4,5};
a // --- 整个数组
a[i] // --- 某个元素
a[i] //作实参 --- 数组元素做函数实参 --- 和普通变量做函数实参没区别
注意:
1.整个数组作为函数参数
从类型角度
printArray(int a[10]) //
printArray(int *a) //编译器的理解 --- int *a //指针类型 --- 需要的是一个地址
//从值的角度 --- 数组名代表 首元素的地址
//数组的特点:
// 连续性 ,有序性,单一性
// 64位的平台上 ,指针大小都是一样的 8字节
写法:
//形参
printArray(int a[10],int len);//可以写成这个形式 --- 提高可读性
printArray(int *a,int len); //编译器实际理解的形式
//实参 --- 数组名 数组长度
printArray(a,len);
C语言的程序(5个区):
栈
函数调用的相关信息
局部变量 --- 自动申请,自动释放
堆
空间非常大
手动申请,手动释放
全局区/静态区
放的就是全局变量 静态变量 //默认初始化为 0
字符串常量区
存放 "hello" 这样的字符串常量
代码区
机器指令