7.1为什么要用函数

函数就是功能,每个函数用来实现一个特定的功能。

在程序设计中要善于利用函数,来减少重复编写程序段的工作量实现模块化设计。

一个C程序可以由一个主函数和若干个其他函数构成

例7.1

# include "stdio.h"
int main()
{
    //声明print_star函数   告知编译器函数的相关信息 参数类型和数量  返回值类型   
    void print_star();
    //声明print_message函数
    void print_message(); 
    //调用print_star函数
    print_star();
    //调用print_message函数
    print_message(); 
    //调用print_star函数
    print_star();
    return '\0';
 } 

 //定义print_star函数
void print_star()
{
    printf("**********************\n");
 }

//定义一个print_message函数
void print_message()
{
    printf("Hello World!!\n");
  }  

说明:

  1. 一个C程序由一个或多个程序模块组成,每个程序模块作为一个源程序文件。
  2. 一个源程序文件由一个或多个函数以及其他有关内容组成。一个源程序文件是一个编译单位。
  3. C程序执行是从main函数开始的。在main函数中结束整个程序的运行。
  4. 所有函数都是平行的,定义函数时是分别进行,相互独立的。
  5. 从用户使用的角度看,函数有两种。
    • 库函数,由编译系统提供,不用自己定义,可以直接使用。
    • 用户自定义函数。用于解决用户撰文需要的函数。
  6. 从函数形式上看,函数分两类。
    • 无参函数。
    • 有参函数。

7.2 怎样定义函数

C语言要求,在程序中用到的所有函数,必须“先定义,后使用”。

定义函数应包括以下几个内容:

  1. 函数名
  2. 函数返回值类型
  3. 函数参数名和类型,无参函数不需要这项
  4. 指定函数应当完成什么操作,函数的功能

7.2.2 定义函数的方法

定义无参函数

一般形式:

类型名    函数名()
{
    函数体
}
或
类型名    函数名(void)
{
    函数体
}

注意:

函数体包括声明部分语句部分

声明部分,如:int x;

语句部分,如:printf(“%d”,x);

定义有参函数

类型名 函数名()
{
    函数体
}

定义空函数

类型名    函数名()
{}

空函数用于扩充新功能

7.3 调用函数

7.3.1 函数调用形式

按函数调用在程序中出现的形式和位置来分,可以有一下3种调用方式。

1.函数调用语句

把函数调用作为单独的语句

2.函数表达式

函数调用出现在另一个表达式中

3.函数参数

函数调用作为另一个函数的参数

7.3.2 函数调用时数据传递

形式参数

定义函数时的变量名

实际参数

调用时传递的常量,变量或表达式。

实参和形参间的数据传递

形参从实参那获取值。

7.3.3 函数调用的过程

  1. 发生函数调用时,函数的形参被临时分配内存单元
  2. 将实参对应的值传递给形参
  3. 执行被调用函数的语句
  4. 通过return语句将函数值带回主调函数(如果函数不需要返回值,则不需要return语句,函数定义类型应定义为void类型)
  5. 调用结束,形参单元被释放。实参没有改变

7.3.4 函数的返回值

函数的返回值:函数调用使主调函数能得到一个确定的值。

说明(对于函数返回值):

  1. 函数的返回值是通过函数中的return语句获得的。
  2. 定义函数时指定函数返回值类型。
  3. 在定义函数时指定函数类型一般和return语句中的表达式类型一致。函数类型决定返回值的类型。数值类型会自动进行类型转换。
  4. 没有返回值的函数,定义为void类型

7.4 对被调用函数的声明和函数原型

