1,什么是函数
function ->功能
c语言中函数是指能够完成某个特定功能的代码块的封装形式
怎么封装? -》把实现某个特定功能的代码块放在一个 {}内部
比如前面写的: //选择排序 void test5() { int a[10] = {13,2,6,5,47,100,234,90,-20,0}; int n = 10; int i,j,temp; int max_index; for(i=0;i<n-1;i++) { max_index = 0;//假设下标为0的元素是最大的 for(j=0;j<n-i;j++) { if(a[j]>a[max_index]) { max_index = j; } } //交换 a[max_index] 和 a[n-1-i]的值 if(max_index != n-1-i) { temp = a[max_index]; a[max_index] = a[n-1-i]; a[n-1-i] = temp; } } for(i=0;i<n;i++) { printf("%d ",a[i]); } printf("\n"); } test5就是一个函数,它把能够实现选择排序的代码封装在一个{}内部 封装函数的优点: 功能模块化 代码重复使用 增强代码的可读性 方便后续的维护、升级 test5这个函数其实不是一个合格的函数,它只是用于课堂上的测试某个功能而已,不能用于实际的项目 开发中。 原因是:他只能排序 int a[10] = {13,2,6,5,47,100,234,90,-20,0}; 这个固定的数组 不能排序其它的数组,比如 int a[7] = {32,4,66,8,234,5,0}; 因为在程序的运行过程中, test5的代码不能被修改了,是固定的 //2,定义一个数组并初始化,求这个数组所有元素之和 void test2() { double b[5] = {5.5,1.8,3.4,0.9,1.1}; double sum = 0; int i; for(i=0;i<5;i++) { sum += b[i]; } printf("sum=%lf\n",sum); } 这个函数也只能作为一个测试函数,而不能作为实际项目开发中的函数 原因是:1, 它只能求一个固定数组的元素和 2,求完之后,只是把元素和打印出来,实际情况可能: 需要把这个数据保存到数据库 需要把这个数据通过网络发送给别人 需要把这个数据参与某种运算 ......
2,如何写好一个函数
定义函数的语法:
函数的返回值类型 函数名(形式参数列表) <- 函数头
{
函数体语句块 <- 函数体
}
说明: 函数与函数之间是独立的,不要在一个函数中定义另一个函数(虽然可能不会报错),但是这么做 没有实际意义 函数返回值类型:任意的c语言合法类型 可以是基本类型,也可以是构造类型 int double char void ...指针、结构体 ....... 函数名:一个函数的名字,要符合c语言标识符的规定,最好是顾名思义 底层会把这个函数的名字和函数对应的代码块对应起来 以后通过这个函数名就可以访问/调用到这个函数对应的代码块
(形式参数列表): 形式参数简称为形参 形参列表的语法: (形参1的类型 形参1的名字,形参2的类型 形参2的名字......) 形参的个数是不固定的:可以是0个,1个,2个 ...... 形参的类型也是任意合法c语言类型 如果没有形参,()里面空着 或者()里面写void 如果有多个,以逗号隔开即可 函数体中的代码根据实际功能去写 具体怎么写好一个函数? 从以下四个方面去进行分析 (1) 明确目标:写这个函数用来实现什么功能? 明确目标之后给这个函数取一个对应的函数名 (2) 明确必要的条件 想一想要实现这个功能,需要哪些必要的条件 这些必要的条件数据就是通过参数传输给该函数的 需要几个数据,就要几个形参,数据是什么类型,形参就是什么类型 (3) 明确完成这个任务需要得到的结果是什么 如果结果不需要返回(反馈):函数的返回值类型为 void 如果结果需要返回(反馈):函数的返回值类型为 要返回的这个结果的类型 (4) 具体如何去实现这个功能 具体代码 -》 根据实际情况去写 如果第三步需要反馈结果,那么代码块中必须要有 return语句 return 结果;
例如: 求两个整数之和 (1) 明确目标:写这个函数用来实现什么功能? sum (2) 明确必要的条件 得知道是求哪两个整数的和 需要两个数据 -》 两个形参 两个数据都是int -》 两个形参都是int类型 (int a,int b) (3) 明确完成这个任务需要得到的结果是什么 如果结果不需要返回(反馈):函数的返回值类型为 void 如果结果需要返回(反馈):函数的返回值类型为 要返回的这个结果的类型 需要反馈结果 ,结果的类型是 int -》函数返回值类型为 int (4) 具体如何去实现这个功能 具体代码 -》 根据实际情况去写 如果第三步需要反馈结果,那么代码块中必须要有 return语句 return 结果; 如果没有这句话,那么这个结果就是不确定的值 一个函数,只要运行了 return 语句,这个函数就结束了 分析好四个方面之后,就可以封装函数了 -》 int sum(int a,int b) { int s = a+b;//具体的代码 return s; }
例子:求斐波拉契数列的前n项之和 long fibolacci(int n) { if(n<=0) { printf("输入错误\n"); return -1; } else if(n==1) { return 1; } else { //int a[n] = {1,1};//很多编译器支持写变量,但是写变量的同时不允许初始化 int a[n]; a[0] = 1; a[1] = 1; int i; for(i=2;i<n;i++) { a[i] = a[i-1] + a[i-2]; } long sum=0; for(i=0;i<n;i++) { sum += a[i]; } return sum; } } 练习: 1, 写一个函数,判断一个正整数是否是质数 2, 写一个函数,求两个整数的最大公约数 3, 写一个函数,求两个整数的最小公倍数
除了完成功能之外,还需要写注释。 函数的注释一般描述清楚以下内容: 函数功能 函数每个参数的含义 函数返回值的含义
//greatest common divisor(gcd)
/* 功能:求最大公约数
参数: a,b 就是求他们两个的最大公约数
返回值: 返回a,b的最大公约数*/
/ int gcd(int a,int b)
{
//int a,b;
//scanf("%d%d",&a,&b);
/ * a,b不能由键盘输入,应该由参数传入,原因是:
如果由键盘输入:那么该函数的功能是 先从键盘输入两个整数,然后求他们的最大公约数
如果由参数传入:那么该函数的功能是 求两个整数的最大公约数
从语文表达上就可以看出来,由键盘输入,降低了这个函数功能的通用性 */ int min = a>b?a:b; int i; for(i=min;i>=1;i--) { if(a%i==0 && b%i==0) { //printf("最大公约数为%d\n",i); //break; return i; /* 打印结果和返回结果两种情况分析 打印结果就是把结果输出到屏幕,功能单一 返回结果,别的函数得到结果之后,可以对结果做很多不同的出来: 得到结果之后再参与某种运算,得到结果之后写入本地文件保存, 得到结果之后通过网络发送给别人 .......当然也包括打印输出 可以看出,返回结果比打印结果功能更加通用 */ } }
}
3,函数调用
调用一个已经写好的函数去执行(可以是你自己写好的,也可以是c语言标准库提供的,或者操作系统提供的) 一个函数,如果不调用是不会执行的
调用函数的语法: 函数名(实参列表); (1) 或者 变量名 = 函数名(实参列表); (2) 调用函数的表达式叫做函数调用表达式 说明: 实参列表 : 参数名,参数名 ..... 多个参数以逗号隔开 到底几个参数?? 和函数形参对应,有几个形参就需要几个实参,并且类型要兼容 如果函数的返回值不为 void,那么必须要用一个变量接收函数的返回值 (2) 接收完之后,这个变量的值就是函数 return语句后面的表达式的值。 如果这个函数返回值类型不是 void,但是忘记写 return语句了,函数名(实参列表) 这个表达式的值为不确定的值 总结: 变量名 = 函数名(实参列表); //函数没有return 语句,这个变量就是随机值 //函数有return语句,这个变量就是 return 后面的表达式的值
调用函数的过程:分为3个步骤 1,传参 把实参的值一一对应的传递给形参(这个过程操作系统会为形参分配内存) 2,执行 执行函数体内部的语句直到这个函数结束 3,返回 就是把 return 后面的表达式 赋值给 函数调用表达式 主调函数和被调函数 主调和被调是一个相对关系 调用其它函数的函数叫做主调函数 被其它函数调用的函数叫做被调函数 传参 主调函数 -------------> 被调函数 返回 主调函数 <------------- 被调函数 void func(int a) { a = 200; } void test2() { int x = 100; func(x); printf("%d\n",x); } int main() { test2(); return 0; }
4,函数的声明
声明:告诉编译器,这是一个什么东西,表示这个东西存在。
编译器工作的时候,是从文件的第一行逐行编译的
有以下代码 int main() { test2(); return 0; } void test2() { int x = 100; printf("%d\n",x); } 编译时会报错:implicit declaration of function 'test2'; did you mean 'test1'? 不认识 test2()是什么,因为编译器编译到 main函数中的 test2()时,之前并没有 test2的任何信息 有两种解决方法: 1,把 test2(被调函数)的定义放在 main函数(主调函数)的前面 2,在主调函数前面对被调函数进行声明 《-
声明函数的语法: 函数返回值类型 函数名(形参列表); 其实就是函数头后面加上一个分号
练习: 1,写一个函数,求一个int类型数据二进制补码中1的个数 2,写一个函数,求一个正整数的阶乘 3,写一个函数,打印杨辉三角的前n行
5,变量的作用域和生存期
变量的作用域:就是这个变量起作用的区域,在作用域内可以通过变量名字访问该变量
在作用域外不可以通过变量的名字访问该变量
局部作用域: 在一个{}内可以直接通过变量名访问,{}外就不可以通过变量名直接访问了 如: void test() { int x = 10;//x的作用域就是局部作用域,在这个{}内起作用 x = 20;//可以访问 printf("x=%d\n",x);//可以访问 } int main() { //x = 20;//报错,不认识这个 x //printf("x=%d\n",x);//报错,不认识这个 x if(..) { int y = 5;//y只能在这个{}内被直接访问 } y = 8;//不可以访问 } 函数的形参也是属于局部变量 全局作用域: 在整个工程(目前来讲就是整个文件)中都可以通过变量名直接访问。 一般定义在文件开头位置,在这个文件中的任意函数内都可以通过变量名去访问它 还有一个特殊的全局变量 :被 static修饰的,以后再补充 普通的全局变量的作用域是整个项目工程的所有文件。 被 static修饰的全局变量的作用域是当前文件(工程项目的其它文件中不能直接访问)
变量的生存期: 就是变量生存的时间跨度 从定义(分配内存空间) 到 销毁(对应的内存被回收了) 有这么几种: 随代码块持续性(代码块运行时被定义,代码块运行完被销毁) 普通的局部变量,函数的形参 随进程持续性(进程目前就理解为程序,程序开始运行时被定义,程序运行完被销毁) 全局变量,被关键字 static修饰的局部变量 随内核持续性 (程序运行完之后都不会自动释放这块内存,需要程序员手动释放, 如果忘记了,就会一直占用,直到重启内核/操作系统),后面学
6,一维数组作为函数的参数
(学指针的时候会再次讲解)
一般来说需要传两个参数:
int a[],int n
a :数组的首地址
n :数组元素的个数
比如: 写一个函数求一个一维数组(元素是int类型)的元素之和
int array_sum(int a[],int n ) { int sum = 0; int i; for(i=0;i<n;i++) { sum += a[i]; } return sum; } int main() { int a[8] = {12,2,3,45,234,230,56,87}; array_sum(a,8); }
作业: 写一个函数,对一个整型数组进行冒泡排序 写一个函数,对一个整型数组进行选择排序 写一个函数,对一个整型数组进行插入排序 写一个函数,用来判断2048游戏是否结束 用一个数组表示游戏当前的状态 int game[4][4] = { .... };
用一个数组表示游戏状态,值为-1表示该位置是地雷,值为0表示该位置周围没有地雷, 值为1表示该位置周围有1个地雷...... 写一个函数,初始化扫雷游戏(为每个位置赋值),地雷的位置你自己固定 int a[10][10] ={0}; a[2][3] = -1; a[2][7] = -1; a[4][5] = -1; ...... 怎么生成随机数: 先生成随机数种子 srand(time(NULL)); 利用rand函数生成随机数 返回值是一个任意整数 int i = rand();//范围很大 把随机数的范围限制在 [0,9] int i = rand()%10; 把随机数的范围限制在 [5,9] int i = rand()%5+5;
7,递归
递归是一种特殊的函数调用形式,在函数内直接或间接调用自己
void func1(int i) { if(i<3) { i++; func1(i); } printf("i=%d,func()\n",i); } void test1() { //func(); int i = 0; func1(i); } 依次打印 3,3,2,1 具体调用过程见图。 例子: 写一个函数,用递归的方法求一个正整数的阶乘 数学函数,用f(n)代表n的阶乘 f(n) = 1 n=0 f(n) = f(n-1) * n n>0 -> c语言中也可以写一个 f函数,用来求阶乘 -> f(n)就是求n的阶乘 -> f(n-1) 就是求 n-1的阶乘 long long f(int n) { //..... long long x;//保存n的阶乘 if(n==0) { x = 1; } else { //n>0,n的阶乘等于n-1的阶乘 乘以 n x = f(n-1) * n; } return x; }
练习: 写一个函数,用递归的方法求斐波拉契数列的第n项 凡是用递归解决问题,技巧是先把这个问题分解为若干个步骤,并且保证 这些步骤里面要么是比较简单,要么是这个问题的子问题。 什么叫做子问题? 一样的问题,并且规模在缩小 比如说前面的求阶乘 求n阶乘这个问题,可以分为:(正确) (1) 先求n-1的阶乘 -> 求的阶乘的子问题 (2) 把第一步的结果 乘以 n ->简单 求n阶乘这个问题,可以分为:(错误,问题规模越来越大) (1) 先求n+1的阶乘 (2) 把第一步的结果 除以 n+1
1 1 2 3 5 8 13 .... 求斐波拉契数列第n项 前2项都是1,不需要分步骤,直接求 后面每一项都等于前两项之和,这个问题可以分为: (1) 求斐波拉契数列第n-1项的值 (2) 求斐波拉契数列第n-2项的值 (3) 把前两个步骤的结果相加
int fibolacci(int n) { if(n==1 || n==2) return 1; else { int x = fibolacci(n-1);//(1) 求斐波拉契数列第n-1项的值 int y = fibolacci(n-2);//(2) 求斐波拉契数列第n-2项的值 return x+y;//(3) 把前两个步骤的结果相加 } }
如果遇到一个问题,怎么知道是否可以用递归解决? 想方设法把这个问题分解为若干个步骤,并且这些个步骤要么是比较简单,要么是这个问题的子问题。 并且当这个问题规模足够小的时候,问题的结果是显而易见的
练习: 写一个函数,用递归实现判断一个数组是否递增 问题规模足够小的时候,问题的结果是显而易见的 -》 当数组只有一个元素时,该数组肯定是递增的 数组元素个数>1 时:分为几个步骤? (1) 前n-1个元素是递增的 (2) 最后一个元素 > 倒数第二个元素 练习: 写一个函数,用递归实现逆向输出一个整数的各位 12345 输出 5 4 3 2 1 练习:汉诺塔问题 汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。 大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着n片黄金圆盘。 大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。 并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。 A,B,C三个柱子,n个圆盘最开始时在A柱子上,现在需要你把他们全部移到C柱子上 写一个函数,打印出所有的移动步骤 例如: 'A' -> 'B' 表示把 'A'柱子上的最上面的圆盘移动到'B' A: 源 C:目的 B:中间 n个圆盘 分为 n-1 和 1 (1) 把 n-1个圆盘从 A移到B,借助中间盘子C (2) 把剩下的一个圆盘从 A移到C (3) 把 n-1个圆盘从 B移到C,借助中间盘子A /* 功能是打印汉诺塔问题的移动步骤 参数: n 圆盘的数目 A 源柱子 B 中间柱子 C 目的柱子 */ void hanoi(int n,char A,char B,char C) { if(n==1)//只有一个圆盘,直接移动 { printf("%c->%c\n",A,C); } else { //(1) 把 n-1个圆盘从 A移到B,借助中间盘子C hanoi(n-1,A,C,B); //(2) 把剩下的一个圆盘从 A移到C printf("%c->%c\n",A,C); //(3)把 n-1个圆盘从 B移到C,借助中间盘子A hanoi(n-1,B,A,C); } }
在上面的基础上,计算一下移动次数
int i=0; while(i++<7) { if(p[i]%2) { j+=p[i]; } } 10 14 16 17 18 20
1 4 3 2 8 6 5 7 3 7 2 5 4 8 6 1
1 4 2 1 3 6 3 2 4 7 5 5 8 8 6 7
"a0\0a0\0"
'a' '0' '\0' 'a' '0' '\0' '\0'
作业: 1, 写一个函数,用递归实现正向输出一个整数的各位 12345 输出 1 2 3 4 5
2,求池塘数 由于近日阴雨连天,约翰的农场中中积水汇聚成一个个不同的池塘,农场可以用10*10 的正方形来表示。 农场中的每个格子可以用1或者是0来分别代表积水或者土地,约翰想知道他的农场中有多少池塘。 池塘的定义:一片相互连通的积水。任何一个正方形格子被认为和与它相邻的8个格子相连。 给你约翰农场的航拍图,确定有多少池塘
1 0 1 0 0 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 ...