第12章 存储类别、链接和内存管理

发布于 2021-09-14  704 次阅读


C语言能让程序员恰到好处的控制程序。这是它的优势之一。程序员通过C的内存管理系统指定变量的作用域和生命期,实现对程序的控制。合理的使用内存存储是涉及程序的一个要点。

12.1 存储类别

C提供了多种不同的模型或存储类别(storage class)在内存中储存数据。
之前所有的编程示例中使用的数据都存储在内存中。从硬件方面看,被存储的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象(object)。对象可以储存一个或多个值。一个对象可能并未储存实际的值,但是它在储存适当的值时一定具有相应的大小(面向对象编程中的对象指的是类对象,其定义包括数据和允许对数据进行的操作,C不是面向对象编程语言)
从软件方面来看,程序需要一种方法访问对象。这可以通过声明变量来完成:
int entity = 3;
这个声明创建了一个名为entity的标识符(identifier)。标识符是一个名称,在这种情况下,标识符可以用来指定(designate)特定对象的内容。标识符遵循变量的命名规则。在该例中,标识符entity是即软件(C程序)指定硬件内存中的对象方式。该声明还提供了储存在对象中的值。
变量名不是指定对象的唯一途径:
int *pt = &entity;
int ranks[10];
第1行声明中,pt是一个标识符,它指定了一个储存地址的对象。但是,表达式*pt不是标识符,因为它不是一个名称。然而,它确实指定了一个对象,在这种情况下,它与entity指定的对象相同。一般而言,那些指定对象的表达式被称为左值。所以entity既是标识符也是左值;*pt既是表达式也是左值。按照这个思路,ranks+2*entity既不是标识符(不是名称),也不是左值(他不指定内存位置上的内容)。但是表达式*(ranks+2*entity)是一个左值,因为它的却制定了特定内存位置的值,即为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)描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。不同的存储类别具有不同的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数的某部分使用,对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期间。对于并发编程,对象可以在特定线程的执行期存在。可以通过函数调用的方式显示分配和释放内存。

12.1.1 作用域

