C语言中的extern,static,auto,register

今晚继续在阅读Skynet的源代码时,发现了多处static关键词和extern关键词。读着读着,突然发现自己对static和extern的概念和作用有些模糊,所以索性花点时间重温static、extern以及和其作用类似的auto、register。接下来,将对extern、static、auto、register的概念和作用做一个总结,在此之前,先介绍几个前导概念。 什么是翻译单元?翻译单元一般可认为就是编译单元,而编译单元可以粗略地认为就是C编译器预处理之后的单个源文件。 什么是函数或变量声明(declaration)?声明一个函数或变量意味着,编译器知道这里有指定的一个标识符,然后编译器可以使用此标识符,但是函数或变量一定要在某处定义,否则虽然编译时不会报错,但是链接时就会找不到函数或变量定义。 什么是函数或变量定义(definition)?定义一个函数或变量意味着,编译器要在定义时为函数或变量分配内存。声明时,并不会分配内存,只是告诉编译器:“喂,我是XXX啦,我可以被你使用了,但是我可能定义在别处。”。 声明和定义有以下规则:函数或变量声明可以有多处,它的作用仅仅是告诉编译器其可以被使用,但是其定义只能有一处,因为同一个函数或变量只会有一个。如果你定义同一个函数或者变量多次,编译器就会报告重复定义的错误信息。 变量按作用域划分:

  • 全局变量
  • 局部变量

一个全局变量意味着,它必须声明在函数之外,它可以被所有其他函数访问。但是当static修饰全局变量时,此时全局变量只能被当前翻译单元内的所有函数访问)。 一个局部变量意味着,它必须声明在函数内或者语句块中,它只能在声明的函数或者语句块中被访问。 变量按生命周期划分:

  • 程序执行期间存活的变量
  • 函数执行期间存活的变量
  • 语句块执行期间存活变量

函数按作用域划分:

  • 外部函数(声明函数的默认策略,程序内都可以访问)
  • 内部函数(一个翻译单元内能访问)

因为标准C并不支持内嵌函数,所以一般不考虑函数的生命周期。只要程序仍然在执行,函数就活着。 extern、static、auto、register是C语言用来表示函数或者变量的存储策略的4个关键词。他们对函数和变量的影响,如下表:

存储策略

函数或变量的生命周期

函数或变量的作用域

extern

程序执行期间

整个程序内

static

程序执行期间

函数:翻译单元内
全局变量:翻译单元内
局部变量:函数或者语句块中

auto

函数或语句块执行期间

只能修饰变量。函数或者语句块中。

register

函数或语句块执行期间

只能修饰变量。函数或者语句块中。

接下来我们对其一一说明。 首先看extern关键词。extern的使用并不是那么频繁。对于函数来说,默认声明函数时就是外部函数。

int sum(int);
extern int sum(int);

上述两个函数声明是等价的。 对于全局变量来说就有些不一样了(在函数外定义)。

extern int b;//仅仅是声明b,并不会为b分配内存
int b;//定义b,为b分配内存,默认值是0

执行如下代码:

#include <stdio.h>
extern int b;
int main() {
    b = 10;
    printf("%d\n", b);
    return 0;
}

编译器会报告类似“/home/yangyuan/training/C/main.c:4:对‘b’未定义的引用”的错误。原因就是,extern关键词仅仅声明b是一个外部变量,它的定义在其他某处。编译器都没有为b分配内存,你又如何能够访问b呢?执行如下代码:

#include <stdio.h>
int b;
int main() {
    printf("%d\n", b);
    return 0;
}

当我们去掉extern关键,int b;就由声明变成了定义。所以此代码可正常执行。我们继续看一段代码:

#include <stdio.h>
extern int b = 10;
int main() {
    printf("%d\n", b);
    return 0;
}

这段代码可以执行,并得到输出结果10。但是编译器会给出警告“warning ‘b’ initialized and declared ‘extern’”,即一个声明为extern的变量却被初始化了,严格来讲这是错误的,但是编译器采用的策略是发出警告。 接下来看static关键词。当static修饰函数声明或定义时,其表明被修饰的函数只能在同一翻译单元内被访问。这样做,可以做一定程度上的封装和隐藏(不是完全的封装和隐藏,下文会解释),避免和其他翻译单元发生名字冲突。当static修饰变量声明或定义时,就得分情况讨论:

  • static global variable(静态全局变量)
  • static local variable(静态局部变量)

当static修饰的是全局变量时(定义在函数外),其表明此变量只能在同一翻译单元内被访问。当static修饰的是局部变量时,其声明周期和全局变量一致(存活于程序执行期间),但是其作用域仍然和普通的局部变量一样。只能在其声明的函数或语句块内被访问。比如:

#include <stdio.h>

void func() {
    static int a = 1;
    printf("%d\n", ++a);
}

int main() {
    func();
    a++;
    return 0;
}

执行上述代码,编译器就会报告错误“‘a’ undeclared (first use in this function)”。 上文说到,static仅仅能做一定程度上的封装和隐藏。我们仍然能够通过指向被static修饰的函数或变量的指针来访问函数或变量。 再看auto关键词。这个关键词,很少使用。在C++的新标准中有其新的含义。在C语言中,它表示变量是一个自动变量,自动变量从其定义时生,从离开其作用域时死。可以粗略地认为auto修饰的变量就是局部变量,但是别忘了,局部变量被static修饰时,其作用域不变,但是其生存周期已和程序一致。所以,auto变量的范围小于局部变量。当然了,我们使用的最多的就是auto变量,局部变量默认就是auto变量,auto只是隐藏在幕后。

void func() {
    auto int a = 1;
    printf("%d\n", ++a);
}
void func() {
    int a = 1;//auto关键词被省略了
    printf("%d\n", ++a);
}

上述两个函数中定义的变量a是等价的。 最后看register关键词。这个关键词,几乎都被快被我们遗忘了,甚至C在过去的一段时间里都废弃过这个关键词,直到C17才重新启用这个关键词。一般而言,我们很少会使用到该关键词。在变量声明时修饰变量暗示C编译器将此变量存储在寄存器或者其他能快速存取的地方,而不是放入内存中。但是,C编译器不一定会将你用register修饰的变量存储在寄存器中,而是直接放在内存中,这是C编译器的自由。前文说了,register关键词只是暗示C编译器,到底怎么做仍然是C编译器的自由。当你使用register修饰一个变量的时候,这个变量的地址是无法被访问的,这是C编译器对变量的一种保护,你可以这样理解:存储在寄存器中的变量都不存在于内存中,怎么会有地址呢?如下代码:

#include <stdio.h>
int main() {
    register int a = 1;
    printf("%p\n", &a);
    return 0;
}

运行上述代码,一般会出现类似“error address of register variable ‘a’ requested”这样的报错信息。 我们探究这些语言的细节,并不是为了成为语言律师,而是为了对其作用做到了然于胸。比如你对static的作用不了解,那么你在阅读开源C代码时,就很有可能遇到障碍,会让你感到confused。探究语言细节,但也不能咬文嚼字,在语言细节上玩一些文字游戏,这是得不偿失的,因为我们最终的目的是更好地阅读和编写代码,而不能本末倒置。