概述

C语言中的变量可以大致分为4种:

  • (非静态)局部变量
  • 静态局部变量
  • (非静态)全局变量
  • 静态全局变量

变量的类别由定义的位置和修饰词决定,例如

1
2
3
4
5
6
7
8
int s_global = 10; // 全局变量
static int s_global_static = 20; // 静态全局变量

void fun(){
int s = 100; // 局部变量
static int s_local_static = 200; // 静态局部变量
// ...
}

变量的类别决定了它们具有完全不同的性质。

与变量不同,C语言中的函数大致只需要分成两种:

  • 普通函数
  • 静态函数

函数的类别取决于是否使用了static修饰,不同类别的函数也具有不同的性质。

下面会从不同的角度对变量和函数的类别进行理解。

进程的内存分区

在启动一个可执行程序时,系统会创建一个进程并为其分配一个内存空间,其中包括进程执行所涉及的所有指令和数据, 大致分为如下几个区域:

  • 文本区(Text Segment):存放程序的具体代码指令(包括函数),这块区域在程序运行期间是只读的
  • 数据区(Data Segment):
    • 全局/静态存储区:存放所有的全局变量/静态变量,它们的生命周期是整个程序运行期,对于已初始化和未初始化的变量会分开存放(C++好像又不区分了)
    • 常量区:用来存放常量值,如字符串常量等,这块区域是只读的
  • 栈区(Stack Segment):函数调用栈随着函数调用而动态变化,其中主要存储着所有的非静态局部变量,还有函数调用时的传参以及返回地址信息
  • 堆区(Heap Segment):程序通过malloc等函数申请的动态内存位于堆区中,它们不会被自动释放,必须手动通过free函数释放,或者在程序结束时由系统统一进行回收

补充:

  • 对于未初始化的全局变量/静态变量,程序会默认赋值为0。
  • 在堆区和栈区之间预留了一块较大的空间,堆和栈从两端到中间增长,中间区域也被用于加载共享库。
  • 如果在编译时附带了调试选项,那么程序在内存中也会单独分配相关的空间存储调试需要的额外信息。

变量的存储位置与生命周期

从变量在内存中的存储位置可知,变量明显被分成了两类:(存储位置直接决定了变量的生命周期)

  • 第一类:存储在数据区,变量的创建时机为进入main函数之前或第一次调用时,销毁时机为退出main函数之后;
  • 第二类:存储在栈区,变量的创建和销毁时机由函数调用栈决定,调用函数后就会创建其中的变量,函数退出后,对应的栈空间被回收

这种分类是由全局和静态的语义决定的:这两个语义都意味着变量的存在不会受到函数调用栈的任何影响,因此必须划分一个单独区域存储它们。具体来说:

  • 第一类变量包括:非静态的全局变量,静态全局变量,静态局部变量;
  • 第二类变量包括:非静态的局部变量。

变量的初始化

局部变量如果没有显式初始化,在内存中的值是随机的,这是非常危险的。在debug模式下可能会对未显式初始化的局部变量执行默认初始化,将其设置为零值,但是release模式则不会。

全局变量的初始化则比较复杂:

  • 程序保证在程序进入main函数之前完成所有全局变量的初始化;
  • 对于在单个源文件中的定义的所有全局变量,初始化顺序与在源代码中的顺序保持一致,但是在不同源文件中的全局变量的初始化顺序不确定。
  • 如果没有显式初始化,程序也会对它执行默认初始化,默认初始化通常设置为零值:对数值类型为零,对指针类型为 nullptr,对布尔类型为 false;(在C++中,对自定义类型的全局变量也会尝试使用其默认构造函数进行初始化)

静态局部变量是定义在函数体内部的变量

1
2
3
4
5
int func(){
static int s = 0;
s++;
return s;
}

它的初始化只发生在第一次进入函数并执行初始化语句时,此后该变量的值会一直保持,不会受到函数调用栈的影响, 再次进入函数体时,程序会自动跳过初始化语句。

链接属性

对于不同的编译单元,它们可能会共享某些标识符,此时一个编译单元负责定义,另一个编译单元只需要声明即可使用, 在链接阶段会负责处理跨单元的标识符使用(如果出现问题会报错符号重定义或符号未定义), 由此自然引出了标识符的链接属性问题。

链接属性有三种:

  • external:外部链接,代表不同编译单元中出现的多个同名称标识符将指向同一个实体,这意味着允许跨单元使用。
  • internal:内部链接,代表仅在当前编译单元内该标识符会指向同一个实体,不同编译单元中的同名标识符会指向不同的实体,这意味着不允许跨单元使用。
  • none:无链接

默认情况下,标识符的链接属性与其出现的位置有关:

  • 全局变量和所有的函数默认的链接属性为external
  • 其余标识符的默认链接属性为none

对原本默认链接属性为external的标识符,可以使用static关键字改变其链接属性为internal

  • 对于全局变量:
    • 默认链接属性为external,对应为非静态的全局变量
    • 使用static将链接属性改为internal,对应为静态全局变量
  • 对于函数:
    • 默认链接属性为external,对应为普通函数
    • 使用static将链接属性改为internal,对应为静态函数

因此,静态全局变量和静态函数的核心语义就是对文件外部不可见,如果在头文件中定义静态全局变量和静态函数,会导致引用该头文件的每一个编译单元都持有一份独立的实体。

例如

1
2
3
4
5
static int s = 100;

static void func(){
// ...
}

static关键词在C语言中有两种语义:对于静态全局变量和静态函数,语义是修改链接属性,对文件外部不可见;对于静态局部变量,语义是延长生命周期。

如果在当前编译单元需要使用某个不在当前编译单元定义的外部标识符,需要使用extern修饰的声明,例如

1
2
extern int s;
extern void func();

其中:

  • 对于全局变量来说,extern出现在声明中是必要的,否则在当前编译单元会将其自动转换为变量定义,无论是否提供了初始值;
  • 对于函数来说,extern可以省略,因为声明缺少函数体,编译器不会将其转换为函数定义。

例如下面的多文件编译无法成功链接

1
2
3
4
5
6
7
8
9
10
11
12
// func.cpp
static void func();

void func() {} // static

// main.cpp
extern void func();

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

static关键词是阻止链接的关键,将其移除之后多文件编译就会顺利链接

1
2
3
4
5
6
7
8
9
10
11
12
// func.cpp
void func();

void func() {}

// main.cpp
extern void func();

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

补充:

  • 在C++中,const的全局变量默认的链接属性为internal,但是C语言中不是如此。
  • 如果函数既有定义也有声明,static关键词只需要加在前面的函数声明中。(但是对于静态全局变量,声明和定义都需要加上static

变量的作用域

根据定义位置,可以把变量分为全局的和局部两大类:

  • 独立于所有的函数体之外定义的变量称为全局变量
  • 在某个函数体内部定义的变量称为局部变量

每一个变量都必须从属于一个作用域,作用域代表了变量可被访问的范围(当然还要在变量的定义语句之后才会生效),作用域也由定义位置决定:

  • 全局变量属于全局作用域;
  • 静态全局变量属于当前文件作用域;
  • 在函数中定义的局部变量(静态局部变量)属于函数作用域;
  • {}包裹的代码块(包括循环,条件语句)中定义的局部变量(静态局部变量)属于单独的块作用域

不同的作用域会产生嵌套,这种嵌套关系也是由定义位置决定的,注意函数调用不会产生作用域的嵌套。

同一个作用域中的变量不允许重名,但是不同作用域中的变量是允许重名的,分别对应不同的实体。 同名的内层变量会遮蔽外层变量,读写操作只能作用于内层变量上。