C语言中,存储类别(Storage Class)是一个标识符(也就是说函数和变量)的重要属性。存储类别决定着一个函数或变量的作用域(Scope,即可见性)和生命周期(Life time)。C语言中,主要有四种存储类别,即auto、register、static和extern,下面将一一说明。
在说明之前,先讲清楚几个概念:
(1) 定义(Definition)和声明(Declaration)
声明是通知编译器变量的名字(name),类型(type),作用域(scope),链接性(linkage)和变量的声明周期(Duration or Lifetime);而变量的定义会导致编译器给该变量分配内存。需要注意的是只能在变量定义的时候对其进行初始化。因为,只有变量的定义才导致内存分配才需要初始化,变量声明是声明的一块已经分配好的内存空间,该空间在变量定义的时候已经被初始化过。
(2)作用域(scope)
作用域是一个编译时概念,主要有两种:文件(file)和块(block)。文件作用域是指从变量的声明到文件的末尾,该变量都是可以访问的;块作用域是指从变量被声明到改程序块的末尾是可以被访问的。
(3)链接性(linkage)
在C语言中,大体来说,有两种类型的对象(这里不是指面向对象编程中的对象,只是一般意义上的对象,也可以理解为元素)外部对象和内部对象(External和Internal,也就是上面的连接性)。所有在函数外声明的对象都是外部的,包括函数(函数也必须在函数外定义,因此,这很容易理解);所有在函数内部的对象都是内部的,包括函数的参数。从这种意义上来说,C语言程序是由外部对象的集合构成的。只有外部对象能够参与跨文件和代码库之间的通讯。
链接性是一个链接时概念,用来定义对象是否能被跨文件访问,或者只能在一个文件中访问,主要有三种:外部(External),内部(Internal)和No Linkage。函数内部的任何对象,包括参数,变量和其它都是no linkage,因此它们只能够从函数内部被访问(当然,如果在函数内部用extern作前缀声明了一个对象,这就是说,它不是no linkage的,这里先不讨论这种情况);具有外部链接性的对象必须在程序的最外层(也就是说不能够在函数或语句块内部),外部链接性也是在函数外声明的函数或其它对象的默认链接性,所有名字相同的具有外部链接性的实例都是引用的程序中的同一对象(也就是说内存中的同一区域);当希望函数或其它对象只能够在一个文件内被访问,而不能被文件外访问时,应该使这种对象具有内部链接性,相同名字的具有内部连接性的对象只在同一个文件内引用的是同一个对象,通过在对象的声明前加static关键字可以将外部对象的链接性声明为内部链接性(当然,也可以在局部变量前加static,意义和这里不一样)。
外部链接性是指该变量能够被多个文件访问;内部链接性是指该变量只能够在被声明的文件中被访问。
(4)声明周期(duration)
变量的声明周期是一个运行时概念,主要有两种Temporary和Process。Temporary的变量只在函数或代码块执行的时候存在,这类变量存储在栈(stack)中;Process变量在程序运行的整个过程中都会存在,这类变量存储在内存中的静态存储区。
1、auto
局部变量的缺省存储类别是auto,也就是说,下面两个变量的定义在存储类别层面对编译器来说是一样的。
{
int life;
auto int love;
}
存储类别为auto类型的标识符只能用在函数内部,比如做局部变量。其实,这是一种节省内存的方法,因为存储类别为auto的变量只在需要的时候才存在:当程序进入了它们所在的函数中时才会创建它们,而当退出这个函数时,它们也会随之被销毁。
2、register
register存储类别用来定义应该被存储在寄存器而非内存的变量。这样,一个存储类别为register的变量的最大不能超过寄存器的存储空间(通常是一个字长),并且register类型的变量不能用一元的&运算符来(也就是取地址操作符)操作。(很容易理解,因为register类型的变量是存储在寄存器,而非内存,所以不能用&来取它的内存地址。)。通常,register类型的变量通常用于经常被访问的变量,比如计数器。
{
register int counter;
}
当然,也要知道,register说明符只是建议编译器将该变量存储到寄存器中,但是,在实际中,编译器并不一定会这样做(比如,可能没有足够数量的寄存器供编译器使用)。实际上,现在性能优越的编译器能够识别出经常使用的变量,并将其放在寄存器中,而无需程序员给出register声明。
3、static
3.1 static对于全局变量
static是全局变量的缺省的存储类别,因此,下面的两个变量都有static的存储类别。需要注意的是,虽然下面的语句显示声明life变量的存储类别是static,但是,不要忘了这样显示的声明也将life变量的链接性定为internal,也就是说不能够跨文件访问。
static int life;
int love;
int main()
{
printf("%d\n",life);
printf("%d\n",love);
}
3.2 在函数内部使用static
也可以在函数的内部对变量(这里自然是说局部变量)使用static,如果这样,那么该变量在编译的时候被初始化(并且仅会在编译的时候初始化一次,其他时候初始化语句不会起作用),并且能够在函数调用时,保留上次函数调用结束后该变量的值(实际上,向在上篇文章中提到的,这种变量是存储在程序内存区域的静态存储区,如果没有初始化,这里的变量都会被默认初始化为0)。注意,由于是在编译时初始化,因此,为static变量初始化的必须是一个常量,而不能是表达式或其他(在编译其程序还没开始运行,因此,没法计算表达式的值进行初始化)。下面的static变量可以用来保存函数的调用次数。
void func()
{
static count=1;
count++;
}
下面的例子,更加直接的说明了static变量的作用:
char *func();
int main()
{
char *t1;
t1=func();
}
char *func()
{
static char t2[10]="lfqy"
return t2;
}
上面的例子中,如果t2不声明为static,那么局部变量(存储类型为auto)在函数结束后会被释放掉,那么函数返回的地址将没有意义。如果将变量声明为static,那么该变量在程序运行期间都会存在,这样根据函数返回的地址,在整个程序中都可以访问到字符串“lfqy”。
3.3 在函数声明中使用static
同上面3.1中提到的类似,如果显示地使用static声明或者定义一个函数,那么会将该函数的链接性定义为internal,那么该函数将只会在该文件中可见,不能够跨文件访问。
4、extern
简单地说,当extern用于变量时,extern声明了一个全局变量,该全局变量在其他程序文件中定义。通过该声明,在该文件中,可以引用该变量(该变量的定义在其它文件中)。因为该变量在其它文件中定义,而在该文件中只是进行声明,因此,不能在声明的时候进行初始化(该声明引用的是已经定义的全局变量所指向的内存空间,在定义时它已经被初始化,所以,在声明时不能被初始化)。
file1.c:
extern int count;
write()
{
printf("count is %d.\n",count);
}
file2.c
int count=5;
int main()
{
write();
return 0;
}
在上面的例子中,file1.c中能访问到count的值5,如果file1.c改变了count的值,那么在file2.c中也可以看到被改动的新值。
5、实验
下面是关于上面的所述内容的几个实验,并进行了一些说明,通过它们可以更好地理解上面提到的内容。
E1、默认的在函数外的对象具有外部链接性
funcs.c:
#include
void func1()//默认具有外部链接性
{
printf("Func1 called!\n");
}
void func2()//默认具有外部链接性
{
printf("Func2 called!\n");
}
main.c:
extern void func1();
extern void func2();
int main()
{
func1();
func2();
return 0;
}
测试环境:vs2010,将上面的文件都放在源文件中,编译运行。
output:
Func1 called!
Func2 called!
两点说明:
(1)实际上,如果将main.c中对func1和func2的声明去掉,程序也能够正常运行。不过,我们写程序的时候还是最好声明,这样更加清晰。
(2)其实,main.c中的对func1和func2的声明可以不用加extern,因为默认就是extern的。实际证明可以运行。
E2、外部变量的跨文件访问
file1.c
int a=6;
int b=7;
main.c:
#include
extern int a,b;
int main()
{
printf("%d\n",a);
printf("%d\n",b);
return 0;
}
output:
6
7
这里如果在main.c中没有对变量a,b进行声明的话是没法再main.c中访问a,b的。vs2010会提示未声明的标识符。另外,如果在main.c中对变量a,b的声明中去掉extern,也是没有问题的,因为默认就是extern的。
E3、用static将变量的链接性声明为Internal
file1.c
static int a=6;
int b=7;
main.c:
#include
extern int a,b;
int main()
{
printf("%d\n",a);
printf("%d\n",b);
return 0;
}
上面的程序运行会出错,因为将static将a的链接性定为internal,这样在main.c中就没法访问到a,所以在main.c中试图告诉编译器a变量在其它文件中定义的时候,编译器会提示错误,因为编译器找不到可以跨文件访问的a(在file1.c中定义的变量a是internal的,不具有外部链接性,不能在文件外被访问)。
下面的实验比较有趣,可以看出
file1.c
static int a=6;
int b=7;
main.c:
#include
int a,b;
int main()
{
printf("%d\n",a);
printf("%d\n",b);
return 0;
}
output:
0
7
之所以输出0和7是因为file1.c中定义的变量b具有外部链接性,可以被跨文件访问,这样,通过标识符b可以访问到file1.c文件中定义的b的那块内存区域;而对于在file1.c中定义的a,由于用了static,其链接性变为了internal不能被外部文件访问,这样main.c中的int a,编译器会将其处理成一个新变量的定义,全局变量会被初始化为0,所以会输出0。
关于变量只能在定义时可以被初始化,很容易验证。将上面的int a,b;换成int a,b=0;会报错,提示有一个多重定义的符号(b)。但是,换成int a=8,b;就可以,因为在这里编译器把a理解成定义然后初始化,把b理解成声明。
E3、用static将函数的链接性声明为Internal
funcs.c:
#include
static void func1()//默认具有外部链接性
{
printf("Func1 called!\n");
}
void func2()//默认具有外部链接性
{
printf("Func2 called!\n");
}
main.c:
extern void func1();
void func2();
int main()
{
func1();
func2();
return 0;
}
上面的程序会提示“无法解析的外部符号func1”,因为在文件外部没法找到链接性为internal的函数func1。(注意,main中对两个函数的声明是等价的。)
上面的程序也可以将对func1在funcs.c中的定义和声明分开,写成下面的形式,是等价的。
funcs.c:
#include
static void func1();//默认具有外部链接性
void func1()//默认具有外部链接性
{
printf("Func1 called!\n");
}
void func2()//默认具有外部链接性
{
printf("Func2 called!\n");
}
main.c:
extern void func1();
void func2();
int main()
{
func1();
func2();
return 0;
}
Now, this page is over!