C语言基础九:函数、实参形参

函数

函数的概述

  • 函数:实现一定功能的,独立的代码模块。我们的函数一定是先定义,后使用

  • 使用函数的优势:

    ①我们可以通过函数提供功能给别人使用。当然我们也可以使用别人提供的函数,减少代码量。

    ②借助函数可以减少重复性的代码。

    ③实现结构化(模块化)程序设计思想。

    关于结构化设计思想:将大型的任务功能划分为相互独立的小型的任务模块来设计。

  • 函数是C语言程序的基本组成单元:

    C语言程序是由一个(必然是main函数)或多个函数组成。

函数的分类
  • 从函数实现的角度:

    • 库函数:C语言标准库实现的并提供使用的函数,比如常见有scanf0,printf0,strlen0…
    • 自定义函数:需要程序员自行实现,开发中大部分函数都是自定义的。
  • 从函数形式的角度:

    • 无参函数:函数调用时,无需传参,可有可无返回值。
    • 有参函数:函数调用时,需要参数传递数据,经常需要配套返回值使用。

    相关概念:

    • 主调函数:主动去调用其他函数的函数(如:main函数)

    • 被调函数:被其他函数调用的函数。

      int main()
      {
      	//被调函数,需要注意的是,有些函数只能作为被调函数,如库函数
      }
      

      很多时候,尤其是对于自定义函数来说,一个函数既可能是主调函数,还可能同时是被调函数。

      int fun_b()
      {
      	printf("函数B\n");
      }
      int fun_a()
      {
      	printf("函数A\n");fun_b();
      }
      int main()
      {
      	fun_a();
      }
      

      以上案例中,fun_a0对于fun_b0来说是主调函数,同时对于main0来说,它又是被调函数。

函数的定义

