C 语言-4 函数

4 函数

  • 函数是C源程序的基本模块,通过对函数模块的调用实现特定的功能,C 提供了丰富的库函数、还允许建立自己定义的函数。可以说C程序的全部工作都是由各式各样的函数完成的,所以也把C语言称为函数式语言

  • 从不同的角度对函数分类

    分类角度
    函数定义 库函数:由C系统提供,用户无须定义,也不必在程序中作类型说明,只需在程序前包含有该函数原型的头文件即可在程序中直接调用 用户定义函数:由用户按需要写的函数。对于用户自定义函数,不仅要在程序中定义函数本身,而 且在主调函数模块中还必须对该被调函数进行类型说明,然后才能使用
    有无返回值 有返回值函数:此类函数被调用执行完后将向调用者返回一个执行结果,称为函数返回值;由用户定义的这种要返回函数值的函数,必须在函数定义和函数说明中明确返回值的类型 无返回值函数:此类函数用于完成某项特定的处理任务,执行完成后不向调用者返回函数值,由于函数无须返回值,用户在定义此类函数时可指定它的返回为“空类型”, 空类型的说明符为"void"
    主调函数和被调函数之间数据传送 无参函数:函数定义、函数说明及函数调用中均不带参数。主调函数和被调函数之间不进行参数传 送。此类函数通常用来完成一组指定的功能,可以返回或不返回函数值 有参函数:也称为带参函数。在函数定义及函数说明时都有参数,称为形式参数(简称为形参)。在 函数调用时也必须给出参数,称为实际参数(简称为实参)。进行函数调用时,主调函数将把实参的 值传送给形参,供被调函数使用
    • 从功能角度作以下分类:

      函数类型 说明
      字符类型分类函数 用于对字符按 ASCII 码分类:字母、数字、控制字符、分隔符、大小写字母等
      转换函数 用于字符或字符串的转换;
      在字符量和各类数字量(整型,实型等)之间进行转换;
      在大、 小写之间进行转换
      目录路径函数 用于文件目录和路径操作
      诊断函数 用于内部错误检测
      图形函数 用于屏幕管理和各种图形功能
      输入输出函数 用于完成输入输出功能
      接口函数 用于与 DOS,BIOS 和硬件的接口
      字符串函数 用于字符串操作和处理
      内存管理函数 用于内存管理
      数学函数 用于数学函数计算
      日期和时间函数 用于日期,时间转换操作
      进程控制函数 用于进程管理和控制
      其它函数 用于其它各种功能
  • 在C语言中,所有的函数定义,包括主函数 main 在内,都是平行的。也就是说,在一个函数的函数体内,不能再定义另一个函数,但是函数之间允许相互调用,也允许嵌套调用。习惯上把调用者称为主调函数。函数还可以自己调用自己,称为递归调用

    • main 函数是主函数,它可以调用其它函数,而不允许被其它函数调用。因此,C程序的执行总是从 main 函数开始,完成对其它函数的调用后再返回到 main 函数,最后由 main 函数结束整个程序
    • 一个C源程序必须有,也只能有一个主函数 main

4.1 函数的定义

  • 格式:

    类型标识符 函数名([形式函数表列])
    {声明内容语句
    }
    

    类型标识符:指明了本函数的类型,函数的类型实际上是函数返回值的类型,如果不要求函数有返回值,此时函数类型符可以写为 void

    函数名:由用户定义的标识符,函数名后有一个空括号,其中可有或无参数,而括号不可少

    形参表:在,可以是各种类型的变量,各参数之间用逗号间隔,同时必须在形参表中给出形参的类型说明

    • 函数头:类型标识符和函数名一起称为函数头
    • 函数体:{} 中的内容称为函数体。在函数体中声明部分,是对函数体内部所用到的变量的类型说明
    • 形式参数:形参表中给出的参数,进行函数调用时,主调函数将赋予这些形式参数实际的值
    • 有叁和无参:如果形式函数表列有参数,称为有参函数,反之则为无参函数
  • 示例:

    int max(int a,int b)
    {
        if(a>b) return a;
        else return b;
    }
    
  • 在C程序中,一个函数的定义可以放在任意位置,既可放在主函数 main 之前,也可放在 main 之后