作用域描述程序中可访问标识符的区域。一个C变量的作用域可以、函数作用域、函数原型作用域或文件作用域。到目前为止,所有的程序示例中使用的变量几乎都具有块作用域。块是用一对花括号括起来的代码区域,例如,整个函数体是一个块,函数中的任意复合语句也是一个块。定义在块中的变量具有块作用域(block scope)。块作用域变量的可见范围是从定义处到包含该定义的块的尾部。另外,虽然函数的形参声明在函数的做花括号之前,但是它们也具有块作用域,属于函数体这个块。所以到目前为止,使用的局部变量(包括函数的形式参数)都具有块作用域。因此。下面的代码中的变量cleo和patrick都具有块作用域:
double blocky(double cleo)
{
    double patrick = 0.0;
    ...
    return patrick;
}
声明在内层块中的变量,其作用域仅局限该声明所在的块。
double block(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。
以前,具有块作用域的变量都必须声明在块的开头,V99放宽了这一限制,允许在块中的任意为止声明变量,因此,对于for的循环头,现在可以这样写:
for(int i;i<10;i++)
    printf("A C99 feature:i=%d",i);
为适应这个新特性,C99并把块的概念扩展到for循环、while循环、do while循环和if语句所控制的代码,即使这些代码没有用花括号括起来,也算是块的一部分。所以,上面for循环中的变量i被视为for循环块的一部分,它的作用域仅限于for循环,一旦程序离开for循环,就不能再访问i。
函数作用域(function scope)仅用于goto语句的标签,这意味着即使一个标签首次出现再函数的内层块中,它的作用域也延伸至整个函数,如果再两个块中使用相同的标签会很混乱,标签函数作用域防止了这样的事情发生。
函数原型作用域(function prototype scope)用于函数原型中的形参名(变量名):
int mighty(int mouse,double large);
函数原型作用域的范围是从形参定义处到原型声明结束。这意味着,编译器再处理函数原型中的形参时只关心它的类型,而形参名通常无关紧要。而且,即使有形参名,也不不与函数定义中的形参名相匹配。只有在变长数组中,形参名才有用:
void use_a_VAL(int n,int m,ar[n][m]);
方括号中必须使用在函数原型中已声明的名称。
变量的定义在函数的外面,具有文件作用域(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)。描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成,每个翻译单元均对应一个源代码文件和它所包含的文件。

12.1.2 链接

C变量有3种链接属性:外部链接、内部链接或无链接。具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用。
注意:正式或非正式术语
C标准用”内部链接的文件作用域“描述仅限一个翻译单元(即一个源码文件和它所包含的头文件)的作用域,用”外部链接的文件作用域“描述可延伸至其他翻译单元的作用域。但是对程序员这些术语太长了,一些程序员把”内部链接的文件作用域“简称为”文件作用域“,把”外部链接的文件作用域“简称为”全局作用域“或者”程序作用域“
如何知道文件作用域是内部链接还是外部链接?可以查看文件是否使用了存储类别说明符static
    int giants = 5;     //文件作用域,外部链接
    static int dodgers = 3; //文件作用域,内部链接
    int main()
    {
    ..
    }
该文件和同一程序的其他文件都可以使用变量giants。
而变量dodgers属于文件私有,该文件中的仍以函数都可以使用它。

12.1.3 存储期

作用域和链接描述了标识符的的可见性。存储期描述了通过这些标识符访问对象的生存期。C对象有4中存储期:静态存储期、线程存储期、自动存储器、动态分配存储期。
如果对象具有静态存储期,那么它在程序的执行期间一直存在。文件作用域变量具有静态存储期。注意,对于文件作用域变量,关键字static表明了其链接属性,并非存储期,以static声明的文件作用域变量具有内部链接。但无论是内部链接还是外部链接,所有的文件作用域都具有静态存储期。
线程存储期用于并发程序设计,程序设计可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。
块作用域的变量通常都有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。这种做法相当于把自动变量占用的内存视为一个可重复使用的工作区或暂存区。例如,一个函数调用结束后,其变量占用的内存可用于储存下一个被调用函数的变量。
变长数组稍有不同,它们的存储期从生命处到块的末尾,而不是从块的开始到块的末尾。
目前为止使用的局部变量都是自动类别。例如,在下面代码中变量number和index在每次调用bore()函数时被创建,在离开函数时被销毁。
void bore(int number)
{
    int index;
    for(index = 0;index < number;index++)
        puts("They don't make them the way they used to.\n");
    return 0;
}
然而,块作用域变量也能具有静态存储区。为了创建这样的变量,要把变量声明在块中,且在声明前面加上关键词static:
void more(int number)
{
    int index;
    static int ct = 0;
    ...
    return 0;
}
这里,变量ct储存在静态内存中,它从程序被载入到程序结束期间都存在。。但是,它的作用域定义在more()函数块中,只有在执行函数时,程序才能使用ct访问它所指定的对象(但是,该函数可以给其他函数提供该存储区的地址以便简洁访问该对象,例如通过执政形参返回值)。
C使用作用域、链接和存储期为变量定义了多种存储方案。除了并发剩下5中存储类别:自动、寄存器、静态块作用域、静态外部链接、静态内部链接,如下表所示。

file

12.1.4 自动变量

属于自动存储类别的变量具有自动存储存储期、块作用域且无链接。默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。为了更清楚的表达你的意图(例如,为了表明有意覆盖一个外部变量定义,或者强调不要把改变量改为其他存储类别),可以显式使用关键字auto。如下所示:
int main(void)
{
    auto int plox;
}
关键字auto是存储类别说明符(storage - class specifier)。auto关键字在C++中的用法完全不同,如果编写C/C++兼容的程序,最好不要使用auto作为存储类别的说明符。
块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然,参数用于传传递变量的值和地址给另一个函数,但是这是间接的方法)。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量。
变量具有自动存储期意味者,程序在进入该变量声明所在的块变量存在,程序在退出该块时变量消失。原来该变量占用的内存位置现在也可以用。
接下来分析一下嵌套块的情况。块中声明的变量仅限于该块及其包含的块使用。
int loop(int n)
{
    int m;  //m的作用域
    scanf("%d",&m);
    {
    int i;  //m和i的作用域
    for(i = m;i < n;i++)
        puts("i is local to a sub-block\n");
    }
    return m;   //m的作用域,i已经消逝
}
在上面的代码中。i仅在内层块中可见。如果在内层块的前面或后面使用i,编译器会报错。通常,在设计程序时用不到这个特性。然而,如果这个变量仅供该块使用,那么在块中就近定义就很方便。这样,可以在靠近使用变量的地方记录其含义。另外,这样的变量只有在使用的时候才占用内存。变量n和m分别定义在函数头和外层块中,它的作用域是整个函数,而且在调用函数到函数结束期间都一直存在。
如果内存块中声明变量与外层块中的变量同名会怎样?
内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用域又回到了原来的作用域。
//hiding.c  --块中的变量
#include<stdio.h> 
int main(void)
{
    int x = 30;     //原始x
    printf("x in outer block :%d at %p\n",x,&x);
    {
        int x = 77; //新的x,隐藏了原始的x 
        printf("x in inner block:%d at %p\n",x,&x);
    } 
    printf("x in outer block :%d at %p\n",x,&x);
    while(x++<33)        //原始的x
    {
        int x = 100;
        x++;
        printf("x in while loop:%d at %p\n",x,&x);
     } 
    printf("x in outer block :%d at %p\n",x,&x);

    return 0;
}

