预计阅读本页时间:-
C为变量提供了5种不同的存储模型,或称存储类。还有基于指针的第6种存储模型,本章稍后(“分配内存malloc()和free()”小节)将会提到。可以按照一个变量(更一般地,一个数据对象)的存储时期(storage duration)描述它,也可以按照它的作用域(scope)以及它的链接(linkage)来描述它。存储时期就是变量在内存中保留的时间,变量的作用域和链接一起表明程序的哪些部分可以通过变量名来使用该变量。不同的存储类提供了变量的作用域、链接以及存储时期的不同组合。您可以拥有供多个不同的源代码文件共享的变量、某个特定文件中的所有函数都可以使用的变量、只有在某个特定函数中才可以使用的变量、甚至只有某个函数的一个小部分内可以使用的变量。
您可以拥有在整个程序运行期间都存在的变量,或者只有在包含该变量的函数执行时才存在的变量。您也可以使用函数调用为数据的存储显式地分配和释放内存。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
在分析这5种存储类之前,我们需要研究这些术语的意义:作用域、链接以及存储时期。然后,我们再介绍具体的存储类。
12.1.1 作用域
作用域描述了程序中可以访问一个标识符的一个或多个区域。一个C变量的作用域可以是代码块作用域、函数原型作用域,或者文件作用域。到目前为止的程序实例中使用的都是代码块作用域变量。回忆一下,一个代码块是包含在开始花括号和对应的结束花括号之内的一段代码。例如,整个函数体是一个代码块。一个函数内的任一复合语句也是一个代码块。在代码块中定义的变量具有代码块作用域(block scope),从该变量被定义的地方到包含该定义的代码块的末尾该变量均可见。另外,函数的形式参量尽管在函数的开始花括号前进行定义,同样也具有代码块作用域,隶属于包含函数体的代码块。所以我们迄今为止使用的局部变量,包括函数的形式参量,都具有代码块作用域。因此,下面代码中的变量cleo和patrick都有直到结束花括号的代码块作用域。
在一个内部代码块中声明的变量,其作用域只局限于该代码块:
在这个例子中,q的作用域被限制在内部代码块内,只有该代码块内的代码可以访问q。
传统上,具有代码块作用域的变量都必须在代码块的开始处进行声明。C99放宽了这一规则,允许在一个代码块中任何位置声明变量。一个新的可能是变量声明可以出现在for循环的控制部分,也就是说,现在可以这样做:
作为这一新功能的一部分,C99把代码块的概念扩大到包括由for循环、while循环、do while循环或者if语句所控制的代码一即使这些代码没有用花括号括起来。因此在前述for循环中,变量i被认为是for循环代码块的一部分。这样它的作用域就限于这个for循环,程序的执行离开该for循环后就不再能看到变量i了。
函数原型作用域(function prototype scope)适用于函数原型中使用的变量名,如下所示:
函数原型作用域从变量定义处一直到原型声明的末尾。这意味着编译器在处理一个函数原型的参数时,它所关心的只是该参数的类型;您使用什么名字(如果使用了的话)通常是无关紧要的,不需要使它们和在函数定义中使用的变量名保持一致。名字起作用的一种情形是变长数组参量:
如果在方括号中使用了变量名,则该变量名必须是在原型中已经声明了的。
一个在所有函数之外定义的变量具有文件作用域(file scope)。具有文件作用域的变量从它定义处到包含该定义的文件结尾处都是可见的。看看下面的例子:
这里,变量units具有文件作用域,在main()和critic()中都可以使用它。因为它们可以在不止一个函数中使用,文件作用域变量也被称为全局变量(global variable)。
另外还有一种被称为函数作用域(function scope)的作用域,但它只适用于goto语句使用的标签。函数作用域意味着一个特定函数中的goto标签对该函数中任何地方的代码都是可见的,无论该标签出现在哪一个代码块中。
12.1.2 链接
接下来,让我们看看链接。一个C变量具有下列链接之一:外部链接(external linkage),内部链接(internal linkage),或空链接(no linkage)。具有代码块作用域或者函数原型作用域的变量有空链接,意味着它们是由其定义所在的代码块或函数原型所私有的。具有文件作用域的变量可能有内部或者外部链接。一个具有外部链接的变量可以在一个多文件程序的任何地方使用。一个具有内部链接的变量可以在一个文件的任何地方使用。
那么怎样知道一个文件作用域变量具有内部链接还是外部链接?您可以看看在外部定义中是否使用了存储类说明符static:
和该文件属于同一程序的其他文件可以使用变量giants。变量dodgers是该文件私有的,但是可以被该文件中的任一函数使用。
12.1.3 存储时期
一个C变量有以下两种存储时期之一:静态存储时期(static storage duration)和自动存储时期(automatic storage duration)。如果一个变量具有静态存储时期,它在程序执行期间将一直存在。具有文件作用域的变量具有静态存储时期。注意对于具有文件作用域的变量,关键词static表明链接类型,并非存储时期。一个使用static声明了的文件作用域变量具有内部链接,而所有的文件作用域变量,无论它具有内部链接,还是具有外部链接,都具有静态存储时期。
具有代码块作用域的变量一般情况下具有自动存储时期。在程序进入定义这些变量的代码块时,将为这些变量分配内存;当退出这个代码块时,分配的内存将被释放。该思想把自动变量使用的内存视为一个可以重复使用的工作区或者暂存内存。例如,在一个函数调用结束后,它的变量所占用的内存可被用来存储下一个被调用函数的变量。
迄今为止我们使用的局部变量都属于自动类型。例如,在下列代码中,变量number和index在每次开始调用函数bore()时生成,在每次结束函数调用时消失:
C使用作用域、链接和存储时期来定义5种存储类:自动、寄存器、具有代码块作用域的静态、具有外部链接的静态,以及具有内部链接的静态。表12.1列出了这些组合。现在已经介绍了作用域、链接和存储时期,我们可以详细地讨论这些存储类了。
表12.1 5种存储类
存 储 类 | 时 期 | 作 用 域 | 链 接 | 声 明 方 式 |
---|---|---|---|---|
自动 | 自动 | 代码块 | 空 | 代码块内 |
寄存器 | 自动 | 代码块 | 空 | 代码块内,使用关键字register |
具有外部链接的静态 | 静态 | 文件 | 外部 | 所有函数之外 |
具有外部链接的静态 | 静态 | 文件 | 内部 | 所有函数之外,使用关键字static |
空链接的静态 | 静态 | 代码块 | 空 | 代码块内,使用关键字static |
12.1.4 自动变量
属于自动存储类的变量具有自动存储时期、代码块作用域和空链接。默认情况下,在代码块或函数的头部定义的任意变量都属于自动存储类。然而,也可以如下面所示的那样显式地使用关键字auto使您的这个意图更清晰:
例如,为了表明有意覆盖一个外部函数定义时,或者为了表明不能把变量改变为其他存储类这一点很重要时,可以这样做。关键字auto称为存储类说明符(storage class specifier)。
代码块作用域和空链接意味着只有变量定义所在的代码块才可以通过名字访问该变量(当然,可以用参数向其他函数传送该变量的值和地址,但那是以间接的方式知道的)。另一个函数可以使用具有同样名字的变量,但那将是存储在不同内存位置中的一个独立变量。
回忆一下,自动存储时期意味着在程序进入包含变量声明的代码块时,变量开始存在。当程序离开这个代码块时,自动变量消失了。它所占用的内存可用来做别的事情。
再来仔细看一下嵌套代码块。只有定义变量的代码块及其内部的任何代码块可以访问这个变量:
在这段代码中,变量i仅在内层花括号中是可见的。如果试图在内层代码块之前或之后使用该变量,将得到一个编译错误。通常,在设计程序时不使用这一特性。然而有些时候,如果其他地方用不到这个变量的话,在子代码块中定义一个变量是有用的。通过这种方式,您可以在使用变量的位置附近说明变量的含义。而且,变量只会在需要它时才占用内存。变量n和m在函数头部和外层代码块中定义,在整个函数中可用,并一直存在到函数终止。
如果在内层代码块定义了一个具有和外层代码块变量同一名字的变量,将发生什么?那么在内层代码块定义的名字是内层代码块所使用的变量。我们称之为内层定义覆盖(hide)了外部定义,但当运行离开内层代码块时,外部变量重新恢复作用。程序清单12.1对此进行了示例说明。
程序清单12.1 hiding.c程序
输出如下:
首先,程序创建了一个变量x并为其赋值30,如第一个printf()语句所示。接着定义了一个新的值为77的变量x,如第二个printf()语句所示。第三个printf()语句显示出是一个新的变量覆盖了初始的变量x。该语句位于第一个内层代码块后,显示出起始的x值,表明起始的变量x既没有消失也不曾改变过。
该程序最令人迷惑的部分也许是while循环。这个while循环的判断使用了起始的x:
然而,在循环内部,程序看到了第三个x变量,即在while循环代码块内定义的一个变量。因此,当循环体中的代码使用x++时,是新的x被递增到101,接着被显示。每次循环结束以后,新的x就消失了。然后循环条件判断语句使用并递增起始的x,又进入循环代码块,再次创建新的x。在本例中,新的x创建和消亡了3次。注意,该循环必须在条件判断语句中递增x,因为若在循环体内递增x的话,递增的将是另一个x而非判断所用的那个x。
这个例子并不是要鼓励您写类似的代码,而是举例说明在一个代码块中定义变量时将会发生什么。
一、不带{}的代码块
先前曾提到C99有一个特性,语句若为循环或者if语句的一部分,即使没有使用{},也认为是一个代码块。更完整地说,整个循环是该循环所在代码块的子代码块,而循环体是整个循环代码块的子代码块。与之类似,if语句是一个代码块,其相关子语句也是if语句的子代码块。这一规则影响到您能够在何处定义变量以及该变量的作用域。程序清单12.2显示了在一个for循环中该特性是如何作用的。
程序清单12.2 forc99.c程序
输出如下,假设编译器支持这一特定的C99特性: