VectorLu

C 语言存储类详解

如果对以下问题有疑惑,不妨看看全文。

  1. 在编写多文件的 C 语言程序时,在 C 源文件中使用了同名的变量时,应该如何保护有些变量不被意外修改?
  2. 究竟需要初始化变量吗?什么时候,应该如何,初始化什么种类的变量?
  3. 静态变量 static variable 中的静态究竟指什么?是指值不能改变吗?
  4. 一个 C 语言变量会存放在哪里,可以指定它的存放位置吗?
  5. 为什么有些算法书建议,将很大的数组放在所有函数之外声明?

由于要写 C 语言编译器,整理一下 C 语言中不太清楚的知识。

C 语言中有 5 个作为存储类说明符 (storage class specifier) 的关键字,它们是auto register static extern以及typedef。

本文主要摘自 《C Primer Plus》第 12 章。

存储类、链接和内存管理

C 使用作用域、链接和存储时期来定义 5 种存储类,首先来认识一下这三个术语。

作用域

——程序中可以访问一个标识符的一个或多个区域。

代码块作用域 block scope

在代码块(一对花括号中包含的代码)中定义的变量具有代码块作用域,从该变量被定义的地方到相应代码块的结尾,该变量均可见。

另外,函数的形式参量尽管在函数的开始花括号前进行定义,同样也具有代码块作用域,隶属于包含函数体的代码块。

下列代码中变量 cleo 和 patrick 都有直到结束花括号的代码块作用域。

1
2
3
4
5
6
double blocky(double cleo)
{
double patrick = 0.0;
...
return patrick;
}

在一个内部代码代码块中声明的变量,其作用域只局限于该代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
double blocky(double cleo)
{
double patrick = 0.0;
int i;
for (i = 0; i < 10; i++)
{
double q = cleo * i; // q 作用域的开始
...
patrick *= q;
} // q 作用域的结束
...
return patrick;
}

在这个例子中,q 的作用域被限制在内部代码块内,只有该代码块内的代码可以访问 q。

C99 之前(你用的 Xcode 完全支持 C99),具有代码作用域的变量都必须在代码块开始处进行声明,C99 开始允许在一个代码块中任何位置声明变量。

1
2
for (int i = 0; i < 10; i++)
{printf("A C99 feature: i = %d", i);}

作为这一新功能的一部分,C99 把代码块的概念扩大到包括由 for循环、while 循环或者if语句所控制的代码——即使这些代码没有用花括号括起来。

文件作用域 file scope

一个在所有函数之外定义的变量具有文件作用域。具有文件作用域的变量从它定义处到包含该定义的文件 结尾处都是可见的。除非被同名的局部变量屏蔽。

函数原型作用域 function prototype scope

适用于函数原型中使用的变量名。函数原型作用域从变量定义处一直到原型声明的末尾。这意味着编译器在处理一个函数原型的参数时,它所关心的只是该参数的类型。名字通常是无关紧要的,名字起作用的一种情形是变长数组参量:

1
void use_a_VLA(int n, int m, ar[n][m]);

如果在方括号中使用了变量名,则该变量名必须是在原型中已经声明了的。

链接,一个 C 变量具有下列链接之一:

外部链接 external linkage

具有外部链接的变量可以在一个 多文件程序 的 任何 地方使用。

内部链接 internal linkage

在定义时使用了存储类说明符static,可以在定义该变量的文件的任何地方使用。

空链接 no linkage

具有代码块作用域和函数原型作用域的变量有空链接。意味着它们由其定义所在的代码块或函数原型所私有。

1
2
3
4
5
6
7
int giants = 5; // 文件作用域,外部链接
static int dodgers = 5; // 文件作用域,内部链接
int main()
{
int i = 0; // 空链接
...
}

存储时期

一个 C 变量有以下两种存储时期之一:

静态存储时期 static storage duration

  1. 具有文件作用域的变量,具有静态存储时期。

    注意对于具有文件作用域的变量,关键词 static 表明链接类型,并非存储时期。一个使用 static 声明了的文件作用域变量具有内部链接,而所有文件作用域变量,无论它是具有内部链接,还是具有外部链接,都具有静态存储时期。

  2. 在函数内,定义时,声明为 static 的变量。

自动存储时期 auto storage duration

在代码块内,在定义时,没有用关键字static声明的变量具有自动存储时期。在程序进入定义这些变量的代码块时,将为这些变量分配内存;当退出这个代码块时,分配的内存将被释放。该思想吧自动变量使用的内存视为一个可以重复使用的工作区或者暂态内存。例如,在一个函数调用结束后,它的变量所占用的内存可以被用来存储下一个被调用函数的变量。