4.2 参数和值

  • 形参和实参

    形参 实参
    使用范围 出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用 出现在主调函数中,进入被调函数后,实参变量也不能使用
    功能 作数据传送。实现主调函数向被调函数的数据传送 作数据传送。发生函数调用时,主调函数把实参的值传送给被调函数的形参
    内存分配 在被调用时分配内存单元,在调用结束时,即刻释放所分配的内存单元
  • 特点

    1. 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,都必须具有确定的值,以便把这些值传送给形参。因此应预先用赋值,输入等办法使实参获得确定值
    2. 实参和形参在数量上,类型上,顺序上应严格一致,否则会发生类型不匹配”的错误
    3. 函数调用中发生的数据传送是单向的,只能把实参的值传送给形参,而不能把形参的值反向地传送给实参
      • 因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化
  • 示例:

    main()
    {
        int n;
        printf("input number\n");
        scanf("%d",&n);
        s(n);
        printf("n=%d\n",n);
    }
    int s(int n)
    {
        int i;
        for(i=n-1;i>=1;i--)
            n=n+i;
        printf("n=%d\n",n);
    }
    

    在主函数中输入 n 值,并作为实参,在调用 时传送给 s 函数的形参量 n

    注意:本例的形参变量和实参变量的标识符都 n,但这是两个不同的量,各自的作用域不同

  • 当参数是数组时:

    1. 把数组元素(下标变量)作为实参使用

      • 数组元素与普通变量并无区别。 因此它作为函数实参使用与普通变量是完全相同的, 在发生函数调用时,把作为实参的数组元素的值传送给形参,实现单向的值传送

      • 示例:

        void nzp(int v)
        {
            if(v>0)
                printf("%d ",v);
            else
                printf("%d ",0);
        }
        main()
        {
            int a[5],i;
            printf("input 5 numbers\n");
            for(i=0;i<5;i++)
            {scanf("%d",&a[i]);
             nzp(a[i]);}
        }
        

        在 main 函数中用一个 for 语句输入数组各元素,把 a[i] 的值传送给形参 v,供 nzp 函数使用

    2. 把数组名作为函数的形参和实参使用

      • 注意:

        1. 用数组名作函数参数时,要求形参和相对应的实参都必须是类型相同的数组,都必须有明确的数组说明。当形参和实参二者不一致时,即会发生错误

        2. 用数组名作函数参数时,不是进行值的传送,不是把实参数组的每一个元素的值都赋予形参数组的各个元素。因为实际上形参数组并不存在,编译系统不为形参数组分配内存

          • 在数组名作函数参数时所进行的传送只是地址的传送,也就是把实参数组的首地址赋予形参数组名
        3. 形参数组和实参数组的长度可以不相同,因为在调用时,只传送首地址而不检查形参数组的长度

          • 当形参数组的长度与实参数组不一致时,虽不至于出现语法错误(编译能通过),但程序执行结果将与实际不符
        4. 当用数组名作函数参数时,由于实际上形参和实参为同一数组,因此当形参数组发生变化时,实参数组也随之变化。

          • 这种情况不能理解为发生了“双向”的值传递。从实际情况来看,调用函数之后实参数组的值将由于形参数组值的变化而变化
        5. 在函数形参表中,允许不给出形参数组的长度,或用一个变量来表示数组元素的个数

          void nzp(int a[8]) //等同于void nzp(int a[]) 或者 void nzp(int a[],int n)
          

          形参数组 a 没有给出长度,而由 n 值动态地表示数组的长度。n 的值由主调函数的实参进行传送

        6. 多维数组也可以作为函数的参数。在函数定义时对形参数组可以指定每一维的长度,也可省去第一维的长度

      • 示例:

        float aver(float a[5])
        {
            int i;
            float av,s=a[0]; 
            for(i=1;i<5;i++)
                s=s+a[i];
            av=s/5;
            return av;
        }
        void main()
        {
            float sco[5],av;
            int i;
            printf("\ninput 5 scores:\n");
            for(i=0;i<5;i++)
                scanf("%f",&sco[i]);
            av=aver(sco);
            printf("average score is %5.2f",av);
        }
        
  • 返回值

    • 函数的值是指函数被调用之后,执行函数体中的程序段所取得的并返回给主调函数的值,即返回值

    • 一般形式:

      return 表达式;
      return (表达式);
      

      功能:计算表达式的值,并返回给主调函数。在函数中允许有多个 return 语句,但每次调用只能有一个 return 语句被执行,因此只能返回一个函数值

    • 注意:

      1. 函数值的类型和函数定义中函数的类型应保持一致。如果两者不一致,则以函数类型为准,自动进行类型转换
      2. 如函数值为整型,在函数定义时可以省去类型说明
      3. 不返回函数值的函数,可以明确定义为“空类型”,类型说明符为“void”,一旦函数被定义为空类型后,就不能在主调函数中使用被调函数的函数值

