变量 Static VS. Extern 修饰关键字

该讨论基于C语言标准。

C语言本身并不支持在一个函数中定义另外一个函数。那么一个变量的作用域(scope)就和C语言本身的块结构特点(block structure)紧密联系起来。

所谓块结构block structure简单地将就是通过{}将作用域划分为一个个{}包围的块。使得在块中声明的变量(指automatic variable)生命周期就是其声明开始到函数执行离开该块;另外,在一个内部块中定义的局部变量与外部块中定义的同名变量相互并不影响。

例如:

void sample_function(void) {
    int i = 0, n = 10; /* outter i */
    if (n > 0) {
	    int i;  /* declare a new i */
	    for (i = 0; i < n; i++) {
		    ...
	    }
    }
}

for循环中使用变量i是一个内部块中的局部变量;它和进入函数时定义的变量i没有什么关系。

理解了C语言的block structure特征之后,就比较好理解external variable这个概念了。所谓的外部变量就是在任何的函数之外定义的变量。比如,将上面例子修改一下:

int i; /* external variable i */
void sample_function(void) {
    int i = 0, n = 10; /* outter i */
    if (n > 0) {
	    int i;  /* declare a new i */
	    for (i = 0; i < n; i++) {
		    ...
	    }
    }
}

最外层定义的变量i就是外部变量(external variable)。该外部变量,同样道理,和sample_function(void)中重新定义的两个同名局部变量i是没有关系的,这两个局部变量会将外部变量i隐藏起来,使得在sample_function(void)内部无法使用外部变量i。因此,这其实也说明在里外代码块中使用同名变量并不是一个好的实践。

extern关键字

理解了外部变量(external variable)这个概念,那外部变量和extern关键字;甚至,和静态变量(static variable)以及static关键字有什么联系?

  • 静态变量static variable指的是在该变量一经申明就在内存中常驻;同时,每次改变该变量的值,该值都会保留下来。那么这个概念和通常所说的局部变量是几乎相对的,局部变量每次代码执行到其声明处时会重新申明新的一份,超过其申明的块时就被销毁。
  • 外部变量external variable在内存属性上其实就是static variable。只不过,它定义处为所有函数的外部。
  • extern关键字用来申明外部变量,使得该外部变量可以在该申明之后被使用。
  • static关键字用来申明某个变量的内存属性为静态变量属性。同时,限定该变量的使用为其申明时所处的块范围或者文件范围(若其为外部变量的话)。

结合下面例子来说明这个上面几个结论。例子包括三个文件:

  • stack.h 一个简单的栈结构头文件

      /* contents of stack.h */
    
      #ifndef stack_h
      #define stack_h
    
      #define MAXVAL (100)
    
      void push(double);
      double pop(void);
    
      #endif /* stack_h */
    
  • stack.c 该栈结构的实现

      #include <stdio.h>
      #include "stack.h"
    
      int sp = 0;
      double val[MAXVAL];
    
      void push(double value) {
          if (sp < MAXVAL) {
          	val[sp++] = value;
          }
          else {
          	printf("error: stack full. Can't push %g\n", value);
          }
      }
    
      double pop(void) {
          if(sp > 0) {
          	return val[--sp];
          }
          else {
          	printf("error: stack empty.");
          return 0.f;
          }
      }
    
  • extern.h 用于将外部变量的申明放在一起

      /* contents of extern.h */
      #ifndef extern_h
      #define extern_h
    
      extern int sp;
      extern double val[];
    
      #endif /* extern_h */
    

    由于函数void push(double value)void pop(void)都需要公用这个栈的内部存储元素和现在栈指针(这里并非真正意义上的指针)的位置,所以需要将变量valsp定义为外部变量,方便访问。

如果外部函数同样需要知道这个栈(尽管几乎没有这个必要)内部的数据结构实现,可以引入extern.h这个头文件;就能够使用valsp这两个在stack.c中定义的外部变量。

因此,这里就能理解:extern关键字仅仅是用来在不可以见外部变量定义的情况下,使得该外部变量在它处可用。

所以,在main函数中,如果引入extern.h头文件。就可以可见spval

#include <stdio.h>
#include "stack.h"
#include "extern.h"

int main(int argc, const char * argv[]) {
    push(2.0);
    printf("print stack position - sp:%d \n", sp);
    pop();
    printf("print stack position - sp:%d \n", sp);

    return 0;
}

输出结果:

print stack position - sp:1
print stack position - sp:0

实际上,可以在多个需要使用该外部变量的文件中引入extern.h,也就是多个文件中保留了同样的多份申明;这是完全可以的。

但是,对这两个外部变量的定义只能有一份;在示例中,放在了stack.c文件中。(所谓定义,就是给变量申请分配了内存空间的地方)。如果感兴趣的话,可以试试多份定义编译器会报什么错误。

static关键字

上面例子有一个很大的缺陷:一个栈的内部实现不应该暴露给外部。因此,尽管需要设计spval为全局变量使得他们globally accessible。另外一方面也需要将它们的可视范围限定在stack.c文件中。

这时候,就可以将stack.c中这两个全局变量的定义前(内存属性上也是静态变量)前面加上static关键字。

#include <stdio.h>
#include "stack.h"

static int sp = 0;
static double val[MAXVAL];

... /* code not shown */

在不修改任何其他代码的前提下运行程序:

Undefined symbols for architecture x86_64:
“_sp”, referenced from:
_main in main.o
ld: symbol(s) not found for architecture x86_64

也就是main函数中找不到sp的定义了。(仍然保留了extern.h头文件)。

当然,在一个函数的内部,也可以声明局部变量为静态变量。这个意义是有两层的:

  • 该局部变量在内存表现上成为了静态变量。为保留修改值。
  • 其可见范围为定义的这个块范围中(由于本身就是局部变量,所以这个并没有改变什么)。

关于storage class

关于这个讨论,有另外一个专业术语叫做storage class。下面是摘自The C Programming Language的引文。有兴趣可以去查阅一下。

An object, sometimes called a variable, is a location in storage, and its interpretation depends on two main attributes: its storage class and its type. The storage class determines the lifetime of the storage associated with the identified object; the type determines the meaning of the values found in the identified object. A name also has a scope, which is the region of the program in which it is known, and a linkage, which determines whether the same name in another scope refers to the same object or function. Scope and linkage are discussed in Par.A.11.

A.4.1 Storage Class
There are two storage classes: automatic and static. Several keywords, together with the context of an object’s declaration, specify its storage class. Automatic objects are local to a block (Par.9.3), and are discarded on exit from the block. Declarations within a block create automatic objects if no storage class specification is mentioned, or if the auto specifier is used. Objects declared register are automatic, and are (if possible) stored in fast registers of the machine. Static objects may be local to a block or external to all blocks, but in either case retain their values across exit from and reentry to functions and blocks. Within a block, including a block that provides the code for a function, static objects are declared with the keyword static. The objects declared outside all blocks, at the same level as function definitions, are always static. They may be made local to a particular translation unit by use of the static keyword; this gives them internal linkage. They become global to an entire program by omitting an explicit storage class, or by using the keyword extern; this gives them external linkage.

参考资料