1,函数
function 功能模块
在c语言中,函数是完成某个特定功能的指令集合/代码块集合,函数的指令封装在{}内部
使用函数的优点:1.实现代码的重复使用。 2. 步骤逻辑清晰,增强可读性,便于维护和扩展
在设计函数式,需要考虑哪些问题??
这个函数要实现一个什么功能? 明确目标
完成这个功能,需要哪些必要的资源?? 输入参数
怎么实现这个功能? 思路,步骤 .....
完成结果? 返回值
2,定义函数语法
返回值类型 函数名(参数列表)
{
完成这个功能的指令集合
}
函数名:表示这个function的一个名字,把这个名字和这个特定的功能函数关联起来;要符合c语言标识符的规定,最好是顾名思义
如 sum 便可以看出来是求和;
参数列表:
具体语法格式:参数1类型 参数1名,参数2类型 参数2名…
如果不需要参数则空着,如果有多个参数用逗号隔开
需要两个参数,都是int类型
int x,int y
完成这个功能的指令集合:
具体的代码
int z;
z=x+y;
如果返回值类型不是 void ,那么必须有 return 语句
return "结果";
返回值类型:
这个功能函数执行完之后有一个“结果”,这个结果是什么类型,返回值就应该是什么类型
“求和”的“结果”是 和
当然有一些特殊情况执行完函数之后没有“结果”,返回值类型为 void
例子:
写一个函数,求两个整数之和
int sum(int x,int y)
{
int z;
z=x+y;
return z;
}
3,函数调用
任何一个程序有且仅有一个 main函数
main函数是程序的入口(最开始执行的函数)
main函数之外的函数被调用的时候才会执行
调用函数的语法:
函数名(参数列表);
参数列表是根据函数定义时的参数列表决定的,定义时如果没有参数,调用时也不需要参数
定义时如果有一个参数,调用时也需要一个参数,定义时如果有2个参数,调用时也需要2个参数...
为了区分定义时的参数列表和调用时的参数列表,有两个专用名词:形式参数(形参)和实际参数(实参)
函数定义时的参数叫做:形式参数(形参)
函数调用时的参数叫做:实际参数(实参),实参前面不需要写类型
如:
sum(10,20);
调用函数这个表达式被称之为函数调用表达式,之前说过任何表达式都有一个值,这个函数调用表达式也不
例外,他也有一个值,他的值就是函数中 return 后面的那个值,如果函数中忘记写 return 语句,
这个函数调用表达式的值是不确定的
int s = sum(10,20);//把函数调用表达式的值赋值给s
练习:
写一个函数求两个整数的最小值,b
分析下面sum函数的两种形式,你认为那种好一点:
int sum(int x,int y)
{
int z;
z=x+y;
return z;
}
void sum()
{
int x,y;
scanf("%d%d",&x,&y);
int s = x+y;
printf("s=%d\n",s);
}
上面的写法好一些,下面的sum函数的利用率太低,因为它只适用于从键盘获取数据并且把结果输出到终端
,如果我需要从网络上,本地磁盘文件,数据库,扫描仪....获取数据,就没办法使用第二个sum。
而第一个函数数据是通过参数传入,结果通过 return 返回,这样做非常灵活多变,利用率高
4,函数声明
我们在main函数中调用 sum函数,而sum函数写在main函数后面,此时编译时会报警告:
warning: implicit declaration of function ‘sum’ [-Wimplicit-function-declaration]
意思是说:编译器不认识 sum,不知道这是什么东西。
原因是编译器在编译程序时是按照从前往后顺序进行处理的
怎么解决?在调用之前需要进行函数声明(作用就是告诉编译器这个sum是一个函数)
函数声明语法:
返回值类型 函数名(形参列表);
比如:
int sum(int x,int y);
注意:函数声明时,函数形参的名字可以省略
->
int sum(int ,int );
作业:
写一个函数,判断一个正整数是否是质数,并且在main函数里面调用。
函数:
提高代码可重用性,降低代码冗余
便于维护,便于扩展
提高代码可读性
....
定义:
返回值类型 函数名(形参列表)
{
具体的代码
}
int func()
{
....
if(...)
{
return xx;
}
else if(...)
{
return yy;
}
...
}
int x = func();
函数调用
函数名(实参列表);
实参列表不需要写类型
函数调用表达式 有一个值,这个值就是 return 后面的那个值,如果没有 函数中没有 return ,该表达式
的值不确定
函数声明:
目的是告诉编译器 函数名这个单词是一个函数
返回值类型 函数名(形参列表);
5,函数调用过程
主调函数:调用别的函数的函数
被调函数:被别的函数调用的函数
除了main函数之外的函数有可能是主调函数也有可能是被调函数…
主调函数 A 被调函数 int sum(int x,int y)
A调用sum的过程步骤如下:
1,传递参数
sum(10,20);//把10传递给形参x,把20传递给形参y
int a=10;
int b=20;
sum(a,b);//把a的值传递给形参x,把b的值传递给形参y
传递参数的这个步骤会为形参分配内存空间并初始化
拿实参初始化形参
2,进入到被调函数内部执行
直到遇到 return 语句或者所有指令执行完毕
3,如果有return 语句,就进行第三步
把“结果”返回 :return后面的"结果"就是函数调用表达式的值
int s;
s = sum(10,20);
分析以下函数
void swap(int x,int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
int main()
{
int x=10;
int y=20;
swap(x,y);
printf("x=%d,y=%d\n",x,y);//
return 0;
}
形不改实
形参和实参是两个不同的变量,改变形参和实参没有任何关系,swap函数中交换的是形参x,y的值,和实参x,y没有关系
练习:
1,写一个函数,求一个int类型数组所有元素之和
记住:
数组作为函数的参数时,形参应该这么写
有两个形参,第一个是 类型*数组名 ,int 数组元素个数
int array_sum(int *a,int n)//与之等价的写法: int array_sum(int a[],int n)
{
//可以直接使用下标法 a[i] 访问数组的每个元素
int i;
int s = 0;
for(i=0;i<n;i++)
{
s+=a[i];
}
return s;
}
int main()
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int s;
s = array_sum(a,10);
return 0;
}
2,写一个函数,求n的阶乘
long long int jiecheng(int n)
{
int s =1;
int i;
for(i=1;i<=n;i++)
{
s*=i;
}
return s;
}
n的阶乘 = 1*2*3*.....*(n-1)*n
n的阶乘 = n-1的阶乘 * n
递归代码引入:
long long int jiecheng(int n)
{
if(n==1)
return 1;
if(n>1)
return jiecheng(n-1) * n;
}
6,递归
在函数内部调用函数本身的形式称之为递归,这个函数我们称之为递归函数
需要防止无限递归:永远都在调用自己,无穷无尽
如:
void func()
{
printf("func\n");
func();
}
所以在递归时,需要注意递归的结束条件:当某种情况下不再调用自己 -》不会无限递归
long long int jiecheng(int n)
{
if(n==1)
return 1;
if(n>1)
return jiecheng(n-1) * n;
}
分析递归的调用过程,见图:
写一个递归函数,求斐波拉契第n项的值
什么问题可以用递归来解决?
当一个问题可以分为若干个小问题/步骤,并且其中一个或多个小问题/步骤和这个问题本身是一样的
这种情况就可以考虑用递归来解决
比如:
求n的阶乘
1,求n-1的阶乘
2,第一步的结果*n
求斐波拉契数列的第n项
1,求 斐波拉契数列的第n-1项
2,求 斐波拉契数列的第n-2项
3,把第一步和第二步结果相加
除了这个特点之外还需要有递归结束条件:就是说当问题规模缩小到一定程度时,结果是明显的,不需要再分解了
汉诺塔问题
有ABC三根圆柱,A圆柱上有若干个圆盘,大小各不一样,大的在下,小的在上。 想办法把A圆柱上的所有圆盘全部移动到C圆柱上,可以利用B圆柱,要求:每次只能移动一个圆盘,并且时刻要保持大的在下,小的在上。
例如:
1个圆盘
A -> C
2个圆盘
A -> B
A -> C
B -> C
3个圆盘
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
.....
写一个函数,打印出n个圆盘的移动步骤
1,把n个圆盘从 A移动到B
2,把n个圆盘从 B移动到C
错误,因为问题规模没有缩小,毫无意义
1,把n-1个圆盘 从A移动到B,中间可以借助C
2,把最后1个从A移动到C
3,把n-1个圆盘 从B移动到C,中间可以借助A
1,3又是一个汉诺塔问题,并且问题规模得到了缩小(n -> n-1),当规模缩小到一定程度时问题的答案
是显而易见的
/*
解决汉诺塔问题
打印出n个圆盘的从A圆柱移动到C圆柱的步骤(中间可以借助B圆柱)
参数:
n 表示有n个圆盘
A 起点圆柱
B 中转站
C 目的圆柱
*/
void hanoi(int n,char A,char B,char C)
{
if(n==1)
{
printf("%c->%c\n",A,C);
return ;//仅仅代表结束函数
}
//1,把n-1个圆盘 从A移动到B,中间可以借助C
hanoi(n-1,A,C,B);
//2,把最后1个从A移动到C
printf("%c->%c\n",A,C);
//3,把n-1个圆盘 从B移动到C,中间可以借助A
hanoi(n-1,B,A,C);
}
int main()
{
hanoi(10,'x','y','z');
}
7,生存期
什么是生存期?
是一个时间跨度,从“出生”到“消亡”
“出生” 操作系统为其分配内存空间,拥有对这块内存的所有权
“消亡” 内存空间被回收了,不拥有对这块内存的所有权了
目前来说需要了解两种生存期:
随代码块持续性
这个代码块运行,他就存在,运行完就消亡
普通的局部变量
if(...)
{
int a;
}
for()
{
int b;
}
while(1)
{
int c;
}
void func()
{
int d;
}
{}里面定义的变量和形参都是局部变量
随进程/程序持续性
程序运行时分配空间,程序运行完才会消亡
全局变量 和 static修饰的局部变量
void func1()
{
static int x = 10;
//随程序/进程持续性,程序运行时就会分配空间,程序运行完毕才会消亡
//这条语句在程序执行时就会运行,只会运行一次
x++;
printf("x=%d\n",x);
}
8,作用域
什么是作用域
就是能够起作用的区域
变量和函数都有作用域
变量从作用域来分,分为两种
(1)局部变量
定义在 {} 内部和形参都是局部变量
局部变量仅在{}内部起作用
int a = 10;
void func()
{
int a = 40;
}
int main()
{
int a = 20;
if(1)
{
int a = 30;
printf("a=%d\n",a);//30
}
//int a = 50;
printf("a=%d\n",a);//20
}
同名不要紧
只要域不同
具体是哪个
往上就近找
(2)全局变量
2.1 在整个工程所有.c文件中都能访问的全局变量
1.c
int data = 100;
2.c
如果希望在 2.c这个文件中访问 定义在1.c中的全局变量data
在2.c中进行外部声明:
extern int data;//外部声明语句,告诉编译器 data是一个全局变量,定义在工程的其他.c文件中
2.2 仅在当前 .c文件中能够访问的全局变量
如果1.c中的全局变量不希望被工程的其他.c文件访问,只需要在前面加上 static即可
函数的作用域也分为两种
整个工程所有.c文件都可以调用
普通写法即可
只能在当前 .c文件调用
在前面加上 static关键字修饰即可
总结 static 的作用:
修饰局部变量,改变局部变量的生存期(由随代码块持续性变为随程序持续性)
修饰全局变量和函数,改变他们的作用域(由整个工程作用域变为当前文件作用域)
程序在运行的时候,操作系统会给这个程序分配内存空间
这个内存空间是分段的
.text :只读的 文本段
你的代码指令就放在这里
.rodata :只读存储段
常量
.data :全局数据段,已经初始化好了的全局变量
和static变量
程序在,它就在,程序死,它就释放
可读可写
.bss :全局数据段,没有初始化好了的全局变量
和static变量
程序在,它就在,程序死,它就释放
可读可写
栈空间 :可读可写 存放局部变量
局部执行完毕,自动回收
堆空间 :可读可写 存放动态分配出来的内存
操作系统会拿内核空间分配给它
分配出来就不会释放了,需要手动释放
水洼问题!!!!!
假设你有一个10*10的二维数组当做你们家的后花园
现在在下雨,后花园上面就有积水
积水可能是一片一片的,注意:一片只能算一个水洼
求出里面有多少个水洼
1 1 0 0 0 0 0 1 0 0
0 0 0 1 0 0 1 0 0 0
0 1 0 0 0 0 1 0 1 1
0 0 0 1 1 1 0 1 0 1
1 1 0 1 0 1 1 0 0 0
0 1 0 1 0 1 0 0 0 0
1 1 0 0 1 0 0 0 1 1
1 1 0 0 1 1 0 1 1 0
0 0 0 0 1 0 0 0 1 0
0 0 1 0 1 1 1 1 0 1
做法:
清除水洼?
当我遇到一个水洼之后,我就把这个水洼给填了
当我遇到1了我就确定有一个水洼了
我马上将这个1给填了(将1变成0)
以相同的方式将它的8个方向给填了
这8个方向首要条件:
1 这8个方向得存在
2 它得是坑你才去填
做法:
碰到1了之后,水洼的个数+1
然后将这个水洼清除
然后找下一个
//自动出水洼
需求利用随机数:
计算机里面是没有随机数的,只有伪随机,在计算机里面有很长一串数据,这些数据是人为抛硬币弄出来的,现在我们需求用算法将这个数据弄出来, 如果算法没有发生变化,那么我每次弄出来的随机数都是一样的, 这样随机数就谈不上了,因此我们利用计算机里面一个会变的东西 --- 时间。
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define N 10
int puddle[N][N] = {0};
//生成这个水洼
void CreatePuddle(void)
{
int i,j;
for(i = 0;i < N;i++)
{
for(j = 0;j < N;j++)
{
puddle[i][j] = rand() % 3 ? 0 : 1;//0的概率是1的两倍
}
}
}
//打印水洼
void PrintPuddle(void)
{
int i,j;
for(i = 0;i < N;i++)
{
for(j = 0;j < N;j++)
{
printf("%d ",puddle[i][j]);
}
printf("\n");
}
}
//清除水洼
void Clear(int i,int j)
{
//这个地方必须要存在
//这个地方必须是水洼才需要清除
if(i < 0 || i >= N || j < 0 || j >= N || puddle[i][j] == 0)
return;
//清除水洼
puddle[i][j] = 0;
//以相同的规则去清除剩余的8个方向
Clear(i,j + 1);
Clear(i,j - 1);
Clear(i + 1,j + 1);
Clear(i + 1,j);
Clear(i + 1,j - 1);
Clear(i - 1,j + 1);
Clear(i - 1,j);
Clear(i - 1,j - 1);
}
//找水洼
int FindPuddle(void)
{
int i,j;
int num = 0;
for(i = 0;i < N;i++)
{
for(j = 0;j < N;j++)
{
//遇到1了就是水洼
if(puddle[i][j])
{
num++;
//为了避免下一次找的时候又找到这个水洼
//我需要将这个水洼整体清除掉
Clear(i,j);
}
}
}
return num;
}
int main()
{
//利用系统的时间产生随机下标
srand((int)time(NULL));
CreatePuddle();
PrintPuddle();
printf("水洼数为: %d 个\n",FindPuddle());
PrintPuddle();
return 0;
}
关于随机数进一步理解:
NAME
rand, rand_r, srand - pseudo-random number generator
SYNOPSIS
#include <stdlib.h>
void srand(unsigned int seed);
利用随机种子弄一个随机的下标
seed:随机种子
一般我们要利用系统时间
NAME
time - get time in seconds
SYNOPSIS
#include <time.h>
time_t time(time_t *tloc);
tloc:保存现在的系统时间
一般我们给NULL ,获取现在的系统时间,通过返回值返回回来
srand((unsigned int)time(NULL));
int rand(void);
//从随机下标开始拿取随机数
//每拿一个下标自动往后面走一个
现在我需要 0 ~ 10中间的某一个随机数
即: rand() % 11;
现在我需要 20 ~ 30中间的某一个随机数
即: rand() % 11 + 20;
现在我要出0 / 1,出1的概率是0的两倍
即: rand() % 3 ? 1 : 0;
#define ::宏定义
只做替换,不做任何的运算