C语言小白养成计划六:函数

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 ::宏定义
只做替换,不做任何的运算

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值