file

首先,程序创建了变量x并初始化为30,如第一条printf()语句所示。然后,定义了一个新的变量x,并设置为77,如第2条printf()语句所示。根据现实的地址可知,新变量隐藏了原始的x。第3条printf() 语句位于第1内层块后面,现实的是原始的x的值,这说明原始的测试条件中使用的是原始的x:
while(x++ < 33)
在该循环中程序创建了第3个x变量,改变量只定义在
while循环中,所以当执行到循环体中的x++时,递增为101的是新的x,然后printf()语句显示了该值,每轮迭代结束,新的x变量就消失了。然后循环的测试条件使用并递增原始的x,再次进入循环体,再创建新的x。再该例中,这个x被创建和销毁了3次。注意,该循环必须再测试条件中递增x,因为如果在循环体中递增x,那么递增的是循环体中创建的x,而非测试条件中使用的原始x。
我们使用的编译器在创建while循环体中的x时,并未复用内层块中x占用的内存,但是有的编译器会这样做。
该程序示例不是为了鼓励读者要编写类似的代码(根据C的命名规则,想要出别的变量名并不难)
,而是为了解释在内层块中定义变量的具体情况。

1.没有花括号的块

前面提到一个C99特性:作为循环或if语句的一部分,即使不适用花括号( {}),也是一个块。更完整的说,整个循环是它所在的子块(sub-block),循环体是整个循环块的子块。与此类似,if语句是一个块,与其相关联的子语句是if语句的子块。这些规则会影响到声明的变量和这些变量的作用域。如下:
//forc99.c  --新的C99块规则
#include<stdio.h>
int main(void)
{
    int n = 8;

    printf("   Initially,n = %d at %p\n",n,&n);
    for(int n=1;n<3;n++)
        printf("   loop 1: n= %d at %p\n",n,&n);
    printf("After loop 1: n= %d at %p\n",n,&n);
    for(int n=1;n<3;n++)
    {
        printf("   loop 2 index n= %d at %p\n",n,&n);
        int n =6;
        printf("   loop 2:n= %d at %p\n",n,&n);
        n++;
    }
    printf("After loop 1: n= %d at %p\n",n,&n);

    return 0;
 } 

file

2.自动变量的初始化

自动变量不会初始化,除非显示初始化它。
int main(void)
{
    int repid;
    int tents = 5;
}

file

12.1.5 寄存器变量

变量通常储存在计算机内存中。如果幸运的话,寄存器变量储存在CPU的寄存器中。由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量地址。
绝大多数方面,寄存器变量和自动变量都一样。也就是说,它们都是块作用域、无法连接和自动存储期。使用存储类别说明符register便可声明寄存器变量
int main()
{
    register int quick;
}
刚才用词幸运,是因为声明变量为register类型与直接命令相比更像是一种请求。编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,所以可能不会如你所愿。在这种情况下,寄存器变量就变成普通的自动变量。即使是这样,仍然不能对该变量使用地址运算符。
在函数头中使用关键词register,便可以请求形参是寄存器变量:
void macho(register int n)
可声明为register的数据类型有限。例如,处理器中的寄存器可能没有足够大的空间来存储double类型的值。