函数调用所需条件

  1. 已经被定义好的函数
  2. 使用库函数,使用预编译指令(#include)将有关库函数时所需用到的信息“包含”到本文件中来。
  3. 使用自定义函数,在主调函数中对被调用函数进行声明。(声明是为了正确识别和检查被调用的函数[包括信息有:函数名,返回值类型,参数数量,顺序,类型]是否合法)

函数原型(函数声明)有两种:

  1. 函数类型 函数名(参数类型1 参数名1,参数类型2 参数2);
  2. 函数类型 函数名(参数类型1,参数类型2);

7.5 函数的嵌套调用

一张图就解释清楚了

函数的嵌套调用.png

7.6 函数的递归调用

在调用一个函数的过程有出现直接或间接的调用该函数本身,称为函数的递归调用。

例如:

# include "stdio.h"
int main() 
{
    int num=5;
    //声明函数
    int age(int num);
    printf("%d",age(num));//在输出函数里调用函数 
    return 0; 
}

//定义函数
int age(int num)
{
    int c;
    if(num==1)
        c=10;
    else
        c=age(num-1)+2; 
    return c;
}

如果要求递归过程不是无限制进行下去,必须具有一个结束递归过程的条件。

# include "stdio.h"
/*
使用递归求阶乘 
*/ 
int main() 
{
    int num=10;
    //声明函数
    int factorial(int num);
    printf("%d",factorial(num));//在输出函数里调用函数 
    return 0; 
}

//定义函数
int factorial(int num)
{
    int c;
    if(num==1||num==0)
        c=1;
    else if(num<0)
        printf("参数出错");
    else
        c=factorial(num-1)*num; 
    return c;
}

7.7 数组作为函数参数

凡是变量可以出现的地方,都可以用数组元素代替。用法与变量相同。

传递数组名,传递的是第一个元素地址。

7.7.1 数组元素作函数实参

数组元素作函数实参时,把实参的值传给形参,是“值传递”方式。

# include "stdio.h"
/*
    练习用数组元素作为参数 
*/
int main()
{
    int a[10],i,n,maxNum;
    int max(int x,int y);//声明函数 
    printf("请依次输入10个数\n");
    for(i=0;i<10;i++)//分别对元素进行赋值
    {
        scanf("%d",&a[i]);
    } 
    for(i=1,maxNum=a[0];i<10;i++)//调用函数 
    {
        if(max(a[i],maxNum)>maxNum)
        {
            maxNum=max(a[i],maxNum);
            n=i+1;
        }
    }
    printf("%d\t%d\n",maxNum,n);
} 

//定义函数
int max(int x,int y)
{
    return x>y?x:y;
}

7.7.2 数组名作函数参数

用数组元素作实参时,向形参变量传递的是数组元素的值,而用数组名作函数实参时,向形参传递的是数组元素的地址。

# include "stdio.h"
/*
    练习用数组名作为参数 
*/
int main()
{
    int i;
    float a[10],av;
    float average(float array[10]);//声明函数 
    printf("请依次输入10个数\n");
    for(i=0;i<10;i++)//分别对元素进行赋值
    {
        scanf("%f",&a[i]);
    }
    av=average(a);
    printf("%f",av);
} 

//定义函数
float average(float array[10])
{
    int i;
    float sum,aver;
    for(i=1,sum=array[0];i<10;i++)
    {
        sum+=array[i];
    }
    aver=sum/10;
    return aver;
}
/*
实参与形参类型不一致,会出错
形参数组可以不指定容量 可以这样定义:a[]
*/
# include "stdio.h"
/*
    练习用选择排序 
*/
int main()
{
    int i;
    float a[10],av;
    void average(float array[],int n);//声明函数 
    printf("请依次输入10个数\n");
    for(i=0;i<10;i++)//分别对元素进行赋值
    {
        scanf("%f",&a[i]);
    }
    average(a,10);
    for(i=0;i<10;i++)
        printf("%f\n",a[i]);
} 

//定义函数
void average(float array[],int n)
{
    int i,j,temp;
    for(i=0;i<n-1;i++)
        for(j=i+1;j<n;j++)
        {
            if(array[i]>array[j])
            {
                temp=array[i];
                array[i]=array[j];
                array[j]=temp;
            }
        }
}

7.7.3 多维数组作函数名

可以作为函数的实参和形参,可以省略第一维的大小,第二维不能省略,C语言编译系统不检查第一维的大小。

7.8 局部变量和全局变量

7.8.1 局部变量

定义变量的位置

  1. 函数的开头定义
  2. 函数内的复合语句内定义
  3. 函数外都定义

在复合语句内定义的变量旨在本复合语句范围内有效(花括号范围内),在本复合语句内才能引用他们。复合语句内不能使用,这被成为局部变量。

形式参数也是局部变量。

7.8.2 全局变量

全局变量:函数外部定义的变量成为外部变量,外部变量也称全程变量

作用范围:定义行到主函数结束,程序是从主函数开始到主函数结束

全局变量采用大驼峰命名

全局变量与局部变量重名,局部变量优先

如:

# include "stdio.h"
 int a=3,b=5;
 int main()
 {
     int max(int a,int b);
     int a=8;
     printf("the max num is %d \n",max(a,b));
 }

 int max(int a,int b)
 {
     return a>b?a:b;
 }


结果:
the max num is 8

7.9 变量的存储方式和生存期

7.9.1 动态存储方式和静态存储方式

从空间(作用域)的角度来观察,变量可以分为全局变量和局部变量

从变量存在的时间(生存期)来看,变量存储存储方式有两种:

  • 静态存储:在程序运行期间由系统分配固定的存储空间的方式。
  • 动态存储:在程序运行期间根据需要进行动态的分配存储空间。

用户可以使用的内存存储空间结构:

  1. 程序区
  2. 静态存储区
  3. 动态存储区

全局按量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕后就释放。

动态存储区存放:

  1. 函数形式参数。在调用函数时给形参分配存储空间。
  2. 函数中定义的没有用关键字static声明的变量,自动变量
  3. 函数调用时的现场保护和返回地址。

在函数调用开始时分配动态存储空间,函数结束时释放这些空间。

在程序执行过程中,同一个程序调用两次相同函数,两次分配的给这些局部变量的存储空间地址可能是不相同的。

每个函数中的局部变量生存期只是程序执行期的一部分。

在程序执行过程中,先后调用函数,动态分配和释放空间。

C语言中,数据类型数据存储类别变量函数的属性。

7.9.2 局部变量的存储类别

1.自动变量(auto变量)

函数中的形参和函数中局部变量,都是动态分配的,调用该函数是,系统会给这些变量分配存储空间,在函数调用结束后,就会自动释放这些存储空间。这类局部变量称为自动变量。

1.以auto修饰的变量 2.没有关键字修饰的。默认为自动变量

2.静态局部变量(static局部变量)

局部变量的值在函数调用后结束不小时而继续保留原值,即不释放存储空间,在下次调用函数时,该变量已有值。这是该指定该局部变量为“静态局部变量”

# include "stdio.h"
int main()
{
    void test();
    test();
    test();
    return 0;
} 

void test()
{
    static int a =1;
    printf("%d\n",++a);
}


2
3

--------------------------------
Process exited after 0.01536 seconds with return value 0
请按任意键继续. . .

静态存储要多占内存,不能合理利用资源(和动态存储对比),且降低了程序的可读性,因此,若非必要,不要多用静态局部变量。

3.寄存器变量(register变量)

用于提高效率,寄存器的存取速度远大于内存的存取速度。

关键字是: register int f

寄存器变量存储在CPU中的寄存器中。

7.9.3 全局变量的存储类别

全局变量都是存放于静态存放区中的,因此它们的生存期(生命周期)是固定的。

作用域:定义行开始到本程序文件末尾

1.一个文件内扩展外部变量的作用域

在定义点之前的函数需要引用该外部变量,在引用之前用关键字externa对该变量作“外部变量声明”。

# include "stdio.h"
int main()
{
    int max();
    extern int A,B,C;//将外部变量的作用扩展到由此处开始。 
    scanf("%d%d%d",&A,&B,&C);
    printf("%d",max());
    return 0;
} 

int A,B,C;//定义外部变量

int max()
{
    int m;
    m=A>B?A:B;
    return m>C?m:C;
 } 

注意:

  1. 建议吧外部函数定义写在所有函数之前,可以避免不必要的extern的声明。
  2. 使用extern时,类型名可以省略

2.将外部变量作用域扩展到其他文件

​ 如果一个程序包含两个文件,在两个文件中都要用到同一个外部变量Num,不能分别在两个文件找那个各自定义一个外部变量,连接时会出现“重复定义”的错误。

正确做法是:

  1. 在任意一个文件中定义外部变量
  2. 另一个文件中用extern对Num作“外部变量声明”。

3.将外部变量的作用域限制在本文件中

只限在本文件使用的外部变量,加关键字:static

static声明一个变量的作用:

  1. 对局部变量用static声明,把分配静态存储区,该变量在整个程序执行期间不释放,其分配的空间始终存在。
  2. 对全局变量用static声明,该变量的作用域只限本文件模块。

7.9.4 存储类别小结

从作用域角度分析,有局部变量和全局变量

从生存期来区分,有静态存储和动态存储

从变量值存放的位置来区分,有内存中的动态存储区,内存中的静态存储区,CPU中的寄存器

作用域和生存期的概念:作用域是空间上的描述;生存期是时间的的描述

static对局部变量来说:使变量从动态存储转为静态存储;static对于全局变量来说:规定了其作用域

7.10 关于定义和声明

定义是声明的一种体现,如int a;

建立存储空间的声明称为定义(定义一个变量,分配内存空间)

不需要简历存储存储空间的声明称为声明。(如:int max(int);)

声明和定义的区别.png

外部变量只能定义一次,可多次声明。系统根据外部变量的定义分配存储单元,同时进行初始化。

7.11 内部函数和外部函数

7.11.1 内部函数

在定义内部函数时,在函数名和函数类型的前面加static

即:

static 类型名 函数名(形参列表)

{

函数体

}

不用担心函数是否会与其他文件模块函数同名

7.11.2 外部函数

在定义函数时,在函数首部的最左端加关键字extern,这函数是外部函数。关键字省略,默认为外部函数。

如:

extern int fun(int a,int b)

{

​ 函数体

}

在需要调用此函数的其他文件中,需要对此函数做声明,声明时需要加extern关键字。表示这是其他文件中定义的外部函数。

由于函数在本质上时外部的,在程序中经常要调用其他文件的外部函数,为了方便编程,C语言允许声明函数时可以省略extern

在调用方中声明一个函数,这个函数就是被调用函数的函数原型。

函数原型的作用:

扩展函数的作用域。

常见的例子就是 # include指令的使用,通过引入其他库函数,然后进行使用。