4.3 main 函数的参数

  • 前面使用的 main 函数都是不带参数的。因此 main 后的括号都是空括号。实际上,main 函数可以带参数,这个参数可以认为是 main 函数的形式参数

  • C语言规定 main 函数的参数只能有两个,习惯上这两个参数写为 argc 和 argv,main 函数头可写为main(argc,argv),C语言还规定 argc(第一个形参)必须是整型变量,argv(第二个形参)必须是指向字符串的指针数组(后面介绍)

    main(int argc,char argv[])
    
  • main 函数不能被其它函数调用,因此不可能在程序内部取得实际值,main 函数的参数值是从操作系统命令行上获得

    • 当我们要运行一个可执行文件时,在 DOS 提示符下键入文件名,再输入实际参数即可把这些实参传送到 main 的形参中去

    • DOS 命令符下的一般形式:

      C:\> 可执行文件 参数 参数 ……
      
  • main 的两个形参和命令行中的参数在位置上不是一一对应的,因为 main 的形参只有二个,而命令行中的参数个数原则上未加限制

    • argc 参数表示了命令行中参数的个数(注意:文件名本身也算一个参数),argc 的值是在输入命令行时由系统按 实际参数的个数自动赋予的

    • 例如:

      C:\> E24 R234 D2345 F23456
      

      由于文件名 E24 本身也算一个参数,所以共有 4 个参数,因此 argc 取得的值为 4

      argv 参数是字符串指针数组,其各元素值为命令行中各字符串(参数均按字符串处理)的首地址

      指针数组的长度即为参数个数

      数组元素初值由系统自动赋予

      • 图示:
        image-20220812153906902
  • 示例:

    main(int argc,char argv[])
    {
        while(argc-->1)
            printf("%s\n",*++argv);
    }
    

    如果上例的可执行文件名为 e24.exe,存放在 A 驱动器的盘内。因此输入的命令行为

    C:\>a:e24 BASIC foxpro FORTRAN
    

    运行结果为:

    BASIC
    foxpro
    FORTRAN
    

