以下文章来源于编程珠玑 ,作者守望先生

前言

这些是编程语言中的基本概念,如果你还不是非常明确地清楚标题的问题,并且不知道作用域,链接属性,存储期等概念的具体含义,那么本文你不该错过。为了更加清晰的理解我们的问题,需要先了解三个概念:作用域,链接属性,存储期。

作用域

C语言中,作用域用来描述标识符能够被哪些区域访问。

而常见作用域有以下几种:

  • 块作用域,可见范围是从定义处到包含该定义的块结尾

  • 函数作用域,goto语句的标签就具有函数作用域

  • 文件作用域,从定义处到定义该文件的末尾都可见。定义在函数之外的变量,就具有文件作用域了。

  • 函数原型作用域,从形参定义处到原型声明结束

为了便于说明,我们来看一个例子,就很容易理解了:

/****************************
作者:守望先生
来源:公众号编程珠玑
个人博客:https://www.yanbinghu.com
***************************************/

#include <stdio.h>
int num1 = 222;         //定位在函数外,具有文件作用域
static int num2 = 111;  //定义在函数外,具有文件作用域
int swap(int *a,int *b)//这里的a,b是函数原型作用域
int swap(int *a,int *b)
{
    if(NULL== a || NULL == b)
        goto error;    
    else
    {
        int temp = *a;  //定义在函数内,块作用域
        *a = *b;
        *b = temp;
        return 0;
    }
    //printf("temp is %d\n",temp);   //因为temp具有块作用域,因此在这里不能直接使用
    error://goto语句的标签,函数作用域,因此在前面就可以引用
        {
            printf("input para is NULL\n");
            return -1;
        }
}
int main(void)
{
    printf("num1=%d,num2=%d\n",num1,num2);
    swap(&num1,&num2);  //num1 num2具有文件作用域,可以在main函数中直接使用
    printf("num1=%d,num2=%d",num1,num2);
    return 0;
}

可以看到,error标签具有函数作用域,整个函数内都可见,而temp具有块作用域,因此在大括号外部,不能直接使用它。而num1和num2具有文件作用域,因此main函数可以直接使用它。

链接属性

在《hello程序是如何变成可执行文件的》我们说到了编译的过程,最后一个步骤就是链接。链接属性决定了在不同作用域的同名标识符能否绑定到同一个对象或者函数。或者说,不同作用域的标识符在编译后是否是同一个实体。

c变量有三种链接属性:

  • 外部链接,extern修饰的,或者没有static修饰的具有文件作用域的变量具有外部链接属性

  • 内部链接,static修饰的具有文件作用域的变量具有内部链接属性

  • 无链接,块作用域,函数作用域和函数原型作用域的变量无链接属性

再稍作解释,没有static修饰,且具有文件作用域的变量,他们在链接时,多个同名标识符的变量最终都绑定到同一个实体。而static修饰的具有文件作用域的变量就不一样了,不同文件内,即便标识符名字相同,它们也绑定到了不同的实体。

因此,如果我们希望某个变量或函数只在某一个文件使用,那么使用static修饰是一个很好的做法。

同样的,来看一个例子。

/****************************
作者:守望先生
来源:公众号编程珠玑
个人博客:https://www.yanbinghu.com
***************************************/

#include <stdio.h>
int a = 5;   //文件作用域,外部链接属性,其他文件可通过extern int a的方式使用该文件的a
static b = 6;  //文件作用域,内部链接属性,即便其他文件也有同名标识符,它们也是不同的
int main(void)
{
    int sum = 0 ; //无链接属性
    sum = a + b;
    printf("sum is %d\n",sum);
    return 0;
}

从代码中可以看到,a和b都具有文件作用域,a具有外部链接属性,而b具有内部链接属性,sum具有块作用域,因此无链接属性。

存储期

实际上作用域和链接属性都描述了标识符的可见性,而存储期则描述了这些标识符对应的对象的生存期。存储期,也分下面几种:

  • 静态存储期,程序执行期间一直都在,文件作用域的变量具有静态存储期

  • 自动存储期,它(变长数组除外)从块开始,到块末尾,因此,块作用域的变量具有自动存储期,它在栈中存储,需要显式初始化。

  • 动态分配存储期,即通过malloc分配内存的变量。它在堆中存储,需要显式初始。

  • 线程存储期,从名字可以知道, 它与线程相关,使用关键字_Thread_local声明的变量具有线程存储期,它从声明到线程结束一直存在。

关于初始化,可参考《C语言入坑指南-被遗忘的初始化》。
同样地,我们通过下面的代码来更好地理解存储期:

/****************************
作者:守望先生
来源:公众号编程珠玑
个人博客:https://www.yanbinghu.com
***************************************/

#include <stdio.h>
int num1 = 222;         //静态存储期
static int num2 = 111;  //静态存储期
int add(int a,int b)
{
    static int tempSum = 0;  //静态存储期
    tempSum = tempSum + a + b;
    return tempSum;
}
int main(void)
{
    printf("num1=%d,num2=%d\n",num1,num2);
    int sum = 0;  //自动存储期
    sum = add(num1,num2);
    printf("first time sum=%d\n",sum);//sum = 333
    sum = add(num1,num2);
    printf("second time sum=%d\n",sum); //sum = 666
    return 0;
}

另外,如果我们通过nm命令查看编译出来的程序文件的符号表,我们可以找到num1,num2,tempSum,而没有sum,前者所用的内存数量在编译时就确定了。关于nm命令的使用可以参考《linux常用命令-开发调试篇》。

$ gcc -g -o lifetime lifetime.c 
$ nm lifetime|grep num1
0000000000601038 D num1
$ nm lifetime|
grep num2
000000000060103c d num2
$ nm lifetime|grep tempSum
0000000000601044 b tempSum.2289
$ nm lifetime|
grep sum
$

什么全局变量,局部变量,静态局部变量,静态全局变量

到这里,我们就可以很容易区分上面的变量类型了。实际上这里只是换了一种说法:
全局:具有文件作用域的变量
静态:具有静态存储期或内部链接属性
局部:具有函数或块作用域的变量

因而结合起来,也就很好理解了。

  • 局部变量:函数或块作用域的变量

  • 静态局部变量:函数或块作用域,静态存储期

  • 全局变量:具有文件作用域的变量

  • 静态全局变量:内部链接属性的,具有文件作用域的变量

当然,这仅仅是为了区分它们,这并不是它们的严格定义。更好的方法,是通过代码来理解:

#include <stdio.h>
int num1 = 222;         //全局变量
static int num2 = 111;  //静态全局变量
int add(int a,int b)
{
    static int tempSum = 0;  //静态局部变量
    tempSum = tempSum + a + b;
    return tempSum;
}
int main(void)
{
    printf("num1=%d,num2=%d\n",num1,num2);
    int sum = 0;  //局部变量
    sum = add(num1,num2);
    printf("first time sum=%d\n",sum);//sum = 333
    return 0;
}

总结

本文总结如下:

  • 具有文件作用域的变量具有静态存储期,并且具有链接属性

  • 不希望其他文件访问的文件作用域变量最好使用static修饰

  • static关键字的含义需要结合上下文来理解

  • 如果可以,全局变量应该尽量避免使用,因为它可能带来变量被意外修改

  • 使用动态内存通常比栈内存慢,但是栈内存很有限

参考

https://en.wikipedia.org/wiki/Global_variables

https://en.wikipedia.org/wiki/Local_variable

《C11标准文档》