第七章 用函数实现模块化程序设计

5/28/2022 C语言

讲师介绍:软件工程师,阿里云技术专家,《智能前端技术与实践》作者

考试科目:883 《C程序设计》(第五版)谭浩强,清华大学出版社

参考资料:《C程序设计(第五版)》谭浩强

# 7.1 为什么要用函数

函数就是功能,每一个函数用来实现一个特定的功能,函数的名字反应其代表的功能。

在设计一个较大的程序时,往往把它分为若干个程序模块,每一个模块包含一个或多个函数,每个函数实现一个特定的功能。一个C程序可由一个主函数和若干个其他函数构成。由主函数调用其他函数,其他函数也可以互相调用,同一个函数可以被一个或多个函数调用任意多次,如图所示。

例7.1 输出特定形状

#include<stdio.h>

int main(){
    int print_star();
    int print_message();
		print_star();
    print_message();
    print_star();
    return 0;
}
int print_message(){
    printf("HelloWorld!\n");
    return 0;
}
int print_star(){
    printf("***************\n");
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行效果如下所示:

在程序中,定义print_star函数和print_message函数的位置都是在main函数的后面,在这种情况下,应当在main函数之前或main函数中的开头部分,对以上两个函数进行声明。函数声明的作用是把有关函数的信息(函数名、函数类型、函数参数的个数与类型)通知编译系统,以便在编译系统对程序进行编译时,在进行到main函数调用print_star()print_message()时知道它们是函数而不是变量或其他对象,此外,还对调用函数的正确性进行检查,如类型、函数名、参数个数、参数类型等是否正确。

【注】:

  • 一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件,对较大的程序,一般不希望把所有内容全部放在一个文件中,而是将它们分别放在若干个源文件中,由若干个源程序文件组成一个C程序,这样便于分别编写和编译,提高调试效率。一个源程序文件可以为多个C程序共用。
  • 一个源程序文件由一个或多个函数以及其他有关内容(如指令、数据声明与定义等)组成。一个源程序文件是一个编译单位,在程序编译时是以源程序文件为单位进行编译的,而不是以函数为单位进行编译的。
  • C程序的执行是从main函数开始的,如果在main函数中调用其他函数,再调用后流程返回到main函数,在main函数中结束整个程序的运行。
  • 所有函数都是平行的,即在定义函数时是分别进行的,是互相独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义。函数间可以互相调用,但不能调用main函数,main函数是被操作系统调用的。
  • 从用户使用的角度看,函数有两种。
    • 库函数:由系统提供,用户不必自己定义,可直接使用,根据不同的C编译系统,提供的库函数的数量和功能会有一些不同;
    • 用户自己定义的函数:用以解决用户专门需要的函数;
  • 从函数的形式看,函数分两类。
    • 无参函数:主调函数无需向被调用函数传递数据,一般仅用来执行指定的一组操作,其函数值可以带回或不带回,一般以不带回函数值的居多;
    • 有参函数:主调函数向被调用函数传递数据,并得到返回值供主调函数使用;

# 7.2 怎样定义函数

# 7.2.1 为什么要定义函数

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

  • 指定函数的名字,以便以后按名调用;
  • 指定函数的类型,即函数返回值的类型;
  • 指定函数的参数的名字和类型,以便在调用函数时向它们传递数据,对无参函数不需要这项;
  • 指定函数应当完成什么操作,也就是函数是做什么的,即函数的功能。

对于C编译系统提供的库函数,只需通过#include命令引入头文件即可。

# 7.2.2 定义函数的方法

  • 定义无参函数
类型名 函数名(){
	//函数体
}
或
类型名 函数名(void){
	//函数体
}
其中,函数名后面括号内的void表示“空”,即函数没有参数。
1
2
3
4
5
6
7
8
  • 定义有参函数
类型名 函数名(形式参数列表){
	//函数体
}
1
2
3
  • 定义空函数
类型名 函数名(){}
1

# 7.3 调用函数

# 7.3.1 函数调用的形式

函数调用的一般形式为:

函数名(实参表列);
1

【注】:若实参表列中包含多个实参,则各参数见用逗号隔开。

3种函数调用方式:

  • 函数调用语句

    把函数调用单独作为一个语句,这时不要求函数带回值,只要求函数完成一定的操作。

  • 函数表达式

    函数调用出现在另一个表达式中,如c = max(a,b);max(a,b)是一次函数调用,它是赋值表达式中的一部分,此时要求函数带回一个确定的值以参加表达式的计算。

  • 函数参数

    函数调用作为另一个函数调用时的实参,如m = max(a,max(b,c)),其中max(b,c)是一次函数调用,它的值作为max另一次调用的实参。

# 7.3.2 函数调用时的数据传递

  • 形式参数:定义函数时函数名后面括号中的变量称为形式参数
  • 实际参数:在主调函数中调用一个函数时,函数名后面括号中的参数称为实际参数

【注】

  • 实参可以是常量、变量或表达式;
  • 实参与形参的类型应相同或赋值兼容。

# 7.3.3 函数调用的过程

  • 在定义函数中指定的形参,当未出现函数调用时,它们并不占内存中的存储单元,在发生函数调用时,其形参才会被临时分配内存单元;
  • 函数调用结束时,形参单元会被释放,但实参单元仍保留并维持原值,没有改变,这是因为实参与形参是两个不同的存储单元;
  • 实参向形参的数据传递是值传递,单向传递,只能由实参传递给形参,而不能由形参传给实参,实参和形参在内存中占有不同的存储单元,实参无法得到形参的值;

# 7.3.4 函数的返回值

  • 函数的返回值是通过函数中的return语句获得的,一个函数中可以有一个以上的return语句;
  • 应当在定义函数时指定函数值的类型;
  • 在定义函数时指定的函数类型一般应该和return语句中的表达式类型一致,若不一致,则以函数类型为准;

例7.2 函数类型决定返回值类型

#include<stdio.h>
int main(){
    int max(float x,float y);
    float a,b;
    int c;
    printf("请输入两个数字:");
    scanf("%f %f",&a,&b);
    c = max(a,b);
    printf("max = %d\n",c);
    return 0;
}
int max(float x,float y){
    float z;
    z = x>y?x:y;
    return z;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

运行效果如下所示:

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

函数的首行称为函数原型,使用函数原型作声明是C的一个重要特点,用函数原型来声明函数能减少编写程序时可能出现的错误。

实际上,在函数声明中的形参名可以省写,而只写形参的类型,编译系统只关心和检查参数个数和参数类型,而不检查参数名,因为在调用函数时只要求保证实参类型与形参类型一致,而不必考虑形参名是什么。

一般情况下,函数原型的一般形式有两种:

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

例7.3 输入两个实数,用一个函数求出它们之和。

#include<stdio.h>
int main(){
    float add(float x,float y);
    float a,b,sum;
    printf("请输入两个数字:");
    scanf("%f %f",&a,&b);
    sum = add(a,b);
    printf("sum = %.2f\n",sum);
    return 0;
}
float add(float x,float y){
    float z;
    z = x+y;
    return z;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

运行结果如下所示:

如果已在文件的开头(在所有函数之前),已对本文件中所调用的函数进行了声明,则在各函数中不必对其所调用的函数再作声明。

# 7.5 函数的嵌套调用

C语言的函数定义是互相平行、独立的,也就是说,在定义函数时,一个函数内是不能再定义另一个函数,也就是不能嵌套定义,但可以嵌套调用函数

  • 执行main函数的开头部分;
  • 遇函数调用语句,调用函数a,流程转去a函数;
  • 执行a函数的开头部分;
  • 遇函数调用语句,调用函数b,流程转去b函数;
  • 执行b函数,如果再无其他嵌套的函数,则完成b函数的全部操作;
  • 返回到a函数中调用b函数的位置;
  • 继续执行a函数中尚未执行的部分,直到a函数结束;
  • 返回main函数中调用a函数的位置;
  • 继续执行main函数的剩余部分直到结束;

例7.5 输入4个整数,找出其中最大的数,用函数的嵌套来处理。

#include<stdio.h>
int main(){
    int max4(int a,int b,int c,int d);
    int a,b,c,d,max;
    printf("请输入四个数字:");
    scanf("%d %d %d %d",&a,&b,&c,&d);
    max = max4(a, b, c, d);
    printf("最大值为%d\n",max);
    return 0;
}
int max4(int a,int b,int c,int d){
    int max2(int a,int b);
    return max2(max2(max2(a, b),c ),d);
}
int max2(int a,int b){
    return a>b?a:b;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行结果如下所示:

# 7.6 函数的递归调用

一个递归问题可以分为两个阶段:回溯递推

在调用函数f的过程中,又要调用f函数,这是直接调用本函数。

如果在调用f1函数过程中要调用f2函数,而在调用f2函数过程中又要调用f1函数,就是间接调用本函数。

例7.5 年龄问题

#include<stdio.h>
int age(int n){
    int c;
    if(n==1){
        c = 10;
    }else{
        c = age(n-1)+2;
    }
    return c;
}
int main(){
    int age(int n);
    printf("第五个学生的年龄为:%d\n",age(5));
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

回溯与递推示意图:

运行结果如下所示:

例7.6 求阶乘

#include<stdio.h>
int main(){
    int fac(int n);
    int n;
    int res;
    printf("请输入一个数字:");
    scanf("%d",&n);
    res = fac(n);
    printf("%d的阶乘值为%d\n",n,res);
    return 0;
}

int fac(int n){
    int f;
    if(n < 0){
        printf("当前值不合法!\n");
    }else if(n==0||n==1){
        f = 1;
    }else{
        f = fac(n-1)*n;
    }
    return f;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

例7.7 汉诺塔问题

n个盘子从A座移到C座的解题思路:

  • An-1个盘借助C座先移到B座上;
  • A座上剩下的一个盘移到C座上;
  • n-1个盘从B座借助于A座移到C座上;
#include<stdio.h>
int main(){
    int hanoi(int n,char one,char two,char three);
    int num;
    printf("请输入盘子数量:");
    scanf("%d",&num);
    printf("移动%d个盘子所需要的步骤为:\n",num);
    hanoi(num, 'A', 'B', 'C');
    return 0;
}
int hanoi(int n,char one,char two,char three){
    void move(char x,char y);
    if(n==1){
        move(one,three);
    }else{
        hanoi(n-1, one, three, two);
        move(one,three);
        hanoi(n-1, two, one, three);
    }
    return 0;
}
void move(char x,char y){
    printf("%c->%c\n",x,y);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

参考链接:汉诺塔问题

# 7.7 数组作为参数

# 7.7.1 数组元素作函数实参

数组元素可以用作函数实参,不能用做形参,因为形参是在函数被调用时临时分配存储单元的,不可能为一个数组元素单独分配单元,因为数组是一个整体,在内存中占连续的一段存储单元。

当用数组元素做函数实参时,把实参的值传给形参,是值传递方式,数据传递的方向是从实参传到形参,单向传递

例7.8 输入10个数,要求输出其中值最大的元素和该数是第几个数。

#include<stdio.h>
int main(){
    int max(int x,int y);
    int arr[10],m,n,i;
    printf("请输入10个数字:");
    for (int i = 0; i < 10; i++) {
        scanf("%d",&arr[i]);
    }
    for (i = 1,m = arr[0],n = 0; i < 10; i++) {
        if(max(m,arr[i])>m){
            m = max(m, arr[i]);
            n = i;
        }
    }
    printf("10个数字中最大者为:%d,其下标索引为:%d\n",m,n);
    return 0;
}
int max(int x,int y){
    return (x>y)?x:y;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

运行效果如下所示:

# 7.7.2 数组名作函数参数

除了可以用数组元素作为函数参数外,还可以用数组名作为函数参数(包括实参和形参),应当注意的是:用数组元素作实参时,向形参变量传递的是数组元素的值,而用数组名作为函数实参时,向形参(数组名或指针变量)传递的是数组首元素的地址。

例7.9 有一个一维数组,存放10个学生的成绩,求成绩的平均数。

#include<stdio.h>
int main(){
    float average(float array[10]);
    float score[10],aver;
    printf("请输入10个学生的成绩:");
    for (int i = 0; i < 10; i++) {
        scanf("%f",&score[i]);
    }
    aver = average(score);
    printf("平均分为%.2f\n",aver);
    return 0;
}
float average(float array[10]){
    float aver,sum = array[0];
    for (int i = 1; i < 10; i++) {
        sum += array[i];
    }
    aver = sum/10;
    return aver;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

运行效果如下所示:

用数组名作函数实参时,不是把数组元素的值传递给形参,而是把实参数组的首元素的地址传递给形参数组,这样两个数组就占同一块内存单元。如果实参数组为a,形参数组为b,若a的首元素地址为1000,则b数组首元素的地址也是1000,显然,a[0]b[0]同占一个单元...假如改变了b[0]的值,也就意味着a[0]的值也改变了,也就是说,形参数组中歌各元素的值如发生变化会使实参数组元素的值同时发生变化。

例7.10 用选择法对数组中10个整数按由小到大排序。

#include<stdio.h>
int main(){
    void select_sort(int array[],int n);
    int arr[10],i;
    printf("请输入一个数组:");
    for (i = 0; i < 10; i++) {
        scanf("%d",&arr[i]);
    }
    select_sort(arr, 10);
    printf("排序之后的数组为:");
    for (i = 0; i < 10; i++) {
        printf("%5d",arr[i]);
    }
    printf("\n");
    return 0;
}

void select_sort(int a[],int n)//n为数组a的元素个数
{
    //进行N-1轮选择
    for(int i=0; i<n-1; i++)
    {
        int min_index = i;
        //找出第i小的数所在的位置
        for(int j=i+1; j<n; j++)
        {
            if(a[j] < a[min_index])
            {
                min_index = j;
            }
        } 
        //将第i小的数,放在第i个位置;如果刚好,就不用交换
        if( i != min_index)
        {
            int temp = a[i];
            a[i] = a[min_index];
            a[min_index] = temp;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

运行结果如下所示:

# 7.7.3 多维数组名作函数参数

可以用多维数组名作为函数的实参和形参,在被调用函数中对形参数组定义时可以指定每一维的大小,也可以省略第一维的大小说明。

int array[3][10] 等价于 int array[][10]
1

这是因为二维数组是由若干个一维数组组成的,在内存中,数组是按行存放的,因此,再定义二维数组时,必须指定列数(即一行中包含几个元素),由于形参数组与实参数组类型相同,所以它们是由具有相同长度的一维数组所组成的,不能只指定第1维而省略第2维。

在第2维大小相同的情况下,形参数组的第1维可以与实参数组不同,如实参数组定义为int score[5][10],而形参数组定义为int array[][10]

C语言编译系统不检查第1维的大小。

例7.11 求二维数组中的最大值

#include<stdio.h>
int main(){
    int max_value(int array[3][3]);
    int arr[3][3]={{0},{0},{0}};
    printf("请输入原数组元素:");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            scanf("%d",&arr[i][j]);
        }
    }
    printf("原数组为:\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%3d",arr[i][j]);
        }
        printf("\n");
    }
    printf("原数组中的最大值为:%d\n",max_value(arr));
    return 0;
}
int max_value(int array[3][3]){
    int i ,j ,max;
    max = array[0][0];
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 3; j++) {
            if(array[i][j]>max){
                max = array[i][j];
            }
        }
    }
    return max;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

运行结果为:

# 7.8 局部变量和全局变量

# 7.8.1 局部变量

定义变量可能有三种情况:

  • 在函数的开头定义;
  • 在函数内的复合语句内定义;
  • 在函数的外部定义;

局部变量概念:在函数或复合语句内部定义的变量。

「注」:

  • 主函数中定义的变量也只在主函数中有效,并不因为在主函数中定义而在整个文件或程序中有效,主函数也不能使用其他函数中定义的变量;
  • 不同函数中可以使用同名的变量,它们代表不同的对象,互不干扰;
  • 形式参数也是局部变量;
  • 在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,这种语句也称为分程序程序块

变量C只在复合语句(分程序)内有效,离开该复合语句该变量就无效,系统会把它占用的内存单元释放。

# 7.8.2 全局变量

概念:在函数内定义的变量是局部变量,在函数外定义的变量是全局变量。

设置全局变量的作用是增加了函数间数据联系的渠道,由于同一文件中的所有函数都能引用全局变量的值,因此如果在某一个函数中改变了全局变量的值,就能影响到其他函数中全局变量的值。相当于各个函数间有直接的传递通道,由于函数的调用只能带回一个函数返回值,因此有时可以利用全局变量来增加函数间的联系渠道,通过函数调用能得到一个以上的值。

例 7.12 编写函数求10个学生成绩的平均分、最高分和最低分。

#include<stdio.h>
float Max = 0.0,Min = 0.0;
int main(){
    float average(float array[],int n);
    float ave,score[10];
    printf("请输入学生成绩:");
    for (int i = 0; i < 10; i++) {
        scanf("%f",&score[i]);
    }
    ave = average(score, 10);
    printf("max=%.2f,min=%.2f,average=%.2f\n",Max,Min,ave);
    return 0;
}
float average(float array[],int n){
    float aver,sum = array[0];
    Max = Min = array[0];
    for (int i = 1; i < n; i++) {
        if(array[i]>Max){
            Max = array[i];
        }else if (array[i]<Min){
            Min = array[i];
        }
        sum += array[i];
    }
    aver = sum/n;
    return aver;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

运行效果如下所示:

一般情况下,不建议使用全局变量:

  • 全局变量在程序的执行过程中都占用内存单元,而不是仅在需要时才开辟单元;
  • 降低了函数的通用型,当将该函数移植到另一个文件中时,还要考虑把相关的外部变量一起移植过去,但是若该外部变量与其他文件的变量同名时,就会出现问题,程序设计要求单个模块内部内聚性强,与其他模块耦合性弱;
  • 使用全局变量过多,会降低程序的清晰性,人们往往难以清除地判断出每个瞬时各个外部变量的值;

例 7.13 全局变量与局部变量同名。

#include<stdio.h>
int globalVar = 0;
int main(){
    int globalVar = 3;
    printf("globalVar = %d\n",globalVar);
    return 0;
}
1
2
3
4
5
6
7

运行效果如下所示:

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

# 7.9.1 动态存储方式与静态存储方式

变量的存储有两种不同的方式:静态存储方式动态存储方式,静态存储方式是指在程序运行期间由系统分配固定的存储空间的方式,而动态存储方式则是在程序运行期间根据需要进行动态的分配存储空间的方式。

可供用户使用的存储空间分为三部分:程序区、静态存储区、动态存储区。

数据分别存放在静态存储区和动态存储区,全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放,在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放。

在动态存储区中存放以下数据:

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

对以上这些数据,在函数调用开始时分配动态存储空间,函数结束时释放这些空间。在程序执行过程中,这种分配和释放是动态的,如果在一个程序中两次调用同一函数,而在此函数中定义了局部变量,在两次调用时分配给这些局部变量的存储空间的地址可能是不相同的。

C语言中,每一个变量和函数都有两个属性:数据类型和数据的存储类别,存储类别指的是数据在内存中存储的方式,如静态存储和动态存储,再定义和声明变量和函数时,一般应同时指定其数据类型和存储类别,也可以采用默认方式指定,其存储类别包括autostaticregisterextern四种。

# 7.9.2 局部变量的存储类别

  1. 自动变量auto

函数中的局部变量,如果不专门声明为static存储类别,都是动态地分配存储空间的,数据存储在动态存储区中。函数中的形参和在函数中定义的局部变量(包括在复合语句中定义的局部变量)都属于此类,调用该函数时,系统会自动给这些变量分配存储空间,在函数调用时就自动释放这些存储空间,因此这类局部变量称为自动变量,通过关键字auto进行声明,实际上,该关键字可以省略不写,不写auto则隐含指定为自动存储类别,它属于动态存储方式。

int b,c = 3;auto int b,c = 3;等价
1
2
3
  1. 静态局部变量static

有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原有值,即其占用的存储单元不释放,在下一次调用该函数时,该变量已有值,即上一次函数调用结束时的值,此时应通过static关键字进行声明。

例 7.14 考察静态局部变量的值。

#include<stdio.h>
int main(){
    int f(int n);
    int a = 2;
    for (int i = 0; i < 3;i++) {
        printf("%d\n",f(a));
    }
    return 0;
}
int f(int a){
    auto int b = 0;
    static int c = 3;
    b += 1;
    c += 1;
    return a+b+c;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

运行结果如下所示:

「注」:

  • 对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值,以后每次调用函数时不在重新赋初值而只是保留上次函数调用结束时的值,而对自动变量赋初值,不是在编译时进行的,而是在函数调用时进行的,每调用一次函数重新给一次初值,相当于执行一次赋值语句;

  • 如果在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符\0(对字符变量),而对自动变量来说,它的值是一个不确定值,这是由于每次函数调用结束后存储单元已经释放,下次调用时又重新分配存储单元,而所分配的单元中的内容是不可知的。

例 7.15 求阶乘。

#include<stdio.h>
int main(){
    int fac(int n);
    for (int i = 1; i <= 5; i++) {
        printf("%d!=%d\n",i,fac(i));
    }
    return 0;
}

int fac(int n){
    static int f = 1; 
    f = f * n;
    return f;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

运行效果如下所示:

「注」:静态存储要长期占用内存,故不建议多用静态变量。

  1. 寄存器变量

一般情况下,变量的值是存放在内存中的,当程序中用到哪一个变量的值时,由控制器发出指令将内存中该变量的值送到运算器中,经过运算器进行运算,如果需要存数,在从运算器将数据送到内存存放。

如果有一些变量使用频繁,则为存取变量的值需要花费不少时间,为提高效率,运行将局部变量的值放在CPU中的寄存器中,需要用时直接从寄存器取出参加运算,不必在到内存中去存取,由于对寄存器的存取速度远高于对内存的存取速度,因此这样做可以提高执行效率,这种变量叫做寄存器变量,用关键字register做声明。

然而,由于现在计算的速度越来越快,性能越来越高,因此目前基本不在使用register变量。

# 7.9.3 全局变量的存储类别

全局变量都是存放在静态存储区的,因此它的生存期是固定的,存在于程序的整个运行过程。一般来说,外部变量是在函数的外部定义的全局变量,它的作用域是从变量的定义处开始,到本程序文件的末尾。在此作用域内,全局变量可以为程序中各个函数所引用。

以下是扩展外部变量作用域的三种情况:

  • 在一个文件内扩展外部变量的作用域

通过extern对该变量做外部声明,表示将该外部变量的作用域扩展到此为止。

#include<stdio.h>
int main(){
    extern int variable;
    printf("varibale = %d\n",variable);
    return 0;
}
int variable;
1
2
3
4
5
6
7

运行效果如下所示:

「注」:提倡将外部变量的定义放在引用它的所有函数之前,这样可以避免在函数中多加一个extern声明。

  • 将外部变量的作用域扩展到其他文件

如果一个程序中包含两个文件,在两个文件中都要用到同一个外部变量时,正确的做法是:在任一个文件中定义外部变量,然后在另一文件中有extern对该外部变量做声明即可。

实际上,在编译时遇到extern时,先在本文件中找外部变量的定义,如果找到,就在本文件中扩展作用域,如果找不到,就在连接时从其他文件中找外部变量的定义,如果从其他文件中找到了,就将作用域扩展到本文件,如果再找不到,就按出错处理。

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

有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用,这时可以在定义外部变量时加一个static声明。

static声明一个变量的作用是:

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

# 7.10 关于变量的声明和定义

对变量而言,声明与定义的关系稍微复杂一点,在声明部分出现的变量有两种情况,一种是需要建立存储空间的(如int a;),另一种是不需要建立存储空间的(如extern a;),前者称为定义性声明,或简称定义,后者称为引用性声明。为了叙述方便,把建立存储空间的声明称为定义,而把不需要建立存储空间的声明称为声明。

# 7.11 内部函数和外部函数

函数本质上是全局的,因为定义一个函数的目的就是要被另外的函数调用,如果不加声明的话,一个文件中的函数既可以被本文件中其他函数调用,也可以被其他文件中的函数调用。但是,也可以指定某些函数不能被其他文件调用,根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。

# 7.11.1 内部函数

如果一个函数只能被本文件中其他函数所调用,它称为内部函数,在定义内部函数时,在函数名和函数类型前加上static关键字即可,其定义形式为:

static int fun(int a,int b);
1

# 7.11.2 外部函数

如果在定义函数时,在函数首部的最左端加关键字extern,则此函数是外部函数,可供其他文件调用,C语言规定,如果在定义函数时省略extern,则默认为外部函数。

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

extern int fun(int a,int b)
1

课后题:1、3、4、5、6、9、10、17