Skip to main content

C 作用域、存储期

1. 作用域

当变量在某个部分被声明时,它只有在程序的一定区域才能被访问。这个区域由标识符的作用域决定。标识符的作用域就是程序中该标识符可以被使用的区域。例如,函数的局部变量的作用域仅限于该函数,其他函数无法通过这个名字去访问它们。只要标识符分属不同的作用域,就可以起不同的名字。

所以C 语言中有三个地方声明变量。

C 语言编译器可以确认4种不同类型的作用域——代码块作用域、文件作用域、原型作用域、函数作用域。

1.1 变量的作用域规则

  • 局部变量:在函数或块内部的变量。
    • 它们只能被该函数或该代码块内部的语句使用,局部变量在函数外部是不可知的。如果在该函数内的局部变量和全局变量的名字相同,则优先使用局部变量值。
  • 全局变量:在所有函数外部的变量。
    • 通常定义在程序的顶部,全局变量在整个生命周期内都是有效的,在任意的函数内部都可以访问到全局变量。
    • 与局部变量不同,如果不对全局变量进行初始化,它自动初始化为0,不会开辟空间。
  • 形式参数:在函数参数定义中的变量。
    • 函数的参数,被当作该函数内的局部的变量,如果和全局变量同名,它们会被优先使用。

1.2 代码块作用域(block scope)

代码块就是位于一对花括号{}之间的所有语句。任何在代码块的开始位置声明的标识符都具有代码块作用域,表示它们可以被这个代码块的所有语句访问。

当代码块处于嵌套状态时。声明于内层代码块的标识符的作用域到达该代码块的尾部就会终止。

注意:函数的形式参数也具有代码块作用域。

int function(int n)
{
int a, b, c;
...
{
int a; // 这个变量a跟上面的变量a是两个,它的生命周期只限于这个代码块,但是在该代码块内它会被优先使用
}
}

1.3 文件作用域(file scope)

任何在代码块之外声明的标识符都具有文件作用域,它表示这些标识符从它们的声明之处直到它所在的源文件结尾处都是可以访问的。

在文件中定义的函数名也具有文件作用域,因为函数名本身不属于任何代码块。

在头文件中用 #include 指令包含到其他文件中的声明就好像它们是直接写在那些文件中一样。它们的作用域并不局限于头文件的文件尾。

1.4 原型作用域(prototype scope)

原型作用域只适用于那些在函数原型中声明的参数名。

函数在声明的时候可以不写参数的名字(但参数类型是必须要写上的),其实多尝试你还可以发现,函数原型的参数名还可以随便写一个名字,不必与形式参数相匹配(当然,这样做毫无意义)。允许你这么做,只是因为原型作用域起了作用。

1.5 函数作用域(function scope)

函数作用域只适用于 goto 语句的标签,作用将 goto 语句的标签限制在同一个函数内部,以及防止出现重名标签。

2. 链接属性

简单的来说,编译器会将源文件变成可执行程序需要经过两个步骤:编译和链接。

编译过程主要是将写的源代码生成机器码格式的目标文件,而链接过程则是将相关的库文件添加进来(比如在源文件中调用了 stdio 库的 printf 函数,那么在这个过程中,就会把 printf 函数的代码添加进来),然后整合成一个可执行程序。

在 C 语言中,链接属性一共有三种:

  • external(外部的)-- 多个文件中声明的同名标识符表示同一个实体。
  • internal(内部的)-- 单个文件中声明的同名标识符表示同一个实体。
  • none(无)-- 声明的同名标识符被当作独立不同的实体(比如函数的局部变量,因为它们被当作独立不同的实体,所以不同函数间同名的局部变量并不会发生冲突)。

默认情况下,具备文件作用域的标识符拥有 external 属性。也就是说该标识符允许跨文件访问。对于 external 属性的标识符,无论在不同文件中声明多少次,表示的都是同一个实体。

使用 static 关键字可以使得原先拥有 external 属性的标识符变为 internal 属性。

注意:

  • 使用 static 关键字修改链接属性,只对具有文件作用域的标识符生效(对于拥有其他作用域的标识符是另一种功能)
  • 链接属性只能修改一次,也就是说一旦将标识符的链接属性变为 internal,就无法变回 external 了。

3. 存储类

存储类定义C 程序中变量/函数的范围(可见性)和生命周期。这些说明符会放置在它们所修饰的类型之前。

C 程序中可用的存储类:

  • auto
  • register
  • static
  • extern

3.1 auto 存储类

auto 存储类是所有局部变量默认的存储类。auto 只能使用在函数内,即auto只能修饰局部变量。

{
int mout;
auto int month;//这两个变量的存储类型是一样的
}

3.2 register

register 存储类用于定义存储在寄存器中而不是RAM中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个字),且不能对它应用一元的 ‘&’运算符(因为它没有内存位置)。

register int miles;

寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 'register' 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。

3.3 static 存储类

  • static 可以更改变量存储的位置以及生命周期。

  • static 修饰局部变量可以在函数调用之间保持局部变量的值。

  • static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。

  • static 修饰函数时,会使函数的作用域限制在声明它的文件内。

3.4 extern 存储类

  • **extern **对于无法初始化的变量会把变量名指向一个之前定义过的存储位置。
  • extern 可以用来在另一个文件中声明一个全局变量或函数。

4. 作用域、链接属性和存储类型总结

变量类型声明的位置是否位于堆栈作用域如果声明为static
全局所有代码块之外从声明处到文件尾不允许从其他源文件访问
局部代码块起始位置整个代码块变量不再存储于堆栈中,它的值会在程序整个执行期一直保持
形式参数函数头部整个函数不允许