C Primer Plus 第12章 存储类、链接和内存管理

懵懂的女人 提交于 2019-12-03 21:40:30

12.1  存储类

C为变量提供了5种不同的存储模型,或称为存储类。还有基于指针的第6种存储模型,本章稍后(“分配内存malloc()和free()”小节)将会提到。可以按照一个变量(更一般地,一个数据对象)的存储时期来描述它,也可以按照它的作用域(SCOPE)以及它的链接(linkage)来描述它。存储时期就是变量在内存中保留的时间,变量的作用域和链接一起表明程序的哪些部分可以通过变量名来使用该变量。不同的存储类提供了变量的作用域、链接以及存储时期的不同组合。

您可以拥有供多个不同的源代码文件共享的变量、某个特定文件中所有函数都可以使用的变量、只有在某个特定函数中才可以使用的变量、甚至只有某个函数的一小部分内可以使用的变量。

您可以拥有在整个程序运行期间都存在的变量,或者只有在包含该变量的函数执行时才存在的变量。您也可以使用函数调用 为数据的存储显示的分配和释放内存。

12.1.1  作用域

作用域描述了程序中可以访问一个标识符的一个或多个区域。

一个C变量的作用域可以是代码块作用域、函数原型作用域,或者文件作用域

到目前为止的程序实例中使用的都是代码块作用域变量。在代码块中定义的变量具有代码块作用域(block scope),从该变量被定义的地方到包含该定义的代码块的末尾该变量均可见。另外,函数的形式参量尽管在函数的开始花括号前进行定义,同样也具有代码块作用域,隶属于包含函数体的代码块。

因此,下面的代码中的变量cleo和patrick都有直到结束花括号的代码块作用域。

double blocky(double cleo)
{
    double patrick = 0.0;
    ...
    return patrick;
}

在一个内部代码块中声明的变量,其作用域只局限于该代码块:

double blocky(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。

传统上,具有代码块作用域的变量都必须在代码块开始处进行声明。C99放宽了这一规则,允许在一个代码块中任何位置声明变量。一个新的可能是变量声明可以出现在for循环的控制部分,也就是说,现在可以这样做:

for (int i=0;i<10;i++)
    printf("A C99 feature: i = %d",i);

作为这一新功能的一部分,C99把代码块的概念扩大到包括由for循环、while循环、do while循环或者if语句所控制的代码---即使这些代码并没有用花括号括起来。因此在前述for循环中,变量i被认为是for循环代码块的一部分。这样 它的作用域就限于这个for循环,程序的执行离开该for循环后就不再能看到变量i了。

函数原型作用域(function prototype scope)适用于函数原型中使用的变量名,如下所示:

int mighty(int mouse,double large);

函数原型作用域从变量定义处一直到原型声明的末尾。这意味着,编译器在处理一个函数原型的参数时,它所关心的只是该参数的类型;您使用什么名字,通常是无关紧要的,不需要使它们和函数定义中使用的变量名保持一致。名字起作用的一种情形是变长数组参量:

void use_a_VLA(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()中都可以使用它。因为它们可以在不止一个函数中使用,文件作用域变量通常也被称为全局变量(global variable)。

另外,还有一种被称为函数作用域(function scope)的作用域,但它只适用于goto语句使用的标签。函数作用域意味着一个特定函数中的goto标签对该函数中任何地方的代码都是可见的,无论该标签出现在哪一个代码块中。

12.1.2  链接

一个C变量具有下列链接之一:外部链接(external linkage),内部链接(internal linkage),或者空链接(no linkage)。

具有代码块作用域或者函数原型作用域的变量具有空链接,意味着它们是由其定义所在的代码块或函数原型所私有的。具有文件作用域的变量可能有内部或外部链接。一个具有外部链接的变量可以在一个多文件程序 的任何地方使用。一个具有内部链接的变量可以在一个文件的任何地方使用。

那么怎么样知道一个文件作用域变量具有内部链接还是外部链接?您可以看看在外部定义中是否使用了存储类说明符static:

int giants=5;  //文件作用域,外部链接
static int dodgers=3;  //文件作用域,内部链接
int main()
{
    ...
}
    ...

和该文件属于同一程序的其他文件可以使用变量giants。变量dodgers是该文件私有的,但是可以被该文件中的任一函数使用。        

12.1.3  存储时期

一个C变量有以下两种存储时期:静态存储时期(static storage duration)和自动存储时期(automatic storage duration)。如果一个变量具有静态存储时期,它在程序执行期间将一直存在具有文件作用域的变量具有静态存储时期。注意,对于具有文件作用域的变量,关键词static表明链接类型,并非存储时期。一个使用static声明了的文件作用域变量具有内部链接,而所有的文件作用域变量,无论它具有内部链接,还是具有外部链接,都具有静态存储时期。

具有代码块作用域的变量一般情况下具有自动存储时期。在程序进入定义这些变量的代码块时,将为这些变量分配内存;当退出这个代码块时,分配的内存将被释放。该思想把自动变量使用的内存视为一个可以重复使用的工作区或者暂存内存。例如,在一个函数调用结束后,它的变量所占用的内存可以被用来存储下一个被调用函数的变量。

迄今为止我们使用的局部变量都属于自动类型。例如,在下列的代码中,变量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;
}

C使用作用域、链接和存储时期来定义5种存储类自动、寄存器、具有代码块作用域的静态、具有外部链接的静态和具有内部链接的静态。

5种存储类

存储类 时期 作用域 链接 声明方式
自动 自动 代码块 代码块内
寄存器 自动 代码块 代码块内,使用关键词register
具有外部链接的静态 静态 文件 外部 所有函数之外
具有内部链接的静态 静态 文件 内部 所有函数之外,使用 static
空链接的静态 静态 代码块 代码块内使用关键字 static

12.1.4  自动变量

属于自动存储类的变量具有自动存储时期、代码块作用域和空链接。默认情况下,在代码块或函数的头部定义的任意变量都属于自动存储类。也可以如下面所示的那样显式地使用关键字auto使您的这一意图更加清晰:

int main(void)
{
    auto int plox;
...

例如,为了表明有意覆盖一个外部函数定义时,或者为了表明不能把变量改变为其他存储类这一点很重要时,可以这样做。关键字auto称为存储类说明符(storage class specifier)。

代码块作用域和空链接意味着只有变量定义所在的代码块才可以通过名字访问该变量(当然,可以用参数向其他函数传送该变量的地址和值,但那是以间接的方式知道的)。另一个函数可以使用具有同样名字的变量,但那将是存储在不同内存位置中的一个独立变量。

再来看一下嵌套代码块。只有定义变量的代码块及其内部的任何代码块可以访问这个变量:

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仅在内层花括号中是可见的。如果试图在内层代码块之前 或之后使用该变量,将得到一个编译错误。通常,设计程序时不使用这一特性。然而有些时候,如果其他地方 用不到这个变量的话,在子代码 块中定义 一个变量是有用的。通过这种方式 ,您可以在使用变量的位置附近说明变量的含义 。而且,变量只会在需要 它的时候 才占用内存。变量n和m在函数头部和外层代码块中定义 ,在整个函数中可用,并一直存在直到函数终止。

如果在内层代码块定义了一个具有和外层代码块变量同一名字的变量,将发生什么?那么在内层代码块定义的名字是内层代码块所使用的变量。我们称之为内层定义覆盖了(hide)外部定义,但当运行离开内层代码块时,外部变量重新恢复作用。

程序清单hiding.c程序

#include <stdio.h>
int main()
{
    int x = 30;  /*初始化x*/
    printf("x in outer block:%d\n",x);
    {
        int x = 77;  /*新的x,覆盖第一个x */
        printf("x in inner block:%d\n",x);
    }
    printf("x in outer block:%d\n",x);
    while (x++ < 33)
    {
        int x = 100; /*新的x,覆盖第一个x*/
        x++;
        printf("x in while loop:%d\n",x);
    }
    printf("x in outer block:%d\n",x);
    return 0;
}

输出如下:
x in outer block: 30
x in inner block: 77
x in outer block: 30
x in whlie loop: 101
x in whlie loop: 101
x in whlie loop: 101
x in outer block: 34 

该语句最令人迷惑的地方也许是while循环。这个while循环的判断使用了起始的x:

while (x++ < 33)

然而在循环内部,程序看到了第三个x的变量,即在while循环代码内定义的一个变量。因此,当循环体中的代码 使用x++时,是新的x被递增到101,接着被显示。每次循环结束以后,新的x 就消失了。然后循环条件判断语句使用并递增起始的x,又进入循环代码块,再次创建新的x。注意,该 循环必须在条件判断语句中递增x,因为若在循环体内递增x的话,递增的将是另一个x而非判断所用的那个x。

一、不带{ }的代码块

先前曾提到C99有一个特性,语句若为循环或者if语句的一部分,即使没有使用{},也认为是一个代码块。更完整的说,整个循环是该 循环所在代码 块的子代码 块,而循环体是整个循环代码 块的子代码 块。与之类似,if语句 是一个代码 块,其相关子语句 也是if 语句的子代码块。这一规则影响到您能够在休息定义 变更 以及该 变量的作用域。程序清单12.2显示 了在一个for 循环中该 特性是如何作用的。

程序清单 12.2 for c99.c程序

#include <stdio.h>
int main()
{
    int n = 10;

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

    return 0;
}

/*输出如下,假设编译器支持这一特定的c99特性
Initially,n = 10
loop 1 :n = 1
loop 1 :n = 2
After loop 1,n = 10
loop 2 index n = 1
loop 2 n = 30
loop 2 index n = 2
loop 2 n = 30
After loop 2, n = 10
*/

在第一个for循环的控制部分中声明 的n到该 循环末尾一直起作用,覆盖了初始的n。但在运行完该 循环后,初始的n恢复作用。

在第二个for 循环中,n 声明为一个循环索引,覆盖了初始的n。接着,在循环体内声明的n覆盖了循环索引 n。当程序执行完循环体后,在循环体内声明 的n消失,循环判断使用索引n。整个循环终止 时,初始的n又恢复作用。

二、自动变量的初始化

除非您显式的初始化自动变量,否则它不会被自动初始化。考虑下列声明:
 

int main(void)
{
    int repid;
    int tents = 5;

变量tents初始化为5,而变量repid的初始值则是先前占用分配给它的空间的任意值。不要指望这个值是0。倘若一个非常量 表达式中所用到的变量先前都定义过的话,可将自动变量初始化为该表达式:

int main(void)
{
    int ruth = 1;
    int rance = 5 * ruth; //使用先前定义过的变量

12.1.5  寄存器变量

通常,变量存储在计算机内存中。如果 幸运,寄存器变量可以被存储在CPU寄存器中,或者更一般地,存储在速度最快的可用内存中,从而可以比普通变量更快地被访问和操作。因为寄存器变量多是存放 在一个寄存器而非内存中,所以无法获得寄存器变量的地址。但在其他许多方面,寄存器变更和自动变量是一样的。也就是说,他们都具有代码块作用域、空链接以及自动的存储时期。通过使用存储类说明符register可以声明寄存器变量:

int main(void)
{
    register int quick;
...

我们说“如果幸运”是因为声明一个寄存器变量仅是一个请求,而非一条直接指令。编译器必须在您的请求与可用寄存器的个数或可用调整内存的数量之间做个权衡,所以您可能达不到自己的愿望。这种情况下,变量变成一种普通的自动变量;然而,您依然不能对他使用地址运算符。

可以把一种形式参量请求为寄存器变量。只需在函数头部使用register关键字:

void macho(register int n)

可以使用register声明的类型是有限的。例如,处理器可能没有足够大的寄存器来容纳double类型。

12.1.6  具有代码块作用域的静态变量

静态变量(static variable)这一名称听起来很矛盾,像是一个不可变的变量。实际上,“静态”是指变量的位置固定不动。具有文件作用域的变量自动(也是必须)具有静态存储时期。也 可以创建具有代码块作用域,兼具静态存储的局部变量。这些变量和自动变量具有相同的作用域,但当包含这些变量的函数完成工作时,它们并不消失 。也就是说,这些变量具有代码块作用域、空链接,却有静态存储时期从一次函数调用到下一次调用 ,计算机都记录着它们的值这样的变量通过使用存储类说明符static(这提供了静态存储时期)在代码块内声明(这提供了代码块作用域和空链接)创建。程序清单12.3中的例子说明了这一技术。

程序清单12.3 loc_stat.c程序

/* loc_stat.c --使用一个局部静态变量*/
#include <stdio.h>
void trystat(void);
int main(void)
{
    int count;

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

注意,trystat()在打印出每个变量的值后递增变量。运行程序将返回下列结果:

Here comes iteration 1:

fade = 1 and stay = 1

Here comes iteration 2:

fade = 1 and stay = 2

Here comes iteration 3:

fade = 1 and stay = 3

静态变量stay记得它的值曾被加1,而变量fade每次都重新开始。这表明了初始化的不同:在每次调用trystat()时fade都被初始化,而stay只在编译trystat()时被初始化一次。如果不显式的对静态变量进行初始化,它们将被初始化为0.

下面的两个声明看起来很相似:

int fade = 1;

static int stay = 1;

然而,第一个语句确实 是函数trystat()的一部分,每次调用该函数时都会执行它。它是个运行时的动作。而第二个语句 实际 上并不是函数trystat()的一部分。如果用调试程序逐步运行该程序,您会发现程序看起来跟过了那一步。那是因为静态变量和外部变量在 程序调入内存时 已经就位了。把这个语句放在trystat()函数中是为了 告诉编译器只有函数trystat()可以看到该 变量它不是在运行时执行的语句。

对函数参量不能使用static:

int wontwork( static int flu);  //不允许

12.1.7  具有外部链接的静态变量

具有外部链接的变量具有文件作用域、外部链接和静态存储时期这一类型有时被称为外部存储类(external storage class),这一类型的变量被称为外部变量(external variable)。把变量的定义声明放在所有函数之外 ,即创建了一个外部变量。为了使程序更加清晰可以在使用外部变量的函数中通过使用extern关键字来再次声明它如果变量是在别的文件中定义的,使用extern来声明该变量就是必须的。应该像这样声明:

int Errupt;                    /*外部定义的变量    */
double Up[100];                /*外部定义的数组    */
extern char Coal;              /*必须的声明       */
                               /*因为Coal在其他文件中定义*/

void next(void);
int main(void)
{
    extern int Errupt;         /*可选的声明       */
    extern double Up[];        /*可选的声明       */
    ...
}
void next(void)
{
    ...
}

Errupt 的再次声明是个链接的例子,因为它们都指向同一变量。

请注意不必在double Up的可选声明中指明数组大小。第一次声明已经提供了这一信息。因为外部变量具有文件作用域,它们从被声明外到文件结尾都是可见的,所以main()中的一组extern声明完全可以省略掉。而它们出现在那里,作用只不过是表明main()函数使用这些变量。

如果函数中的声明漏掉了extern ,就会建立 一个独立 的自动变量。也就是说,如果在main()中用:

extern int Errupt ;

替换

int Ettupt ;

将使编译器创建一个名为Errupt的自动变量。它将是一个独立的局部变量,而不同于初始的Errupt。在程序执行main()时该 局部变量会起作用;但在像next()这种同一文件内的其他函数中,外部的Errupt将起作用。简言之,在程序执行代码块内语句时,代码块作用域的变量覆盖了具有文件作用域的同名变量。

外部变量具有静态存储时期。因此,数组Up一直存在并保持其值,不管程序是否在执行main( )、next( )还是其他函数。

下列的3个例子展示了外部变量和自动变量的4种可能组合。

例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()通过默认的方式获知外部变量。

/*例1*/
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()之后 。

/*例3*/
int Hocus;
int magic();
int main(void)
{
    int Hocus;        //声明Hocus默认为自动变量
    ...
}
int Pocus;
int magic()
{
    auto int Hocus;        //把局部变量Hocus显式地声明为自动变量
...
}

这些例子说明了外部变量的作用域:从声明的位置开始到文件结尾为止。它们也说明了外部变量的生存期。外部变量Hocus和Pocus存在的时间与程序运行时间一样,并且他们不局限于任一函数,在一个特定函数返回时并不消失 。

一、外部变量初始化

和自动变量一样,外部变量可以被显式地初始化。不同于外部变量的是,如果您不对外部变量进行初始化,它们将自动被赋初始值0这一原则也适用于外部定义的数组元素。不同于自动变量,只可以用常量表达式来初始化文件作用域变量

int x = 10;                //可以,10是常量 
int y = 3 + 20;            //可以,一个常量表达式
size_t z = sizeof(int);    //可以,一个常量表达式
(只要类型不是一个变长数组,sizeof表达式就被认为是常量表达式)

int x2 = 2*x;              //不可以,x 是一个变量

二、外部变量的使用

假设需要两个分别叫作main()和critic()的函数来访问变量units。可以如程序清单12.4所示,在这两个函数之外的开始处声明变量units。

程序清单12.4  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,chummy.Try again.\n");
    scanf("%d",&units);
}

下面是一个输出示例

How many pounds to a firkin of butter?
14
No luck,chummy.Try again.
56
You must have looked it up!

注意函数critic()是怎么样读取units的第二个值的;当main()结束while循环时,也知道 了新值。因此,两个函数main()和critic()都用标识符units来访问同一个变量。在c的术语中,称units具有文件作用域、外部链接以及静态存储时期。

通过在所有函数定义的外面定义变量units,它成为一个外部变量。要使units对文件中随后的全部函数可用,只需要像前面这样做即可。

来看一些细节。首先,units声明所在的位置使得它对后的函数可用,而不需采取其他任何操作。这样,critics()就可以使用变量units。

与之类似,也不需要做任何事来允许main()访问units。然而,main()中确实有如下声明:

extern int units;

在这个例子中,声明主要是使程序的可读性更好。存储类说明符extern告诉编译器在该函数中用到的units都是指同一个在函数外部(甚至在文件之外)定义的变量。再次,main()和critic()都使用了外部定义的units。

三、外部名字

c99标准要求编译器识别局部标识前63个字符和外部标识符的前31个字符。

对外部变量名字规定比对局部变量名字规定更严格, 是因为外部名字需要遵守局部环境规则,而该规则可能是有更多的限制。

四、定义和声明

我们来更为仔细的看一下变量定义和变量声明的区别。考虑下面的例子:

int tern = 1;          /*定义tern*/
main( )
{
    extern int tern;   /*使用在其他地方定义的tern变量*/

这里,tern声明了两次。第一次声明为变更留出了存储空间,它构成了变量的定义 。第二次声明只是告诉编译器要使用先前定义的变量tern,因此不是一个定义。第一次声明被称为定义声明(defining declaration),第二次声明称为引用声明(referencing declaration)。关键字extern表明该声明不是一个定义,因为它指示编译器参考其他地方。

如果这样做:

extern int tern;
int main(void)
{

那么编译器假定tern的真正定义在程序中其他某个地方,也许是在另一个文件中。这样的声明不会引起空间分配 。因此,不要用关键字extern来进行外部定义;只用它来引用一个已经存在的外部定义。

一个外部变量只可进行一次初始化,而且一定是在变量被定义时进行。

下面的语句是错误的:

extern char permis = 'Y';  /*错误*/

因为关键字extern的存在标志着这是一个引用声明,而非定义声明。

12.1.8  具有内部链接的静态变量

这种存储类的变量具有静态存储时期、文件作用域以及内部链接。通过使用存储类说明符static在所有函数外部进行定义(正如定义外部变量那样)来创建一个这样的变量:

static int svil = 1;  //具有内部链接的静态变量
int main(void)
{

普通的外部变量可以被程序的任一文件中所包含的函数使用,而具有内部链接的静态变量只可以被与它在同一个文件中的函数使用。可以在函数中使用存储类说明符extern来再次声明任何具有文件作用域的变量。这样的声明并不改变链接。例如:

int traveler = 1;          //外部链接
static int stayhome = 1;   //内部链接
int main()
{
    extern int traveler;  //使用全局变量traveler 
    extern int stayhome;  //使用全局变量stayhome

 对于这个文件来说,traveler 和stayhome都是全局变量,但只有traveler 可以被其他文件中的代码使用。使用extern的两个声明表明main()在使用两个全局变量,但stayhome仍具有内部链接。

12.1.9  多文件

复杂的C程序往往使用多个独立的代码文件。有些时候,这些文件可能需要共享一个外部变量。ANSI C通过在一个文件中定义变量,在其他文件中引用 声明这个变量来实现共享。也就是说,除了一个声明(定义声明)外,其他所有声明都必须使用关键字extern,并具只有在定义声明中才可以对该变量进行初始化。

注意:除非在第二个文件也声明 了该 变量(通过使用extern),否则在一个文件定义的外部变量不可以用于第二个文件。一个外部变量声明本身只是使一个变量可能对其他文件可用。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!