4.4 函数的调用

  • 一般语法格式:

    函数名(实际参数表)
    

    对无参函数调用时则无实际参数表。实际参数表中的参数可以是常数,变量或其它构造类型数据及表达式

    各实参之间用逗号分隔

  • 调用方式:

    1. 函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参与表达式的运算。这种方式要求函数是有返回值的

      z=max(x,y);
      

      将 max 的返回值赋予变量 z

    2. 函数语句:函数调用的一般形式加上分号即构成函数语句

      printf ("%d",a);
      scanf ("%d",&b);
      
    3. 函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为实参进行传送, 因此要求该函数必须是有返回值的

      printf("%d",max(x,y));
      

      将 max 调用的返回值又作为 printf 函数的实参来使用

  • 注意:求值顺序的问题

    • 所谓求值顺序是指对实参表中各量是自左至右使用,还是自右至左使用
    • 无论是从左至右求值, 还是自右至左求值,其输出顺序总是和实参表中实参的顺序相同
  • 嵌套调用

    • C语言中不允许作嵌套的函数定义。因此各函数之间是平行的,不存在上一级函数和下一级函数的问题。 但是C语言允许在一个函数的定义中出现对另一个函数的调用。这样就出现了函数的嵌套调用。即在被调函数又调用其它函数

    • 示例:

      long f1(int p)
      {
          int k;
          long r;
          long f2(int);
          k = p*p;
          r = f2(k);
          return r;
      }
      long f2(int q)
      {
          long c=1;
          int i;
          for(i=1;i<=q;i++)
              c=c*i;
          return c;
      }
      main()
      {
          int i;
          long s=0;
          for (i=2;i<=3;i++)
              s=s+f1(i);
          printf("\ns=%ld\n",s);
      }
      
  • 递归调用

    • 一个函数在它的函数体内调用它自身称为递归调用

    • 为了防止递归调用无终止地进行,必须在函数内有终止递归调用的手段

      • 常用的办法是加条件判断,满足某种条件后就不再作递归调用,然后逐层返回
    • 示例:计算 n 的阶乘

      long ff(int n)
      {
          long f;
          if(n<0) printf("n<0,input error");
          else if(n==0||n==1) f=1;
          else f=ff(n-1)*n;
          return (f);
      }
      

4.5 被调用函数的声明

  • 在主调函数中调用某函数之前应对该被调函数进行说明(声明),就像使用变量之前要先进行变量说明

  • 目的:使编译系统知道被调函数返回值的类型,以便在主调函数 中按此种类型对返回值作相应的处理

  • 一般形式:

    类型说明符 被调用函数名(类型 形参,类型 形参,...);
    类型说明符 被调函数名(类型,类型…);
    

    括号内给出了形参的类型和形参名,或只给出形参类型。这便于编译系统进行检错,以防止可能出现的错误

  • 以下几种情况时可以省去被调函数的函数说明:

    1. 如果被调函数的返回值是整型或字符型时,可以不对被调函数作说明,而直接调用。这时系统将自动对被调函数返回值按整型处理

    2. 当被调函数的函数定义出现在主调函数之前时,在主调函数中也可以不对被调函数再作说明而直接调用

    3. 如在所有函数定义之前,在函数外预先说明了各个函数的类型,则在以后的各主调函数中,可不再对被调函数作说明

      char str(int a);
      float f(float b);
      main()
      {
          ……
      }
      char str(int a)
      {
          ……
      }
      float f(float b)
      {
          ……
      }
      
    4. 对库函数的调用不需要再作说明,但必须把该函数的头文件用 include 命令包含在源文件前部

