存储类别
C 提供了多种不同的模型或存储类别(storage class) 在内存中存储数据。先要复习一些概念和术语。
从硬件方面看,被存储的每一个值都占用一定的物理内存,C 语言把这样的一块内存称为对象(Object),或者准确点说称为内存对象(Memory Object)。对象 可以存储一个或多个值。一个对象刚初始化时,没有存储实际的值(NULL),但是当它存储适当的值时一定具有相应的大小。
从软件方面看,程序需要一种方法访问对象。可以通过声明变量来完成:
int entity = 3;
该声明创建了一个名为 entity
的标识符(identifier)。标识符是一个名称,在软件层面上代表物理层面上内存对象这个概念的名称。在通过这个方式声明时,标识符entity可以用来**指定(designate)**特定对象的内容。在这里标识符所表示的内存对象的值是3。
变量名不是指定对象的唯一途径,我们还可以通过表达式来进行运算,用以获取内存对象:
int *pt = &entity;
int ranks[10];
pt
是一个标识符,是一个存储地址的对象。 但是,表达式 *pt
不是标识符,因为它不是一个名称。然而,它确实指定了一个对象,在这种情况下,它与 entity
指定的对象相同。
一般而言,那些指定对象的表达式被称为 左值。所以,entity
即是标识符也是左值。
*pt
即是表达式也是左值。按照这个思路,ranks + 2 * entity
既不是标识符,也不是左值,它就是一个指针运算表达式。但是*(ranks + 2 * entity)
,是一个左值,因为ranks + 2 * entity
表达式计算出的内存地址被经过*
取值运算符,取出了其中的值,它就是一个对象。我们可以使用数学中的换元概念来表示:
int ranks[10];
设:
int *v;
v = (ranks + 2 * entity);
那么:
*(v) = *(ranks + 2 * entity)
当 ranks + 2 * entity 的位置为数字 10 时,
*v = 10;
在上文中,你应该理解了左值的概念,我们来重新整理一下,entity
我们称为标识符,方便我们去记忆某一个对象。 而左值不一定是标识符,也可以是一个运算表达式,但是左值必须能够表示一个内存对象。
现在我们引入新的概念,**可修改的左值(modifiable value)**和不可修改的左值。,用以引出const
限定符的概念。现在我们有如下声明:
const char *pc = "Behold a string literal!";
程序根据该声明把相应的字符串字面量存储在内存中,那么包含这些字符值的数组就是一个对象,我们习惯称为数组对象(Array Object)。由于数组中的每一个字符都能够被单独访问,所以每一个字符也是一个对象。该声明还创建了。而一个标识符为 pc
的对象,存储着字符串的地址。由于可以设置 pc
重新只想其他字符串,所以标识符pc
是一个可修改的左值。
const
只能保证被pc
指向的字符串内容不被改变,但是无法保证 pc
不指向别的字符串。现在我们用上面的知识来描述,pc
是一个对象,该对象指向了另外一个对象,这个对象是字符串字面量在内存中的地址,而const
保证的是一个对象的值不能够被修改,这也就意味着,如果pc
是一个存储字面量3
的变量时,字面量3
是不可被修改的。
我们接着回到刚才的话题,由于*pc
指定了存储 B
字符的数据对象,所以 *pc
是一个左值,但不是一个可修改的左值(因为 const
限定符的存在)。与此类似,因为字符串字面量本身制定了存储字符串的对象,所以它也是一个左值,但不是可修改的左值。
可以用存储期(storage duration) 描述对象,所谓存储期是指对象在内存中保留了多长时间。
标识符用于访问对象,可以用 作用域(scope)和链接(linkage) 描述标识符,标识符的作用域和链接表明了程序哪些部分可以使用它。不同的存储类别具有不用的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数中的某部分使用。
对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以在特定现场的执行期存在。可以通过函数调用的方式显式分配和释放内存。
作用域
作用域在C语言中分为四个范围:
- 块作用域
- 函数作用域:
- 函数原型作用域
- 文件作用域
块作用域(block scope)
被{}
包含的代码块,在代码块中声明使用的标识符不能够在块范围外访问(编译器级别限制)。虽然函数的形参声明在{
左边的小括号中,但是形参也具有块作用域,属于函数块这个块作用域中。
变量a
和 变量b
的作用域都仅限于 foo
函数这个块作用域中:
int foo(int a, int b) {
...
return 0;
}
但是在下面的for
循环中,我们声明了4个变量,它们分别是:
a
:函数作用域b
:函数作用域i
:for
循环作用域loop_var
:for
循环作用域
int blocky(int a) {
int b = 0;
for (i = 0; i < 10; i++>) {
int loop_var = a * i; // loop_var 作用域开始
b = a;
} // loop_var 作用域结束
// 当我在这里试图访问 printf("%d", loop_var); 会出现错误
...
return b;
}
当在循环体外部访问 loop_var
和 i
变量时会出现错误。不过相较于 i
变量而言,loop_var
的生命周期更短,因为它会在每次循环时去重新初始化一个新的 loop_var ,而i
是在整个循环周期中一直存在。
函数作用域(function scope)
函数作用域这个概念只作用于foto
语句标签。这意味着即使一个标签首次出现在函数的内城块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。在函数外,是不能够定义 goto
标签的,所以别问。