5 种存储类

C 使用作用域、链接和存储时期来定义 5 种存储类,见下表:

存储类 存储时期 作用域 链接 声明方式
自动 自动 代码块 空 代码块内
寄存器 自动 代码块 空 代码块内,使用 关键字 register
空链接的静态 静态 代码块 空 代码块内,使用关键字 static
具有外部链接的静态 静态 文件 外部 所有函数之外
具有内部链接的静态 静态 文件 内部 所有函数之外,使用关键字 static

自动变量

可用关键字 auto显示定义存储类型。

默认情况下,在代码块或函数的头部定义的任意变量(没有用 static 显示声明)都属于自动存储类。

1
2
3
4
int main(void)
{
auto int plox;
}

再来仔细看一下嵌套代码块。只有定义变量的代码块及其内部的任何代码块可以访问这个变量,这是一个:

1
2
3
4
5
6
7
8
9
10
11
int loop (int n)
{
int m; // m 的作用域
scanf("%d", &m);
for (int i = m; i < n; i++)
{
// m 和 i 的作用域
puts("i is local to a sub-block\n");
}
return m; // m 的作用域,i 已经消失
}

变量 i 仅在内层花括号中可见,如果试图在内层代码块之前或之后使用该变量,将得到一个编译错误。变量 n 和 m 在函数头部和外层代码块中定义,在整个函数中可用,一直存在到函数终止。

如果在内层代码块定义了一个具有和外层代码块变量同一名字的变量,内层定义会暂时覆盖 (hide) 外部定义,但是当运行离开内层代码块时,外部变量重新恢复作用。

不带{}的代码块

在 C99 中,语句若为循环或者 if 语句的一部分,即使没有使用{},也认为是一个代码块,和内层花括号的效果一样。

⚠️注意:有些编译器可能不支持 C99 作用域规则,其他编译器可能提供一个激活这些规则的选项。例如用 gcc 在命令行下编译filename.c这个文件,需要使用gcc -std=c99 filename.c -o filename

自动变量的初始化

除非显示地初始化自动变量,否则它不会被自动初始化。考虑如下声明:

1
2
3
4
5
6
int main(void)
{
int repid;
int tents = 5;
...
}

变量tents初始化为 5,而变量repid的初值则是,先前占用分配给它的空间的任意值。不要指望这个值是 0。

寄存器变量

可以用存储类说明符register声明寄存器变量。

1
2
3
4
5
int main(void)
{
register int quick;
...
}

如果幸运,寄存器变量可以被存储在 CPU 寄存器中,或者存储在速度最快的可用内存中,从而可以比普通变量更快地被访问和操作。因为寄存器变量多是存在一个寄存器而非内存中,所以无法获得寄存器变量的地址。但是在其他的许多方面,寄存器变量域自动变量一样——都有代码块作用域、空链接以及自动存储时期。

之所以说“如果幸运”,是因为声明一个寄存器变量仅是一个请求,而非一条直接的命令。编译器必须在您的请求与可用寄存器的个数或可用高速内存的数量之间做权衡,所以您可能达成不了自己的愿望。这种情况下,变量称为一个普通的自动变量;然而,您依然不能对它使用地址运算符。

可以使用register声明的类型是有限的。例如,处理器可能没有足够大的寄存器来容纳 double类型。

具有代码块作用域的静态变量

通过使用存储类说明符static(这提供了静态存储时期)在代码块内声明(这提供了代码块作用域和空链接)创建。

静态变量 (static variable)中“静态”是指变量的位置固定不动。

具有文件作用域的变量自动(也是必须的)具有静态存储时期。也可以创建具有代码块作用域,兼具静态存储的局部变量。也就是说,从一次函数调用到下一次调用,计算机都记录着它们的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import <stdio.h>
void trystat(void);
int main(int argc, const char * argv[])
{
int count;
for(count = 1; count <= 3; count++)
{
printf("Here comes iteration %d: \n", count);
trystat();
}
return 0;
}
void trystat(void)
{
int fade = 1;
static int stay = 1;
printf("fade = %d and stay = %d\n", fade++, stay++);
}

运行结果为:

1
2
3
4
5
6
7
Here comes iteration 1:
fade = 1 and stay = 1
Here comes iteration 2:
fade = 1 and stay = 2
Here comes iteration 3:
fade = 1 and stay = 3
Program ended with exit code: 0

静态变量stay记得它的值曾被加 1,而变量fade每次都重新开始。这表明初始化的不同:在每次调用trystat()时fade都被初始化,而stay只在编译trystat()时被初始化一次。

如果不显式地对静态变量进行初始化,它们将被初始化为 0。
以下两个声明看起来很相似:

1
2
int fade = 1;
static int stay = 1;

然而,第一个语句确实是函数trystat()的一部分,每次调用该函数时都会执行它——运行时动作。而第二个语句不是该函数的一部分。如果用调试程序逐步运行改程序,会发现,程序看起来跳过了那一步。那是因为静态变量和外部变量在程序调入内存时已经就位了。把这个语句放在trystat()函数中是为了告诉编译器只有函数trystat()可以看到该变量。它不是在运行时执行的语句。

对函数参量不能使用static

1
int wontwork(static int flu); // 不允许

具有外部链接的静态变量

具有外部链接的静态变量具有文件作用域、外部链接和静态存储时期。这一类型被称为外部存储类 (external storage class),这一类型的变量称为外部变量 (external variable)。

把变量的定义声明放在所有函数之外,即创建了一个外部变量。为了使程序更加清晰,可以在使用外部变量的函数中通过使用extern关键字来再次声明它。

如果变量是在别的文件中定义的,使用extern来声明该变量就是必须的。

1
2
3
4
5
6
7
8
9
10
int Errupt; // 外部定义的变量
double Up[100]; // 外部定义的数组
// 必须的声明,因为 Coal 在其他文件中定义
extern char Coal;
int main(void)
{
...
}

外部变量初始化

如果不对外部变量进行初始化,它们将自动被赋初值 0。这一原则也适用于外部定义的数组元素。不同于自动变量,只可以用常量表达式来初始化文件作用域变量:

1
2
3
4
int x = 10; // 可以,10 是常量
int y = 3 * 20; // 可以,一个常量表达式
size_t z = sizeof(int); // 可以,一个常量表达式
int x2 = 2 * x; // 不可以,x 是一个变量

(只要类型不是一个变长数组,sizeof 表达式就被认为是常量表达式。)

外部名字

C99 标准要求编译器识别局部标识符的前 63 个字符和外部标识符的前 31 个字符。

定义和声明

1
2
3
4
5
int tern = 1; // 定义 tern
main()
{
external int tern;
// 使用在其他地方定义的 tern 变量

这里,tern 第一次声明为变量留出了存储空间,它构成了变量的定义。第二次声明只是告诉编译器要使用先前定义的变量 tern,因此不是一个定义。第一次声明称为定义声明 (definig declaration),第二次声明称为引用声明 (referencing declararion)。关键字 extern 表明该声明不是一个定义,因为它指示编译器参考其他地方。

如果这样做:

1
2
3
extern int tern;
int main(void)
{

那么编译器假定 tern 的真正定义是在程序中其他某个地方,也许是在另一文件中。这样的声明不会引起空间分配。因此,不要用关键字 extern 来进行外部定义;只用它来引用一个已经存在的外部定义。

一个外部变量只可进行一次初始化,而且一定是在变量被定义时进行。下面的语句是错的:extern char permis = 'Y'; // 错误

因为关键字extern的存在标志着这是一个引用声明,而非定义声明。

使用

复杂的 C 程序往往使用多个独立的代码文件。有些时候,这些文件可能需要共享一个外部变量。ANSI C 通过在一个文件中定义变量,在其他文件中引用声明这个变量来实现共享。也就是说,除了一个声明(定义声明)外,其他所有声明都必须使用关键字extern,并且只有在定义声明中才可以对该变量进行初始化。

注意:
除非在 B 文件中也声明了该变量(通过使用extern),否则在一个文件中定义的外部变量不可以用于第二个文件。

一个外部变量声明本身只是使一个变量可能对其他文件可用。

具有内部链接的静态变量

这种存储类的变量具有静态存储时期,文件作用域以及内部链接。通过使用存储类说明类static在所有函数外部进行定义(正如定义外部变量那样)来创建一个这样的变量:

1
2
3
static int svil = 1; // 具有内部链接的静态变量
int main(void)
{

可以在函数中使用存储类说明符extern来再次声明任何具有文件作用域的变量。这样的声明并不改变链接。

存储类说明符

C 语言中有 5 个作为存储类说明符 (storage class specifier) 的关键字,它们是auto register static extern以及typedef。

typedef与内存存储无关,由于语法原因被归入此类。不可以在一个声明中使用一个以上存储类说明符,意味着不能将其他任一存储类说明符作为typedef的一部分。

存储类和函数

函数也具有存储类。函数可以是外部的(默认情况下)或者静态的( C 99增加了第三种可能性,即内联函数)。

外部函数可被其他文件中的函数调用,而静态函数只可以在定义它的文件中使用。

使用哪种存储类

尽量使用 auto。

您的支持将鼓励我继续创作!

热评文章