语法:
[返回类型] 函数名([形参类型]//函数首部|函数头
{
	函数体语句;(函数体)     //函数体,整个{}包裹的内容都属于函数体
}

函数首部:

  • 返回类型:函数返回值的类型

  • 函数名:函数的名称,遵循标识符命名(使用英文字母、数字、_,不能以数字开头,建议小写+下划线命名法)

  • 形参列表:用于接收主调函数传递的数据,如果有多个参数,使用 分隔,且每一个形参都需要指定类型。若省略类型标识符,默认返回int

    注意:

    • 函数类型标识符,就是返回值的类型,两个类型可以不同,但必须能够进行转换。

    • 在C语言中还定义无类型(即void类型)的函数,这种函数不返回函数值,只是完成某种功能。

    • 如果省略函数的类型标识符,则默认是int型。

    • 函数中返回语句的形式为return(表达式)return 表达式,其作用是将表达式的值作为函数值返回给主调函数,其中表达式的类型应与函数类型一致(也就是说函数类型和返回值类型之间是支持转换的)

    • 如果参数列表中有多个形式,则它们之间用 分隔。

    • 如果形参列表中有多个形参,即使类型相同,在形参列表中也只能逐个进行说明。fun1(int a ,int b)

    • 函数

形参和实参

形参(形式参数):

函数定义时指定的参数,形参是用来接收数据的,函数定义时,系统不会为形参申请内存,只有当函数调用是,系统才会为形参申请内存,用于存储实际参数,并且当函数返回,系统会自动回收为形参申请的内存资源。(本质上所有函数都有return,只不过当我们的函数返回类型是void时,return为隐式的)

实参(实际参数):
  • 函数调用时主调函数传递的数据参数(常量,符号常量、变量、表达式,只要有确定值),实参是传递的数据。

  • 实参和形参必须类型相同。如果不同时,按赋值规定自动进行类型转换。

  • 在C语言中,参数传递必须遵循 单向值传递 ,实参只是将自身的值传递给形参,而不是实参本身。形参的值的改变不会影响实参。

  • 实参与形参在内存中占据不同的内存空间(尽可能实参和形参名称是一样的)

    double fun(double a,double b)
    {
        return a+b;
    }
    int main()
    {
        int x
    }
    

案例:

#include <stdio.h>
/**
*需求:函数案例-输入两个整数,要求用一个函数求出最大值,并在主调函数输出次数
*/
int max(int x,int y)
{
    return x>y?x:y;
}
int main(int argc,char *argv[])
{
    int a,b,c;
    printf("请输入两个整数:\n");
    scanf("%d,%d",&a,&b);
    //调用函数最大值
	c = max(a,b);
    printf("%d,%d中的最大值是%d\n",a,b,c);
    return 0;
}
函数的返回值
  • 若不需要返回值,函数中可以没有return语句。
  • 一个函数可以有多个return语句,但任一时刻只有一个return语句被执行
  • 被调函数返回给主调函数的结果数据(可以是变量、常量、表达式…,只要是确定值就可以)
  • 返回值类型一般情况下要和函数中return语句返回的数据类型保持一致,如果不一致,以函数定义时指定的返回类型为标准。也就是返回值类型和实际返回值可以存在自动类型或者强制类型转换的关系。
案例
#include <stdio.h>
/**
*需求:函数案例-输入两个整数,要求用一个函数求出最大值,并在主调函数输出次数
*/
int max(int x,int y)
{
    if(x>y)
    return x;//一旦return不会执行以下的语句
    return y;
}
int main(int argc,char *argv[])
{
    int a,b,c;
    printf("请输入两个整数:\n");
    scanf("%d,%d",&a,&b);
    //调用函数最大值
	c = max(a,b);
    printf("%d,%d中的最大值是%d\n",a,b,c);
    return 0;
}
案例2:
#include <stdio.h>
/**
*需求:函数案例-输入两个整数,要求用一个函数求出最大值,并在主调函数输出次数
*/
double max(int x,int y)
{
    return x>y?x:y;
}
int main(int argc,char *argv[])
{
    int a,b,c;
    printf("请输入两个整数:\n");
    scanf("%d,%d",&a,&b);
    //调用函数最大值
	c = (int)max(a,b);//需要强转
    printf("%d,%d中的最大值是%d\n",a,b,c);
    return 0;
}
案例3:
#include <stdio.h>
/**
*需求:函数案例-输入两个整数,要求用一个函数求出最大值,并在主调函数输出次数
*/
int max(int x,int y)
{
    double z;
    z=x>y?x:y;
    return(int)z;//将double类型转换为int类型,此时会强制转换,如果为了增加代码的可读性,我们可以手动强转。
}
int main(int argc,char *argv[])
{
    int a,b,c;
    printf("请输入两个整数:\n");
    scanf("%d,%d",&a,&b);
    //调用函数最大值
	c = max(a,b);//不需要再转
    printf("%d,%d中的最大值是%d\n",a,b,c);
    return 0;
}
函数的调用

调用方式

①函数语句: test();int res = max(2,4);

②函数表达式: 4 + max(2,4);

③函数参数: printf("%d",max(2,4));

在一个函数中调用另一个函数具备以下条件

1被调用的函数必须是已经定义的函数。

2)若使用函数库,应在本文件开头用#include包含。

3)若使用自定义函数,自定以函数又在主调函数的后面,则应在主调函数中对被调用函数进行声明。声明的作用是把函数名、函数参数的个数和类型等信息通知编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的合法性。

函数声明

函数调用时,往往要遵循 先定义后使用,但如果我们对函数的调用操作出现在函数的定义之前,则需要对函数进行声名。

完整的函数使用分为三部分:

  • 函数声明
  • 函数定义
  • 函数调用

函数声明的作用:

  • 是把函数名、函数参数的个数和返回值等信息通知给编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查调用的合法性。

错误演示:

// 主调函数
int main()
{
	int c= add(12,13);//此时编译会报编译错误,因为函数没有经过声明,编译器无法正确识别函数
    printf("%d\n",c);
}
// 被调函数
int add(int x,int y)
{
    return x+y;
}
正确演示:
// 被调函数
int add(int x,int y)
{
    return x+y;
}
// 主调函数
int main()
{
	int c= add(12,13);//此时编译不会报编译错误,
    printf("%d\n",c);
}

正确演示(被调函数和主调函数不分先后,需要增加被调函数的函数声明)

//函数声明(一般放在整个c文件的顶部,在#xxx 的下面,或者将其单独提取到一个,h文件,但是需要跟当前c文件进行关联)
int add(int,int);
//主调函数
int main()
{
	int c = add(12,1);//此时编译通过
    printf("%d\n",c);
// 被调函数
}
    int add(int x,int y)
	return x+ y;
}

