【C语言存储类别、链接和内存管理】
本文内容主要包括:
文章目录
- 一、存储类别
-
- 1.1 作用域
- 1.2 连接
- 1.3 存储期
- 1.4 五种存储类别
-
- 1.41 自动变量
- 1.42 寄存器变量
- 1.43 块作用域的静态变量
- 1.44 外部链接的静态变量
- 1.45 内部链接的静态变量
- 二、随机函数和静态变量
- 三、内存分配
一、存储类别
从硬件方面来看,被储存的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象(object)。
对象可以储存一个或多个值。一个对象可能并未储存实际的值,但是它在储存适当的值时一定具有相应的大小(你不要总想着面向对象里面的对象,面向对象编程中的对象指的是类对象,其定义包括数据和允许对数据进行的操作,C不是面向对象编程语言)。
程序需要一种方法访问对象。这可以通过声明变量来完成:
int a = 3;
该声明创建了一个名为 a 的标识符(identifier)。标识符是一个名称,在这种情况下,标识符可以用来指定(designate)特定对象的内容。标识符遵循变量的命名规则。在该例中,标识符 a 即是软件(即C程序)指定硬件内存中的对象的方式。该声明还提供了储存在对象中的值。
标识符,包括变量名、常量名、对象名、函数名、类型名等等
变量名不是指定对象的唯一途径:
int *pt = &a;
int ranks[10];
第1行声明中,pt
是一个标识符(指针变量名),它指定了一个储存地址的对象(它指定了一块内存,这里存储着它指向值的地址)。但是,表达式 *pt
不是标识符,因为它不是一个名称。然而,它确实指定了一个对象,在这种情况下,它与 a 指定的对象相同。
一般而言,那些指定对象的表达式被称为左值。所以,a 既是标识符也是左值;*pt
既是表达式也是左值。按照这个思路,ranks + 2 *a
既不是标识符(不是名称),也不是左值(它不指定内存位置上的内容)。
但是表达式*(ranks + 2 * a)
是一个左值,因为它的确指定了特定内存位置的值,即ranks数组的第7个元素。顺带一提,ranks的声明创建了一个可容纳10个int类型元素的对象,该数组的每个元素也是一个对象。
所有这些示例中,如果可以使用左值改变对象中的值,该左值就是一个可修改的左值(modifiable lvalue)。
const char *pc = "Behold a string literal!";
程序根据该声明把相应的字符串字面量储存在内存中,内含这些字符值的字符串字面量就是一个对象。
由于字符串字面量中的每个字符都能被单独访问,所以每个字符也是一个对象。
该声明还创建了一个标识符为pc的对象,储存着字符串的地址。由于可以设置pc重新指向其他字符串,所以标识符pc是一个可修改的左值。
const只能保证被pc指向的字符串内容不被修改,但是无法保证pc不指向别的字符串。
由于pc指定了储存’B’字符的数据对象,所以pc是一个左值,但不是一个可修改的左值。与此类似,因为字符串字面量本身指定了储存字符串的对象,所以它也是一个左值,但不是可修改的左值。
可以用 存储期(storage duration) 描述对象,所谓存储期是指对象在内存中保留了多长时间。
标识符用于访问对象,可以用 作用域(scope)和链接(linkage) 描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。
不同的存储类别具有不同的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数中的某部分使用。
对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以在特定线程的执行期存在。可以通过函数调用的方式显式分配和释放内存
1.1 作用域
作用域描述程序中可访问标识符的区域。一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。
(1)块作用域(block scope)
块是用一对花括号括起来的代码区域。
例如,整个函数体是一个块,函数中的任意复合语句也是一个块。定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含该定义的块的末尾。另外,虽然函数的形式参数声明在函数的左花括号之前,但是它们也具有块作用域,属于函数体这个块。
(2)函数作用域(function scope)
仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。(goto很少有人使用它)
(3)函数原型作用域(function prototype scope)
用于函数原型中的形参名(变量名),如下所示:
int mighty(int mouse, double large);
函数原型作用域的范围是从形参定义处到原型声明结束。这意味着,编译器在处理函数原型中的形参时只关心它的类型,而形参名(如果有的话)通常无关紧要。而且,即使有形参名,也不必与函数定义中的形参名相匹配。只有在变长数组中,形参名才有用:
void use_a_VLA(int n, int m, ar[n][m]);
方括号中必须使用在函数原型中已声明的名称。
(4)文件作用域
变量的定义在函数的外面,具有文件作用域(file scope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。
#include <stdio.h>
int units = 0; /* 该变量具有文件作用域 */
void critic(void);
int main(void)
{...
}
void critic(void)
{...
}
变量units具有文件作用域,main()和critic()函数都可以使用它(更准确地说,units具有外部链接文件作用域,稍后讲解)。由于这样的变量可用于多个函数,所以文件作用域变量也称为全局变量(global variable)。
多个文件在编译器中可能以一个文件出现。例如,通常在源代码(.c扩展名)中包含一个或多个头文件(.h扩展名)。头文件会依次包含其他头文件,所以会包含多个单独的物理文件。但是,C预处理实际上是用包含的头文件内容替换#include指令。所以,编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元(translation unit)。 描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。 如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。
1.2 连接
C变量有3种链接属性:外部链接、内部链接或无链接。
具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。
具有文件作用域的变量可以是外部链接或内部链接。
外部链接变量可以在多文件程序中使用(在其他文件中使用时需要使用extern关键字),内部链接变量只能在一个翻译单元中使用。
一些程序员把“内部链接的文件作用域”简称为“文件作用域”,把“外部链接的文件作用域”简称为“全局作用域”或“程序作用域”。
如何知道文件作用域变量是内部链接还是外部链接:
int giants = 5; // 文件作用域,外部链接
static int dodgers = 3; // 文件作用域,内部链接
int main()
{...
}
...
1.3 存储期
作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。
- 如果对象具有静态存储期,那么它在程序的执行期间一直存在。文件作用域变量具有静态存储期。注意,对于文件作用域变量,关键字static表明了其链接属性,而非存储期。以static声明的文件作用域变量具有内部链接。但是无论是内部链接还是外部链接,所有的文件作用域变量都具有静态存储期。
- 线程存储期用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。
- 块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。这种做法相当于把自动变量占用的内存视为一个可重复使用的工作区或暂存区。
- 变长数组的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。
- 块作用域变量也能具有静态存储期。为了创建这样的变量,要把变量声明在块中,且在声明前面加上关键字static。 它的作用域定义在函数块中。只有在执行该函数时,程序才能使用标识符访问它所指定的对象(但是,该函数可以给其他函数提供该存储区的地址以便间接访问该对象,例如通过指针形参或返回值)。
1.4 五种存储类别
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
---|---|---|---|---|
自动 | 自动 | 块 | 无 | 块内 |
寄存器 | 自动 | 块 | 无 | 块内,使用关键字register |
静态外部链接 | 静态 | 文件 | 外部 | 所有函数外 |
静态内部链接 | 静态 | 文件 | 内部 | 所有函数外,使用关键字static |
静态无链接 | 静态 | 块 | 无 | 块内,使用关键字static |
1.41 自动变量
属于自动存储类别的变量具有自动存储期、块作用域且无链接。默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。为了更清楚地表达你的意图(例如,为了表明有意覆盖一个外部变量定义,或者强调不要把该变量改为其他存储类别),可以显式使用关键字auto
:
...
int main(void)
{auto int plox;...
关键字auto是存储类别说明符(storage-class specifier)。auto关键字在C++中的用法完全不同,如果编写C/C++兼容的程序,最好不要使用auto作为存储类别说明符。
块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然,参数用于传递变量的值和地址给另一个函数,但是这是间接的方法)。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量。
变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失。原来该变量占用的内存位置现在可做他用。
如果内层块中声明的变量与外层块中的变量同名会怎样?内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用域又回到了原来的作用域。
注意以下两点:
- 没有花括号的块: 个C99特性:作为循环或if语句的一部分,即使不使用花括号({}),也是一个块。更完整地说,整个循环是它所在块的子块(sub-block),循环体是整个循环块的子块。与此类似,if语句是一个块,与其相关联的子语句是if语句的子块。这些规则会影响到声明的变量和这些变量的作用域。(不用花括号时,只能有一个语句。有的编译器可能不支持)
for (int a = 0; a < 8; a++)printf("%d\\n",a);
- 自动变量的初始化:自动变量不会初始化,除非显式初始化它:
int main(void)
{int repid;int tents = 5;
tents变量被初始化为5,但是repid变量的值是之前占用分配给repid的空间中的任意值(如果有的话),不要认为这个值是0。可以用非常量表达式(non-constant expression)初始化自动变量,前提是所用的变量已在前面定义过:
int main(void)
{int ruth = 1;int rance = 5 * ruth; // 使用之前定义的变量
1.42 寄存器变量
变量通常储存在计算机内存中。寄存器变量有可能储存在CPU的寄存器中,或者概括地说,储存在最快的可用内存中。与普通变量相比,访问和处理这些变量的速度更快。由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址。绝大多数方面,寄存器变量和自动变量都一样。也就是说,它们都是块作用域、无链接和自动存储期。使用存储类别说明符register
便可声明寄存器变量。
int main(void)
{register int quick;
声明变量为register类别与直接命令相比更像是一种请求。编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,所以可能不会如你所愿。在这种情况下,寄存器变量就变成普通的自动变量。即使是这样,仍然不能对该变量使用地址运算符。
在函数头中使用关键字register,便可请求形参是寄存器变量:
void macho(register int n)
可声明为register的数据类型有限。例如,处理器中的寄存器可能没有足够大的空间来储存double类型的值。