0%

深入理解static关键字(1)

static关键字是C和C++中很重要的一个关键字,初学者往往搞不清楚这个关键字的真正含义。很多人把这个关键字与变量作用域混为一谈,这种认识是严重错误的!static确实跟变量的作用域有一些关系,但是这两者并不是一回事。这篇文章来探讨一下static关键字的含义,首先放结论:

static用于修改标识符(变量或者函数)的链接属性或者存储类型!

static用于修改标识符(变量或者函数)的链接属性或者存储类型!

static用于修改标识符(变量或者函数)的链接属性或者存储类型!

(重要的事情说三遍)

那么链接属性、存储类型又是什么鬼?跟作用域有什么关系呢?

1. 链接属性

先来看链接属性是什么意思。当组成一个程序的各个源文件分别被编译之后,所有的目标文件以及那些从一个或多个函数库中引用的函数链接在一起,形成可执行程序。然而,如果相同的标识符(可以理解为函数或者变量)出现在几个不同的源文件中时,它们是表示同一个变量或函数,还是表示不同的变量或函数呢?标识符的链接属性(linkage)就是用于决定如何处理在不同文件中出现的同名标识符。

链接属性有3种,external(外部的,这个你一定听说过)、internal(内部的)和none。none链接属性其实就是没有链接属性,这种标识符总是被当做单独的个体,也就是说该标识符的多个声明被当作独立不同的实体。

属于internal属性的标识符其链接属性被现在在文件内部,也就是说在该标识符同一个源文件内的所有声明中都指同一个实体,但位于不同源文件的多个声明则分属不同的实体。

属于external属性的标识符不论声明多少次、位于几个源文件,通通都表示同一个实体。

在函数外面定义的变量和函数本身的链接属性都是external。局部变量的链接属性是none。

先来看一个例子:

1
2
3
4
5
6
7
8
// test.c
int a;
int add(int a1, int a2)
{
int tmp;
int func(int a,int b); // 这是声明func函数,其定义在别的源文件中
return tmp;
}

在这段代码当中,标识符a的链接属性是external,在别的文件中可见(不过要使用它必须声明)。如果另一个源文件中包含了类似int a或者extern int a的声明,实际上他们引用的都是test.c源文件中定义的a。如果在别的源文件中写int a = 20这样定义a(注意,这里是定义不是声明),这是非法的,因为a已经在test.c文件中定义过一次了,这里再定义一次会造成a标识符重定义,编译时编译器会报错。

标识符add, func的链接属性是也是external,在别的文件中可见(要使用也要声明)。因此在其他源文件中包含int add(int a1, int a2)或者extern int add(int a1, int a2)这样的声明,那么在调用时会调用到test.c文件中定义的add函数。当然我们一般不会在其他源文件中这样写,一般把函数声明全写在头文件中,然后在源文件里面include。同样的如果在别的源文件中再次定义add、func函数,就会造成函数重定义。

上面这段代码中其他的所有标识符(包括a1,a2,a,b,tmp)全部都是none,无链接属性。a1、a2、tmp只能在add函数内使用,a、b只能在func函数内部使用。

为了加深印象,我们再来看一个例子:

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
// a.c文件内容
#include<stdio.h>
int a = 10;
int add(int a1, int a2)
{
return a1 + a2;
}
void print()
{
printf("a = %d\n", a);
}

// b.c文件内容
#include<stdio.h>
extern int add(int a1, int a2);
extern void print();
extern int a;
// int a = 30; 这样写造成a重定义了,编译不过
int main()
{
int ans = add(1,2);
int a = 20; // 定义了一个局部变量a,此时全局的a被在main函数体内被屏蔽了
print(); // 打印全局a的值
printf("a = %d\n", a); // 打印局部a的值
printf("%d\n", ans);
return 0;
}

这个程序的执行结果是:

1
2
3
4
$ ./a.exe
a = 10
a = 20
3

在a.c文件中定义了一个变量a,两个函数函数add和print。这三个标识符的链接属性都是external,这个在前面已经讲过。在b.c文件中,首先声明了函数add和print和变量a。这里三个extern都可以不写,但是为了避免阅读者误解,尤其是a的声明很容易被当成定义,这里还是把extern写上。

在main函数里面我们又定义了一个局部变量a。在main函数的作用域内,全局变量a被局部变量a屏蔽掉了,所以下面printf语句打印出来的是局部变量a的值。在print函数的作用域内,局部变量a不可见,但全局变量a是可见的,因此打印出来的是全局变量a的值。

啰嗦了这么半天貌似也没见提到static!别着急,下面马上进入static的讲解!

2. static关键字修改链接属性

关键字static和extern一样,是用于在声明中修改标识符的链接属性。 如果某个声明在正常情况下具有extern链接属性,在它前面加上static可以使它的属性变为internal。 比如上面那段代码改写成这样:

1
2
3
4
5
6
7
8
// test.c
static int a;
int add(int a1, int a2)
{
int tmp;
int func(int a,int b);
return tmp;
}

