前面的笔记中介绍过在函数内部声明的变量与在函数外部声明的变量不同。
其实这已经介绍了变量作用域的概念,只是你还不知道而已。变量作用域是C语言中的重要部分。
本次将介绍以下内容:
●变量作用域的概念及其重要性
●什么是外部变量,为何要避免使用它们
●局部变量的细节
●静态变量和自动变量的区别
●局部变量和块
●如何选择存储类别
一.什么是作用域
变量的作用域指的是程序中的哪些部分可以访问变量,换句话说,变量在程序中的哪些地方可见。
C语言中提到变量时,可交替使用可访问和可见这两个术语。对于作用域,变量指的是C语言的所有数据类型:简单变量、数组、结构、指针等,还包括由const关键字定义的符号常量。
作用域还会影响变量的生命期( lifetime ) :变量在内存中存活的时间,或者说何时分配和释放变量占用的存储空间。本次先简单地演示什么是作用域,然后再详细探讨可见性和作用域。
1.1:演示作用域
请看程序清单的程序。第5行定义了一个x变量,第11行使用printf()显示x的值,然后调用print_ value() 再次显示x 的值。
注意,并未将x作为参数传递给print_value()函数,该函数在第19行将x作为参数传递给printf()。
输入:
// 解释变量作用域
#include <stdio.h>
int x = 999;
void print_value(void);
int main(void)
{
printf("%d\n", x);
print_value();
return 0;
}
void print_value(void)
{
printf("%d\n", x);
}
输出:
解析:
编译并运行该程序没有任何问题。
现在,稍微修改一下程序, 将x变量的定义移至main()函数中。
新的源代码如下面程序清单所示,x变量的定义在第9行。
输入:
// 解释变量作用域
#include <stdio.h>
void print_value(void);
int main(void)
{
int x = 999;
printf("%d\n", x);
print_value();
return 0;
}
void print_value(void)
{
printf("%d\n", x);
}
输出:
会显示错误未定义标识符x
解析:
在错误消息中,用圆括号括起来的编号是出错的行号。
第19行是在print_value() 函数中调用printf()函数。
这条错误消息指出,编译到第19行时,print_ value()函数中的x变量未定义,也就是说x变量不可见。
但是,第11行调用printf()函数时,并未生成任何错误消息。
这说明在main()中,x变量是可见的。
两个唯一的区别是,x变量的定义位置不同。
移动x的定义便改变了它的作用域。
在程序清单1中,x被定义在main()的外面,因此它是外部变量( external variable ),其作用域是整个程序。
main()函数和print_value() 函数都可以访问x变量。
在程序清单2中,x被定义在main()函数里面,因此它是局部变量( local variable ),其作用域是在main()函数内。
在print_value() 函数看来,x变量并不存在。
因此,编译器会生成一条错误消息。
在详细介绍局部变量和外部变量之前,我们先要理解作用域的重要性。
1.2作用域的重要性
要理解变量作用域的重要性,先回顾一下第5节讨论的结构化编程。
结构化编程把程序分成若干独立的函数,每个函数都执行特殊的任务。
这里的关键是函数独立。为了真正让函数独立,每个函数的变量都不能受其他函数代码的影响。
只有隔离每个函数数据,才能确保函数在完成自身任务时不会被其他函数破坏。
在函数中定义变量,便可“隐藏”这些变量,让程序的其他部分无法访问它们。
然而,并非所有情况都要在函数间完全隔离所有的数据。程序员通过指定变量的作用域能很好地控制数据隔离的程度。
二.创建外部变量
定义在所有函数外面的变量称为外部变量(externalvariable),这意味着也定义在main() 函数外。
到目前为止,程序清单中定义的大部分变量都是外部变量,即位于源代码中main()函数的前面。外部变量有时也称为全局变量。
注意:
如果在声明外部变量时未显式初始化它,编译器会自动将其初始化为0.
2.1外部变量作用域
外部变量的作用域是整个程序。这意味着在程序中,外部变量对main()函数和其他所有函数都可见。
例如,上面第一个程序清单中的x变量就是一个外部变量。当编译和运行该程序时,x变量对于main()函数和print_value()函数都可见。如果在该程序中添加其他函数,x变量也对它们可见(即,可以访问x变量)。
严格地说,外部变量的作用域是整个程序并不准确。应该说,外部变量的作用域是包含该变量定义的整个源代码文件。
如果源代码文件包含了整个程序,则这两种作用域的说法是等价的。
大部分中小型C程序都被包含在一个文件中,目前我所讲的程序清单中的程序便是如此。
然而,程序的源代码也可能包含在多个独立的文件中。
第22节将讲解为何要这样做以及如何做,那时你会明白在某些情况下,需要对外部变量做特殊处理。
2.2何时使用外部变量
虽然本书前面的程序示例都使用外部变量,实际上,很少用到外部变量。这是为什么?
因为在使用外部变量时,就已经违反了结构化编程的核心一一模块化独立原则。模块化独立的思想是,函数中的每个函数或模块都包含为了完成任务所需的所有代码和数据。相对较小的程序(你现在编写的这些程序),这也许不太重要,但是随着学习的深入,在需要编写更大型、更复杂的程序时,过分依赖外部变量会导致一些问题。
那么,何时需要使用外部变量?
如果程序中的大部分函数或所有函数都需要访问某些变量,就让这些变量称为外部变量。
用const关键字定义的符号常量就很适合做外部变量。
如果程序中只有部分函数需要访问一个变量,应将该变量作为参数传递给函数,而不是让它成为外部变量。
2.3 extern关键字
当函数使用外部变量时,最好在函数内使用extern关键字声明该函数。声明形式如下:
extern类型变量名;
类型是变量的类型,变量名是变量的名称。例如,在程序清单1中的main()函数和print_value() 函数中添加x的声明,
如程序清单3所示。
输入:
// 声明外部变量示例
#include <stdio.h>
int x = 999;
void print_value(void);
int main(void)
{
extern int x;
printf("%d\n", x);
print_value();
return 0;
}
void print_value(void)
{
extern int x;
printf("%d\n", x);
}
输出:
解析:
该程序打印x的值两次,第1次在main()函数中(第13行),
第2次在print_value() 函数中(第22行)。
第5行声明并初始化int类型的变量x为999。
第11行和第21行分别声明x为externint。
注意,定义变量和用extern关键字声明变量不同。
前者为该变量预留存储空间,而后者指明了该函数使用的这个变量是定义在别处的外部变量。
其实,本例并不需要使用extern来声明,没有第11行和第21行,程序依然能正常运行。
但是,如果print_value() 函数和x的声明(第5行)分别位于不同代码模块中,
在print_value()函数中声明x时就必须使用extern关键字。
如果移除第5行的声明,编译器在编译时会报错,提示变量未定义或定义在别处(具体内容视编译器而定)。
三.创建局部变量:
在函数内部定义的变量称为局部变量( local variable ),其作用域是它所在的函数。第5节在函数中介绍了如何定义局部变量以及局部变量的优点。编译器不会自动初始化局部变量。如果在声明局部变量时未初始化它,则它的值是未定义的或是垃圾值。在首次使用局部变量之前,必须显式初始化它或为其赋值。
在main()函数中也可以创建局部变量,程序清单2中的x变量就是这种情况。该变量定义在main()函数中,如前面的程序分析可知,它只在main()中可见。
对于循环计数器这样的变量,要使用局部变量。使用局部变量可以将其与程序中其他部分的变量隔离开来。
在程序中,不要把只供少数函数使用的变量声明为外部变量。
3.1静态变量和自动变量
默认情况下,局部变量都是自动变量( automatic variable)。这意味着局部变量在每次调用函数时被创建,在函数执行完毕时被销毁。实际上这说明,定义该变量的函数在两次函数调用期间,不会保留自动变量的值。
假设程序中有一个函数使用局部变量x,而且在第1次调用该函数时,x被赋值为100。
然后该函数将计算结果返回主调函数,稍后再次被调用。
此时,x变量的值是否仍是100 ?
不是的。x变量的第1个实例在完成第1次函数调用时已被销毁。再次调用函数时,会创建一个x变量的新实例,原来的x变量已被销毁。
如何在两次函数调用期间保留局部变量的值?
例如,打印机在打印下一页时,可能需要打印函数把已打印内容的行号发送给它。
要在两次调用期间保留局部变量的值,必须用static关键字定义该变量,如下所示:
void print(int x)
{
static int lineCount;
/*在此处添加代码*/
}
下面程序清单4演示了自动变量和静态局部变量的区别
输入:
// 自动变量和静态局部变量的区别
#include <stdio.h>
void funcl(void);
int main(void)
{
int count;
for (count = 0; count < 20; count++)
{
printf("At iteration %d: ", count);
funcl();
}
return 0;
}
void funcl(void)
{
static int x = 0;
int y = 0;
printf("x = %d, y %d\n", x++, y++);
}
输出:
解析:
该程序的func1()函数(第17~23行)声明并初始化了一个静态局部变量和一个自动变量。
每次调用该函数时,都会在屏幕上显示两个变量的值,并分别将其值递增1 (第22行) 。
main()函数(第4^15行)包含了一个for循环(第8~12行),先打印一条消息,再调用func1()函数(第11行)。
for循环一共迭代20次。
查看输出发现,每次迭代后,静态变量x的值都递增1,因为在每次调用期间都保存了x的值。
而自动变量y在每次调用时都被初始化为0,因此它的值一直是0。
该程序还表明,静态变量和自动变量显示初始化(即,在声明的同时初始化)的处理方式也不同。
函数中的静态变量在第1次调用函数时只初始化一次,程序在后续调用时知道该变量已经被初始化,不会重复初始化它。
因此静态变量仍保留函数退出时的值。而自动变量在每次调用函数时都会被初始化为指定的值。
如果改动程序清单4,在声明时不初始化两个局部变量,第17^23行的func1()函数如下:
void func1 (void) //17行
{
static int x;
int y;
printf("x = &d, y = 8d\n", x++, y++);
}
在编译修改后的程序时,会出现不同的情况。也许无法通过编译,编译器会报告一条错误的消息,指明第22行使用了未初始化的局部变量;或者运行成功,输出的结果中y的值是一个垃圾值。这些情况因操作系统和编译器而异。如果未显示初始化静态变量,编译器会自动将其初始化为0 ;但是编译器不会自动初始化自动变量,你必须显示初始化它。在未初始化之前,局部变量中的值是未定义的垃圾值。使用未初始化的局部变量,将出现无法预知的结果。
在默认情况下,局部变量都是自动变量,因此无需在声明中指明。如果你愿意,也可以在类型关键字前面加上auto关键字,如下所示:
void func1(int y)
{
auto int count;
/*其他代码已省略*/
}
3.2 函数形参的作用域
在函数头的形参列表中的变量具有局部作用域(localscope)。
例如下面的函数:
void func1 (int x)
{
int y;
/*其他代码已省略*/
}
x和y都是局部变量,其作用域是整个func1 () 函数。当然,x的初始值是主调程序传递给函数的值。
可以像使用其他局部变量那样使用x。
因为形参变量的初始值一定是传入的相应实参值,所以不必考虑形参是静态变量还是自动变量。
3.3外部静态变量
使用static关键字也可以将外部变量声明为静态外部变量:
static float rate;
int main( void )
{
/*其他代码已省略*/
}
普通外部变量与静态外部变量的区别在于各自的作用域不同。普通外部变量对于它所在的文件中且在它声明之后的所有函数可见,而且其他文件中的函数也可以使用它;而静态外部变量只能用于它所在的文件中且在它声明之后的所有函数,其他文件中的函数不能使用它
当源代码包含在多个文件中时,这些区别才会显现出来。第22节将介绍相关内容。
3.4寄存器变量
register关键字用于建议编译器将自动变量储存于寄存器,而不是普通内存中。
寄存器是什么?使用它有何优势?
计算机中的中央处理器(CPU) 包含一些被称为寄存器(register )的数据存储位置。实际的数据运算(如加法、除法)就是在CPU的寄存器中进行的。CPU必 须从内存中将数据把至寄存器才能执行一些操作,然后再将数据返回到内存中。在寄存器和内存间移动数据需要一些时间。如果一开始就把某些变量放在寄存器中,操纵数据的速度会更快。
声明自动变量时使用register关键字,可请求编译器把该变量储存在寄存器中。
看下面的例子:
void func1 (void)
{
register int x;
/* 其他代码已省略*/
}
注意是请求,不是告诉编译器。根据程序的需求,寄存器可能无法储存该变量。如果寄存器不可用,编译器将视该变量为普通的自动变量。换句话说,register 关键字是建议,而不是命令。
register存储类别的好处是,为函数频繁使用的变量(如循环中使用的计数器变量)提供极大便利。
register关键字只能用于简单的数值变量,不可用于数组或结构。也不可用于静态或外部存储类别。不能声明一个指向寄存器变量的指针。
编译器经过十几年发展,已经可以最大限度地优化程序代码,似乎没有必要再使用register关键字。本人并不推荐使用register关键字,但是为了看懂以前编写的旧式代码,有必要理解这些。
要初始化局部变量,否则不知道其中包含的值是什么。
即使默认情况下编译器会把外部变量自动初始化为0,仍应该显式初始化它。显式初始化变量可以避免忘记初始化局部变量。
如果某些变量只供少数函数使用,不
要把这些变量都声明为外部变量。
更好的做法是将其作为参数传递给函数不要把非数值变量、结构、数组声明为寄存器变量。
四.局部变量和main()函数:
根据前面介绍的内容,main()函数和其他所有的函数都可以使用局部变量。
严格地说,main()函数与其他的函数一样,操作系统运行程序时首先调用的是main()函数,程序结束时控制将从main()函数返回操作系统。
这意味着定义在main()函数中的局部变量,在程序开始执行时被创建,其生命期是从被创建开始至程序的结束。
但是,静态局部变量的概念是在两次调用main()函数期间其值保持不变,这说不通。因为变量在程序结束时就不存在了,不可能在执行两次程序期间都存在。因此,在main()函数中,自动变量与静态局部变量相同。虽然可以在main()函数中将局部变量定义成静态变量,但实际没什么效果。
请记住,在大多数方面,main() 函数都与其他函数类似。
不要在main()函数中声明静态变量,这样起不到什么作用。
五.如何使用存储类别:
在选择特定变量应使用哪种存储类别时,可参考表1,其中总结了C语言可用的5种存储类别。
1.auto 关键字可选。
2.在函数中要使用extern关键字来声明定义在别处的静态外部变量。
应尽量使用自动存储类别,只在必要时才使用其他类别。下面是一些指导原则:
●对于每个变量,首先考虑自动局部存储类别;
●在除main()以外的其他函数中,如果要在多次调用函数期间保留变量的值,使用静态变量;
●如果程序绝大多数函数或所有的函数都使用某些变量,应将其定义为外部存储类别。
六.局部变量和块:
到目前为止,只讨论了函数中的局部变量。这是使用局部变量的基本方式,除此之外,还可以在程序的任意块(用花括号括起来的部分)中定义变量。在块中声明变量时,必须将声明放在块的开始位置。如程序清单5所示。
输入:
// 在块中定义局部变量的示例
#include <stdio.h>
int main(void)
{
// 在main()中定义一个局部变量
int count = 0;
printf("\nOutside the block, count = %d", count);
// 块开始
{
// 在块中定义一个局部变量
int count = 999;
printf("\nWithin the block, count = %d", count);
}
printf("\nOutside the block again, count = %d\n", count);
return 0;
}
输出:
解析:
在该程序中,在块内部定义的count与在块外部定义的count无关。
第9行定义并初始化int类型的变量count为0。
由于该变量声明在main()的开始位置,因此整个main()函数都可以访问它。
第11行,打印了count变量的值(0 )。
main() 函数中包含一个块(第14~19行),在这个块中定义了另一个int类型的count变量。
第17行将该变量初始化为999,第18行打印块中的count变量的值(999 )。
由于块结束于第19行,
因此第21行打印的是原来main()函数中第9行定义的count变量。
在C语言编程中,这样使用局部变量并不常见,你也很少会用到。
但是,程序员在隔离程序中的问题时,通常会这样做。用花括号将某部分的代码临时隔离,并创建局部变量来帮助查找问题所在。
这样使用局部变量还有一个好处:声明和初始化变量的代码与使用该变量的代码很近,有助于理解程序。
可暂时在块的开始位置创建变量,有助于查出问题所在。
七.小结:
本次涵盖了C语言变量存储类别的作用域和生命期的概念。C语言中的所有变量,无论是简单变量、数组还是结构,都有一个指定的存储类别,用于决定变量的作用域(在程序中何处可见)和生命期(变量在内存中的存活时间)。
对于结构化编程,正确使用存储类别非常重要。在函数中使用局部变量,提高了函数间的独立性。尽量使用自动存储类别的变量,除非有特殊原因需要使用外部或静态变量。