C语言中变量类型、作用域与生命周期的总结
最近在看“C Programing Language" (Kernighan, Ritchie)关于外部变量的讨论,之前在学C的时候对这些extern, auto, static, register等不是太理解,这本书讲的很详细,现在总结一下。
首先, C的变量分成局部变量 local variable 和全局变量 global variable。
【注】
-
C 中局部(local)变量(也有翻译成本地变量),也可以叫做内部(internal)变量
-
C 中全局(global)变量,又可叫外部(external)变量。
这些称呼都可以互换,不同的称呼可能强调的是不同的特性,以下尽量用对应使用的关键字来称呼,比如局部变量将用自动变量(auto
)来称呼,全局变量将用外部变量(extern
)来称呼。
C语言程序可以看成是由许多外部对象构成的,这些外部对象可以是变量或函数。外部(external)和内部(internal)是相对的,internal是用来描述在函数内部的函数参数或变量,external描述的是定义在函数外部的变量。由于C语言不允许在函数内部定义函数,因此函数都可以看成是外部的(extern)。
栗子:
#include <stdio.h>
#define MAX 100
char s[MAX];
void printString(void)
{
printf("%s", s);
}
int main(void)
{
scanf("%s", s);
printString(s);
return 0;
}
这个简单的printString.c
源文件即可看成是由三个外部对象构成:外部变量s
, 外部函数printString()
和main()
组成,还有一个include
的标准库文件stdio.h
(这个下次再谈)。
默认情况下,外部变量与函数具有以下性质:通过同一个名字引用的所有外部变量(即使这种引用来自单独编译的不同函数)实际上都是引用内存中同一个对象。
因为外部变量可以在全局范围内访问,这就为函数之间的数据交换提供一种新的方式,可以代替函数参数与返回值,如上一个栗子。任何函数都可以通过名字访问一个外部变量,当然这个名字需要通过某种方式来声明。
外部变量与自动变量的作用域与生命周期
- 变量或者符号的作用域是指程序中可以使用这个变量或名字的范围。这个可以看成是静态的代码范围
- 变量生命周期则是指该变量存在的时间范围。这个可以理解为程序运行时的变量存在的时间周期。
自动变量只能在函数内部使用,作用域从声明处开始直至函数结束,生命周期是从其所在的函数被调用时,变量开始存在,在函数退出时变量将消失。对于在函数开头声明的自动变量,其作用域即为声明该变量名的函数内部,函数的参数也是如此,实际上可以将它看作是这个函数的局部变量。当然,自动变量也可以定义在函数内的语句块中,比如在下面的for循环中定义的临时变量temp:
int func(void)
{
int i;
for(i = 0; i < 10; i++)
{
int temp;
...
}
}
这种变量当然作用域是限定在语句块内部,生命周期也是在该语句块内部,当程序执行完该语句块,变量也就消失了。
外部变量作用域为从其定义处开始直至所在的文件的结尾结束,生命周期是永久存在的,即程序执行期间一直存在,它们的值在一次函数调用结束到下一次函数调用开始之前保持不变。另一方面,可以通过添加声明的方式来使用外部变量,按照上面所说,外部变量是唯一的,因此添加extern
声明可以扩展外部变量的作用域到其他文件中。
如果要在外部变量的定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个文件中,则必须在相应的变量声明中强制使用关键字extern
。
将外部变量的定义与声明区别开是很有必要的,外部变量的声明用于说明变量的属性(主要是类型),而外部变量的定义除此之外还会引起内存的分配(在定义后编译程序将为它分配内存单元)。
而自动变量则不然,自动变量在C中没有定义这一说法,只要先声明再使用即可,这是因为自动变量(即局部变量)是在运行时由栈来管理的,而外部变量(即全局变量)是在编译过程中由编译器、汇编器分配存储地址,一直到链接时确定内存位置(这些内容将在之后会专门总结有关编译、汇编、链接的内容),在这里都可以理解为在编译时即分配了内存单元。
栗子:如果将下面这两条语句放在所有函数的外部:
int a;
double b[MAX];
则这两条语句将定义外部变量a与b,并为之分配内存,同时这两条语句还可以作为该源文件中其余部分的声明。而下面的两行语句:
extern int a;
extern double b[];
为所在文件该语句之后的部分声明了一个int
类型的外部变量a以及一个double
类型的外部变量b(该数组的长度在其他地方确定),但这两个声明并没有建立变量或为他们分配内存。
在程序的源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern
声明来访问它(定义外部变量的源文件中也可以包含对该外部变量的extern
声明)。外部变量的定义必须指定数组的长度,但extern声明则不一定要指定数组的长度。外部变量的初始化只能出现在其定义中(注:若外部变量未初始化,编译器将它初始化为0)。
【注】外部变量的声明也可以通过上下文隐式声明(即如上所说,定义即可作为之后语句的声明)如下面程序版本2中的外部变量len
和buf
在main函数中就无需在main中再声明。
版本1:
#include <stdio.h>
#define MAXLENGTH 1000 // buffer最大长度
char buf[MAXLENGTH];
int getline(void);
/*一个简单的copy-paste程序
*/
int main(void)
{
while (getline()!=EOF)
{
printf("%s\n", buf);
}
return 0;
}
int getline(void)
{
int c, i;
extern char buf[MAXLENGTH];
i= 0;
while ( i < MAXLENGTH && (c = getchar()) != EOF && c != '\n')
buf[i++] = c;
if (c = '\n')
buf[i++] = c;
buf[i] = '\0';
return i;
}
版本2:
#include <stdio.h>
#define MAXLENGTH 1000 // buffer最大长度
char buf[MAXLENGTH];
int getline(void);
/*一个简单的copy-paste程序
*/
int main(void)
{
while (getline()!=EOF)
{
printf("%s\n", buf);
}
return 0;
}
int getline(void)
{
int c, i;
// 通过上下文隐式声明 buf[]
i= 0;
while ( i < MAXLENGTH && (c = getchar()) != EOF && c != '\n')
buf[i++] = c;
if (c = '\n')
buf[i++] = c;
buf[i] = '\0';
return i;
}
静态变量与寄存器变量
静态变量
之前已经提到了,外部变量与自动变量,其中外部变量是可以被全局使用的,这个全局指的是整个源程序的所有源文件都可以通过添加extern
声明来使用。但是,如果我们希望限定这个外部变量仅限于该定义的源文件使用,而不希望被其他源文件使用。那我们可以使用static声明限定外部变量和函数,可以将其声明的对象的作用域限定为该源文件的剩余部分。通过static限定外部对象,可以达到隐藏外部对象的目的。
static char buf[BUFSIZE];
static int bufp = 0;
变量声明为static之后,该变量即为静态存储,其他文件中的函数就不可以访问变量buf
, bufp
,因此这两个名字就不会和同一程序中的其他文件中相同名字的变量相冲突。
【注】:多个函数中的自动变量同名,也不会造成冲突,因为在编译过程中,编译器会将自动变量改成不同名字,比如加上函数名,具体做法依赖于编译器版本。
外部的static
声明多用于变量,当然,也可以用于声明函数。通常情况下,函数名是全局可访问的,对整个程序的各个部分都是可见的。但是,如果把函数声明为static
类型,则该函数除了对该函数声明所在的文件可见外,其他文件都无法访问。
static
也可用于声明自动变量,static
类型的自动变量同一般的自动变量一样,是某个特定函数内的局部变量,只能在该函数中使用。但它与一般的自动变量不同的是,不管其所在函数是否被调用,它一直存在。换句话说,static
类型的内部变量作用域不变,生命周期和外部变量一样为整个程序运行期间。
寄存器变量
register
声明告诉编译器,它所声明的变量在程序中使用频率较高。其思想史,将register
变量放在机器的寄存器中,这样可以使程序更小,执行速度更快。
register
声明的形式如下:
register int x;
register char c;
register
声明只适用于自动变量以及函数的形式参数,看下面的例子:
void f(register unsigned a, register long n)
{
register int i;
...
}
实际使用的时候,底层硬件环境会对寄存器变量的使用有一些限制。每个函数中只有很少的变量可以保存在寄存器中,且只允许某些类型的变量。但是,过量的寄存器变量并没有什么害处,因为编译器可以忽略过量的或不支持的寄存器变量声明。另外,无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。
总结
变量类型 | 定义 | 作用域 | 生命周期 | 说明 |
---|---|---|---|---|
自动变量 | 定义在函数内部或者是函数参数 | 自声明其至函数结尾或者是所在语句块结尾 | 作用域生效则生效,作用域失效则失效,多次调用,重新创建该变量 | 在运行时,由栈管理 |
外部变量 | 定义在函数外部或者是函数 | 自定义起至所在文件结尾,可通过extern 声明,扩展至全局 | 整个程序运行期间 | 在编译时,一旦定义即创建变量、分配内存 |
静态变量 | 声明时使用,通过添加static 来声明 | 声明外部变量时,该外部变量作用域仅为声明所在文件,声明自动变量时,不改变 | 整个程序运行期间 | 可以限定全局变量,函数或者是自动变量 |
寄存器类型 | 通过添加register 声明 | - | - | 只能限定自动变量或函数参数,可能被存放在寄存器中,也可能被忽略,但是被声明为寄存器类型的变量地址不可访问 |
因此,可以将变量分为:被初始化的全局范围的外部变量,被初始化的静态类型外部变量,未被初始化的两类外部变量,自动变量,静态类型自动变量,寄存器类型自动变量。
至于为什么这么分,下次讨论编译的时候用的上。