12.1.6 块作用域的静态变量

静态变量(static variavle)听起来自相矛盾,像是一个不可变的变量。实际上,静态的意思是该变量在内存中原地不动,并不是说它的值不变。具有文件作用域的变量自动具有(也必须是)静态存储期。前面提到过,可以创建具有静态存储期、块作用域的局部变量。这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量都不会消失。也就是说,这种变量具有块作用域、无链接,但是具有静态存储期。计算机在多次函数调用之间会记录它们的值。在块中(提供块作用域和无链接)以存储类别说明符static(提供静态存储期)声明这种变量。如下所示:
//loc_stat.c    --使用局部变量
#include<stdio.h>
void trystat(void); 

int main(void)
{
    int count;

    for(count = 1;count <=3;count++)
    {
        printf("Here coms 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++);    
}

file

注意,trystat()函数先打印再递增变量的值。
静态变量stay保存了它被递增1后的值,但是stay只在编译trystat()时被初始化一次。如果未显示初始化静态变量,它们会初始化为0。
下面两个声明很相似:
int fade = 1;
static int stay = 1;
第一条声明确实是trystat()函数的一部分,每次调用该函数时都会执行这条声明。这是运行时的行为。
第2条声明实际上并不是trystat()函数的一部分。如果逐步调试该程序会发现,程序似乎跳过了这条声明。这是因为静态变量和外部变量在程序被载入内存时已被执行完毕。把这条声明放在trystat()函数中是为了告诉编译器只有在trystat()函数才能看到该变量。这条声明并未在运行时执行。
不能再函数的形参中声明使用static:
int wontwork(static int ful);   //不允许
"局部静态变量"是描述具有块作用域的静态变量的另一个术语。
这种存储类别被称为内部静态存储类别(internal static storage class)。这里的内部指的是函数内部,而非内部链接。

12.1.7 外部链接的静态变量

外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类别有时称为外部存储类别(external storage class),属于该类别的变量称为外部变量(external variable)。把变量的定义性声明(defining declaration)放在所有的函数外面创造了外部变量。当然,为了指出该函数使用了外部变量,可以在函数中使用关键词extern再次声明。如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern在该文件中声明该变量。如下所示:
int Errupt; //外部定义的变量
double Up[100]; //外部定义的数组
extern char Coal;   //如果Coal被定义在另一个文件中,则必须这样声明

void next(void);
int main(void)
{
    extern int Errup;   //可选的声明

    extern double Up[]; //可选的声明
    。。。
}
void next(void)
{
    ...
}
注意,在main()中声明Up数组时(这是可选的声明)不用指明数组大小,因为第一次声明已经提供了数组大小的信息。main()中两条extern声明完全可以省略。因为外部变量具有文件作用域,所以Errupt和Up从声明到文件结尾都可见。它们出现在那里,仅为了说明main()函数要使用这两个变量。
如果省略掉函数中的extern关键词,相当于创建了一个自动变量。去掉下面声明中的extern:
extern int Errupt;
便称为:
int Errupt;
这使得便器在main()中创建了一个名为Errupt的自动变量。他是一个独立的局部变量,与原来的外部变量不同。该局部变量仅main()中可见,但是外部变量Errupt对于该文件的其他函数也可见。简而言之,在执行块中的语句时,块作用域中的变量将“隐藏”文件作用域中的同名变量。如果不得已要使用与外部变量同名的局部变量,可以在局部变量的声明中使用auto存储类别说明符明确表达这种意图。
外部变量具有静态存储期。因此,无论在程序执行到main()、还是next()还是其他函数,数组Up及其值都一直存在。
下面三个示例演示了外部和自动变量的一些使用情况。示例1中有个Hocus。改变量对main()和magic()均可见.
//  示例1
int Hocus;
int magic();
int main(void)
{
    extern int Hocus;   //Hocus之前已声明为外部变量
    。。
}
int magic()
{
    extern int Hocus;   //与上面的Hocus是同一个变量
}
//示例2中有一个外部变量Hocus,对两个函数均可见,这次,在默认情况下对magic()可见。
//示例2
int Hocus;
int magic();
int main(void)
{
    extern int Hocus;   //Hocus之前已声明为外部变量
    。。
}
int magic()
{
    ... //并未在该函数中声明Hocus,但是仍可使用该变量
}
//在示例3中,创建了4个独立变量。main()中的Hocus变量默认是自动变量,属于main()私有。magic()中的Hocus变量被现实声明为自动,只有magic()可用。外部变量Hocus对main()和magic()均不可见,但是对该文件中未创建局部Hocus变量的其他函数可见。最后,Pocus是外部变量,magic()可见,但是main()不可见,因为Pocus被声明在main()后面。
int Hocus;
int magic();
int main(void)
{
    int Hocus;  //声明Hocus,默认是自动变量
    。。
}
int Pocus;
int magic()
{
    auto int Hocus; //把局部变量Hocus现实声明为自动变量
    。。
}
这3个示例演示了外部变量的作用域是:从声明处到文件结尾。除此之外,还说明了外部变量的生命期。外部变量Hocus和Pocus在程序运行中一直存在,因为它们 不受限于任何函数,不会在某个函数返回后就消失。

1.初始化外部变量

外部变量和自动变量类似,也可以被显示初始化。与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为0.这一原则也使用于外部定义的数组元素。与自动变量不同的是,只能使用常量表达式初始化文件作用域变量:
int x = 10;     //可以
int y = 3 + 20; //可以
size_t z =sizeof(int);  //可以
int x2 = 2*x;   //不可以
(只要不是变长数组,sizeof表达式可被视为常量表达式)

2. 使用外部变量

下面看一个使用外部变量的示例。假设有两个函数main()和critic(),
它们都要访问变量units。可以把units声明在这两个函数的上面。
//(目的演示外部变量工作原理,并非典型用法)
//global.c  --使用外部变量
//
#include<stdio.h>
int units = 0;  //外部变量
void critic(void); 
int main(void)
{
    extern int units;   //可选的重复声明

     printf("How many pounds to a firkin of butter?\n");
     scanf("%d",&units);
     while(units!=56)
        critic();
    printf("You must have looked it up!\n");

    return 0; 
} 
void critic(void)
{   //删除了可选的重复声明
    printf("No luck,my friend.Try again.");
    scanf("%d",&units); 

}

file

critic()是如何读取units的第2个值的。当while循环结束时,main()也知道units的新值。所以main()函数和critic()都可以通过标识符units访问相同的变量。用C的术语来描述是,units具有文件作用域、外部链接和静态存储期。
把units定义在所有函数的外面,units便是一个外部变量,对units定义下面的所有函数均可见。因此,critis()可以直接使用units变量。
类似的,main()也可以直接访问units。但是main()中确实有如下声明:
extern int units;
本例中以上声明主要是为了指出 该函数要使用这个外部变量。存储类别说明符说明符extern告诉编译器,该函数如何使用units的地方都引用同一个定义在函数外部的变量。再次强调,main()和critic()使用的都是外部定义的units。

3. 外部名称

file

4. 定义和声明

//下面进一步介绍定义变量和声明变量的区别。考虑下面的例子:
int tern =1;    //tern被定义
main()
{
    extern int tern;    //使用在别处定义的tern
}
//这里tern被声明了两次,第一次声明为变量预留了存储空间,该声明构成了变量的定义。第2次声明只是为了告诉编译器使用之前已创建的tern变量,所以这不是定义。第1次声明被称为定义声明(defining declaration),第2次声明被称为引用式声明(referencing declatation)。关键字extern表明该声明不是定义,因为它只是编译器去别处查询其定义。
//假设这样写
extern int tern ;
int main(void)
{

}
//编译器会假设tern实际的定义在该程序的别处,也许在别的文件中。该声明并不会引起分配存储空间。因此不要用关键字extern创建外部定义,只用它来引用现有的外部定义。
//外部变量只能初始化一次,且必须在定该变量时进行。
//file_one.c
char permis = 'N';
...
//file_two.c
extern char permis = 'Y'; //错误
//file_two 中的声明是错误的,因为file_one.c中的定义式声明已创建并初始化了permis。

擦肩而过的概率