函数声明的方式:

  • 函数首部后加分号;

    void fun(int a);
    
  • 函数首部后加上分号,可省略形参名,但不能省略参数类型。

    void funa( )
    {
    	void funb()
    	{
    		...	
    	}
    }
    //C语言不支持嵌套定义
    
    • 嵌套调用:在被调函数内又主动去调用其他函数,这样的函数调用方式,称之为嵌套调用。

      举例:

      funa()
      {
      
      }
      funb()
      {
      	funa();
      }
      func()
      {
      	funb():
      }
      

函数的递归调用

  • 递归调用的含义:在一个函数中,直接或者间接调用了函数本身,就称之为函数的递归调用。

    //直接调用
    a() = a();
    
    //间接调用
    a() = b() = a();
    a() = b() =...=a();
    
  • 递归调用的本质:

    是一种循环结构,不同于之前所学的while、do…while、for这样的循环结构,这些循环结构是借助循环变量,而递归是利用函数自身实现循环结构,如果不加以控制,很容易产生死循环。

  • 递归调用的注意事项:

    1.递归调用必须要有出口,一定要终止递归(否则就会产生死循环)。

    2.对终止条件的判断一定要放在函数递归之前(先判断,再执行)
    3.进行函数的递归调用。
    4.函数递归的同时一定要将函数调用向出口逼近。

    int age(int n)
    {
        //存放函数的返回值,也就是年龄
        int c;
        if(n==1)
        {
            c=10;
        }
        else if(n>1)
        {
            c = age(n-1)+2;
        }
        return c;
    }
    
    int main()
    {
    	printf(%d\n,age(5));
    	return 0;
    }
    

案例2:用递归函数求阶乘

long fac(int n)
{
	//定义一个变量f,用来接收乘积
    int f=1;
    if(n<1)
    {
    	printf("n的范围不能是0以下的数\n");
        return -1;
    }
    else if(n==1||n==0)
    {
        f =1;
    }
    else
    {
    	f=fac(n-1)*n;
    }
    return f;
}
int main()
{
    int n;
    printf("请输入一个整数:\n");
    scanf("%d",&n);
	printf("%d的阶层结果是%ld\n",fac(n));
	return 0;
}

数组做函数参数

注意:
当用数组做函数实际参数时,则形参应该也要用数组/指针变量来接收,但请注意,此次并不代表传递了数组中所有的元素数据,而是传递了第一个元素的内存地址(数组首地址),形参接收这个地址后,则形参和实参就代表了同一块内存空间,则形参的数据修改会改变实参的。这种数据传递方式我们可以称之为“引用传递"。
如果用数组做函数形式参数,那么我们提供另一个形参表示数组的元素个数。原因是数组形参代表的仅仅是实际数组的首地址。也就是说形参只获取到了实际数组元素的开始,并未获取元素的结束。所以提供另一个形参表示数组的元素个数,可以防止在被调函数对实际数组元素访问的越界。
但有一个例外,如果是用字符数组做形参,且实际数组中存放的是字符串数据(形参是字符数组,实参是字符串)。则不用表示数组元素的个数的形参,原因是字符串本身会自动结束符\0。

变量的作用域
引入问题

我们在函数设计的过程中,经常要考虑对于参数的设计,换句话说,我们需要考虑函数需要几个参数,需要什么类型的参数,但我们并没有考虑函数是否需要提供参数,如果说函数可以访问到已定义的数据,则就不需要提供函数形参,那么到底是否提供形参取决于变量的作用域。

变量作用域

概念:变量的作用范围,也就是说变量在什么范围有效

变量的分类

根据变量的作用域不同,变量可以分为全局变量局部变量

名称解释作用域
全局变量定义在函数之外的变量,也称为外部变量或全局变量从全局变量定义处到本源文件的结束
局部变量形式参数函数作用域
函数定义的变量函数作用域
复合语句中定义的变量块作用域
for循环表达式1定义的变量块作用域