4.6 全局\局部变量

  • 局部变量

    • 局部变量也称为内部变量,在函数内作定义说明的。其作用域仅限于函数内

    • 示例:

      int f1(int a) /*函数 f1*/
      {
          int b,c; 
          ……
      }
      

      f1 内定义了三个变量,a 为形参,b,c 为一般变量。在 f1 的范围内 a,b,c 有效,或者说 a,b,c 变量的作用域限于 f1 内

    • 注意:

      1. 主函数中定义的变量也只能在主函数中使用,不能在其它函数中使用。同时,主函数中也不能使用其它函数中定义的变量

        • 主函数也是一个函数,它与其它函数是平行关系
      2. 形参变量是属于被调函数的局部变量,实参变量是属于主调函数的局部变量

      3. 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰,也不会发生混淆

      4. 在复合语句中也可定义变量,其作用域只在复合语句范围内

        main()
        {
            int s,a;
            ……
            {
                int b;
                s=a+b;
                …… /*b 作用域*/
            }
            …… /*s,a 作用域*/
        }
        
  • 全局变量

    • 全局变量也称为外部变量,是在函数外部定义的变量。不属于哪一个函数,它属于一个源程序文件。 其作用域是整个源程序

      • 如果不在文件的开头定义,则作用域为从变量定义处开始,到本程序文件的末 尾
    • 要使用全局变量时,一般应作全局变量说明。 只有在函数内经过说明的全局变量才能使用

    • extern 为声明关键字

      • 在一个函数之前定义的全局变量,在该函数内使用可不再加以说明

        int a,b; /*外部变量*/
        void f1() /*函数 f1*/
        {
            ……
        }
        float x,y; /*外部变量*/
        int fz() /*函数 fz*/
        {
            ……
        }
        main() /*主函数*/
        {
            ……
        }
        

        a、b、x、y 都是在函数外部定义的外部变量,都是全局变量

        x、y 定义在函数 f1 之 后,而在 f1 内又无对 x,y 的说明,所以它们在 f1 内无效

        a、b 定义在源程序最前面,因此在 f1、f2 及 main 内不加说明也可使用

      • 在定义点之前的函数想引用该外部变量,则应该在引用之前用关键字

        main()
        {
            extern A,B;
            printf("%d\n",max(A,B));
        }
        int A=13,B=-8;
        

        在最后 1 行定义了外部变量 A,B,但由于外部变量定义的位置在函数 main 之后,因此本来在 main 函数中不能引用全局变量 A,B

        现在在 main 函数中用 extern 对 A 和 B 进行“全局变量声明”,就可以从“声明”处起,合法地使用该全局变量 A 和 B

    • 如果同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部变量被“屏蔽”,即它不起作用

      • 示例:

        int a=3,b=5; /*a,b 为外部变量*/
        max(int a,int b) /*a,b 为外部变量*/
        {int c;
         c=a>b?a:b;
         return(c);
        }
        main()
        {int a=8;
         printf("%d\n",max(a,b));
        
  • 变量的存储方式

    • 按照变量值存在的时间(即生存期)角度来分,可以分为静态存储方式和动态存储方式

      • 静态存储方式:是指在程序运行期间分配固定的存储空间的方式
      • 动态存储方式:是在程序运行期间根据需要进行动态的分配存储空间的方式
    • 用户存储空间可以分为三个部分:程序区、静态存储区、动态存储区

      • 静态存储区,在程序开始执行时分配存储区,程序行完毕就释放。在程序执行过程中它们占据固定的存储单元,而不动态地进行分配和释放。全局变量全部存于此处
      • 动态存储区,在函数开始调用时分配动态存储空间,函数结束时释放这些空间。存放函数形式参数、自动变量(未加 static 声明的局部变量)、函数调用实的现场保护和返回地址等数据
    • C 语言中,每个变量和函数有两个属性:数据类型和数据的存储类别

  • 自动变量

    • 自动变量:在调用该函数时系统会给它们分配存储空间,在函数调用结束时就自动释放这些存储空间的这类局部变量

    • 函数中的局部变量(如不专门声明为 static 存储类别)、函数中的形参、在函数中定义的变量(包括在复合语句中定义的变量)都属自动变量类

    • 使用关键字 auto 作存储类别的声明

    • 示例:

      auto int a,b;
      

      定义 a、b 自动变量

  • 静态局部变量

    • 静态局部变量:函数中的局部变量的值在函数调用结束后不消失而保留原值的这类局部变量

    • 使用 static 关键字声明

    • 与自动变量的对比:

      静态局部变量 自动变量
      存储类别 静态存储类别,在静态存储区内分配存储单元。在程序整个运行期间都不释放 动态存储类别,占动态存储空间,函数调用结束后即释放
      赋值时机 在编译时赋初值,只赋初值一次 是在函数调用时进行,每调用一 次函数重新给一次初值,相当于执行一次赋值语句
      定义局部变量时不赋初值的情况 编译时自动赋初值 0(对数值型变量) 或空字符(对字符变量) 不赋初值则它的值是一个不确定的值
  • 寄存器变量

    • 寄存器变量:将局部变量的值放在 CPU 中的寄存器中的这种局部变量

    • 使用关键字 register 作声明

    • 示例:

      register int a;
      
    • 说明:

      1. 只有局部自动变量和形式参数可以作为寄存器变量
      2. 一个计算机系统中的寄存器数目有限,不能定义任意多个寄存器变量
      3. 局部静态变量不能定义为寄存器变量
posted @   你是我的生命之源  阅读(238)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
页脚
点击右上角即可分享
微信分享提示