那么a变量变为test.c文件私有,在其他文件中不能被链接到,因此不可以访问。如果在别的文件中也访问到一个a变量,那么它引用的是肯定是另外一个a变量,反正不是test.c里面的a。

类似的,函数前面也可以加上static,把函数的链接属性从默认的external改为internal,那么这个函数只能在文件内部使用,因为在别的文件里面不能链接到这个函数。

注意:static只对默认链接属性为external的声明才有作用。如果在上面代码中tmp前加上static,并不是说tmp变量在文件内就可以随便访问了,因为tmp默认是没有链接属性的。那么在tmp前面加static是啥意思呢?这个就涉及到第二个问题,变量的存储类型。

3. 变量的存储类型

变量的存储类型是指存储变量值的内存类型。变量的存储类型决定变量何时创建、何时销毁以及它的值将保持多久。通常有三个地方可以用于存储变量:普通内存、运行时堆栈、硬件寄存器。在这三个地方存储的变量具有不同的特性。

变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态(static)变量。对于这类变量,你无法为它们指定其他存储类型。静态变量在程序运行之前创建,在程序的整个执行期间始终存在。它始终保持原先的值,除非给它赋一个不同的值或者程序结束。

在代码块内部声明的变量的缺省存储类型是自动的(automatic),也就是说它存储于堆栈中,称为自动(auto)变量。关键字auto就是用于修饰这种存储类型的,但它基本没卵用,谁也不会在定义一个局部变量时加上auto,因为代码块中的变量在缺省情况下就是自动变量。

在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开该代码块时,这些自动变量便自行销毁。如果该代码块被多次执行,例如一个函数被反复调用,这些自动变量每次都将重新创建。在代码块再次执行时,这些自动变量在堆栈中所占据的内存位置有可能和原先的位置相同,也可能不同。即使它们所占据的位置相同,你也不能保证这块内存同时不会有其他的用途。因此,我们可以说自动变量在代码块执行完毕后就消失。当代码块再次执行时,它们的值一般并不是上次执行时的值。

还有一种变量是存储在寄存器当中的。C语言中的关键字register可以用于自动变量的声明,提示它们应该存储于机器的硬件寄存器而不是内存中,这类变量称为寄存器变量。通常,寄存器变量比存储于内存的变量访问起来效率更高。但是这个关键字也只是对编译器的一个建议,就像inline关键字一样,编译器并不一定要理睬register关键字。如果有太多的变量被声明为register,它只选取前几个实际存储于寄存器中,其余的就按普通自动变量处理。如果一个编译器自己具有一套寄存器优化方法,它也可能忽略register关键字,其依据是由编译器决定哪些变量存储于寄存器中比人脑的决定更为合理一些。所以这个关键字其实基本没什么用处,在实际开发中很少有人会在自动变量前面加register关键字。

4. static修改自动变量的存储类型

现在回到刚才的问题,在局部变量(也就是自动变量)tmp前面加上static是什么意思? 答案是:对于在代码块内部声明的变量,如果给它加上关键字static,可以使它的存储类型从自动变为静态。

也就是说,一个局部变量(也就是自动变量)前面如果没有加static,那么它是存储在栈中的;加了static之后,它是存储在静态内存区。这也就可以解释为什么出了函数之后,static变量还是可以保持原有的值。具有静态存储类型的变量在整个程序执行过程中一直存在,而不仅仅在声明它的代码块的执行时存在。但是要注意的一点是,修改变量的存储类型并不表示修改该变量的作用域。一个代码块内的静态变量,虽然它在整个程序的生命周期都存在,但是它仍然只能在该代码块内部按名字访问(这句话很重要,许多初学者在这里翻车)。

另外,还有一点需要注意:函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。下面看一个小例子:

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

这段代码是不能编译通过的,因为cnt变量的作用域被限制在for循环内的{}内部,在{}外面是不能访问,相当于没有定义。看一下编译结果:

1
2
3
4
5
6
$ gcc -g a.c
a.c: In function 'main':
a.c:11:30: error: 'cnt' undeclared (first use in this function)
printf("cnt = %d\n", cnt);
^~~
a.c:11:30: note: each undeclared identifier is reported only once for each function it appears in

果然,gcc编译器给出的结果是cnt没有定义。

4.总结

现在可以做一下阶段性的总结了。

  1. C语言中static关键字的作用有两个1.修改连接属性;2.修改变量的存储类型。具体起什么作用需要根据代码上下文来确定。
  2. 函数以及在代码块外面定义的变量(即为全局变量),其缺省链接属性为external。
  3. 在缺省属性为external的标识符前面加static是将该标识符的链接属性限制为internal,此时只能在源文件内部可以链接到该标识符。
  4. 对于缺省属性为none的,在代码块内部声明的自动变量,前面加上static关键字将变量的存储类型从自动改为静态,也就是把变量的存储位置从栈改为静态存储区。
  5. 函数的形参不能声明为static,因为实参总是在堆栈中传递给函数,用于支持递归。
    以上就是C语言中static关键字的含义总结。

    5.参考文献

  6. 《C和指针》