建议在全局变量定义时初始化,如果不初始化,系统会将全局变量初始化为 0(0|\0|0.0)

  • 使用全局变量的优缺点:

    优点:

    1.利用全局变量可以实现一个函数对外输出的多个结果数据。
    2.利用全局变量可以减少函数形参的个数,从而降低内存消耗,

    int a(int a){}
    int b(int b){a(b)}
    int c(int c){b(c)}
    int main(){c(5)} 16
    

    缺点:

    1.全局变量在程序的整个运行期间,始终占据内存空间,会引起资源消耗。
    2.过多的全局变量会引起程序的混乱,操作程序结果错误。

    3.降低程序的通用性,特别是当我们进行函数移植时,不仅仅要移植函数,还要考虑全局变量。

    4.违反了“高内聚,低耦合"的程序设计原则。

    总结:我们发现弊大于利,建议尽量减少对全局变量的使用,函数之间要产生联系,仅通过实参+形参的方式产生联系。

    变量的生命周期
    • 概念:变量在程序运行中的存在时间。
    • 根据变量存在的时间不同,变量可分为静态存储方式和动态存储方式。

    在这里插入图片描述

    • 变量的存储类型

      变星的完整定义格式:[存储类型]数据类型 变量列表;
      

      存储类型
      auto

      auto存储类型只能修饰局部变量,被auto修饰的局部变量是存储在动态存储区的。auto也是局部变量默认的存储类型。

      int a=10;等价于 auto int a = 10

      static

      修饰局部变量:局部变量会被存储在静态存储区。局部变量的生命周期被延长,但是作用域不发生改变。
      修饰全局变量:全局变量的生命周期不变,但作用域被衰减。一般限制全局变量只能在本文件内。

      #include "demo01.h"
      // 全局变是
      static int fun a = 10;
      int fun1()
      

extern

外部存储类型:只能修饰全局变量,此全局变量可以被其他文件访问。相当于扩展了全局变量的作用域。

exter修饰外部变量,往往是外部变量进行声明,声明该变量是在外部文件中定义的;不是变量定义。

#include "demo01.h"// 声明外部文件的变量
extern int fun_a;// 声明外部文件的函数extern int fun1()
main()
{
	fun_a = 20;
	fun1();
}

register

寄存器存储类型:只能修饰局部变量,用register修饰的局部变量会直接存储到CPU的寄存器中,往往将循环变量设置为寄存器存储类型。很小不建议新手使用

register int a = 10;
面试题

static关键字的作用
1.static修饰局部变量,延长其生命周期,但不影响局部变量的作用域。

2.static修饰全局变量,不影响全局变量的生命周期,会限制全局变量的作用域仅限本文件内使用(私有化);

3.static修饰函数:此函数就称为内部兩数,仅限本文件内调用(私有化)。 static int funa(){…}

内部函数和外部函数
  • 内部函数:使用static修饰的函数,称作内部函数,内部函数只能在当前文件中调用。

  • 外部函数:使用extern修饰的函数,称作外部函数,extern是默认的,可以不写(区分环)也就是说本质上我们

  • 缩写的函数基本上都是外部函数,建议外部函数在被其他文件调用的时候,在其他文件中声明的时候,加上exterm关键字,主要是提高代码的可读性。

    • 一个完整C程序中的所有函数可以放在一个文件中,也可以放在多个文件中。

      //demo01.c -->int main(){printf();}
      stdio.c -->printf(char *p){};
      
案例

案例1:

#include <stdio.h>
#define PI 3.14
/*
*需求:函数案例-计算1到n之间自然数的阶乘
*建议:我们设计的函数,尽量让被调函数改动小,主调函数改动大
*/
int fun1(int n)
{
	int k,s;//k:循环变量,s:阶乘的结果
	for(k=1;k<=n;k++)s*=k;
     return s;
}

int main()
{
    //计算1-5的阶乘
    printf("1-5的阶乘结果是:%d\n",fun1(5));
    //计算1-6的阶乘
    printf("1-6的阶乘结果是:%d\n",fun1(6));
    
    return 0;
}
案例二:
#include <stdio.h>
#define PI 3.14
/*
* 需求:函数案例-定义一个函数,实现圆的面积的计算
*/

double circleArea(double r)
{
    return PI*r*r;  
}
int main(int argc,char *argv[])
{
	//需求:计算一个圆柱体台面两个面的面积之和
	//定义了两个半径,两个面积
	double rl r2,areal area2;
    printf("请输入两个圆的地址\n");
    scanf("%lf,%lf",&r1,&r2);
    area1 = circleArea(r1);
    area2 = circleArea(r2);
    printf("1-5的阶层结果是:%f",area1+area2);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值