• C Primer Plus(6) 中文版 第12章 存储类别、链接和内存管理 12.1 存储类别


    程序员通过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的声明创建了一个可容纳10个int类型元素的对象,该数组的每个元素也是一个对象。
    如果可以使用左值改变对象中的值,该左值就是一个可修改的左值(midifiable 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 block( 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;

    以前,具有块作用域的变量都必须声明在块的开头。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 scope)仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。
    函数原型作用域(function prototype scope)用于函数原型中的形参名(变量名),如下所示:
    int mighty( int mouse, double large );
    函数原型作用域的范围是从形参定义到原型声明结束。这意味着,编译器处理函数原型中的形参时只关心它的类型,而形参名(如果有的话)通常无关紧要。而且,即使有形参名,也不必与函数定义中的形参名相匹配。只有在变长数组中,形参名才有用:
    void use_a_VLA( int n, int m, int ar[n][m] );
    方括号必须使用在函数原型中已声明的名称。
    变量的定义在函数的外面,具有文件作用域(file scope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。例如:
    #include
    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( void ){
        ...

    该文件和同一程序的其他文件都可以使用变量giants。而变量dodgers属文件私有,该文件中的任意函数都可使用它。
    12.1.3 存储期
    作用域和链接描述了标识符的可见性。存储器描述了这些标识符访问的对象的生存期。C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期
    如果对象具有动态存储期,那么它在程序的执行期间一直存在。文件作用域变量具有静态存储器。注意,对于文件作用域变量,关键字static表明了其链接属性,而非存储期。以static声明的文件作用域变量具有内部链接。但是无论是内部链接还是外部链接,所有的文件作用域变量都具有静态存储期。
    线程存储期用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字Thread local声明一个对象时,每个线程都获得该变量的私有备份。
    块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存,当退出这个块时,释放刚才为变量分配的内存。这种做法相当于把自动变量占用的内存视为一个可重复使用的工作区或暂存区。例如,一个函数调用结束后,其变量占用的内存可用于存储下一个被调用函数的变量。
    变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。 
    下面的代码中,变量number和index在每次调用bore()函数时被创建,在离开函数时被销毁:
    void bore( int number ){
        int index;
        ...
        return;

    然而,块作用域变量也能具有静态存储期。为了创建这样的变量,要把变量声明在块中,且在声明前面加上关键字static:
    void more( int number ){
        int index;
        static int ct = 0;
        ...
        return;

    这里,变量ct存储在静态内存中,它从程序被载入到程序结束期间都存在。但是,它的作用域定义在more()函数块中。只有在执行该函数时,程序才能访问它所指定的对象(但是,该函数可以给其他函数提供该存储该存储区的地址以便间接访问该对象,例如通过指针形参或返回值)。
    C使用作用域、链接和存储期为变量定义了多种存储方案。本书不涉及并发程序设计,动态分配存储期在后面介绍。因此,剩下5种存储类别:自动、寄存器、静态块作用域、静态外部链接、静态内部链接。 
                表12.1 5种存储类型
    存储类别        存储期    作用域     链接    声明方式 
    自动                自动        块            无        块内
    寄存器            自动        块            无        块内,使用关键字register
    静态外部链接 静态        文件         外部    所有函数外
    静态内部链接 静态        文件         内部    所有函数外,使用关键字static
    静态无链接     静态        块            无        块内,使用关键字static
    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 -- variables in blocks
    #include
    int main()
    {
        int x = 30;      // original x
        
        printf("x in outer block: %d at %p\n", x, &x);
        {
            int x = 77;  // new x, hides first 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) // original x
        {
            int x = 100; // new x, hides first x 
            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;

    /* 输出:

    */

    在while循环里的语句int x = 100;创建了3次,也销毁了3次。
    我们使用的编译器在创建循环体中的x时,并未复用内存块中x占用的内存,但是有些编译器会这样做。
    1. 没有花括号的块
    C99特性:作为循环或if语句的一部分,即使不使用花括号({}),也是一个块。更完整地说,整个循环是它所在块的子块(sub-block),循环体是整个循环块的子块。与此类似,if语句是一个块,与其相关联的子语句是if语句的子块。这些规则会影响到声明的变量和这些变量的作用域。
    for循环中该特性的用法。
    // forc99.c -- new C99 block rules
    #include
    int main()
    {
        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 2, n = %d at %p\n", n, &n);
        
        return 0;

    /* 输出:

    */ 

    没必要在程序中使用相同的变量名。如果用了,各变量的情况如上所述。
    注意 支持C99和C11
    有些编译器(Microsoft Visual Studio 2012就是其中之一)并不支持C99/C11的这些作用域规则。有些编译器会提供激活这些规则的选项。例如,撰写本书时,gcc默认支持了C99的许多特性,但是要用-std=99选项激活forc99.c中使用的特性:
    gcc -std=c99 forc99.c
    与此类似,gcc或clang都要使用-std=c1x或-std=c11选项,才支持C11特性。
    2.自动变量的初始化
    自动变量不会初始化,除非显式初始化它。例如:
    int main( void ){
        int repid;
        int tents = 5;
    }
    tents变量被初始化为5,但是repid变量的值是之前占用分配给repid的空间中的任意值(如果有的话),别指望这个值是0。可以用非常量表达式(non-constant expression)初始化自动变量,前提是所用的变量已在前面定义过:
    int main( void ){
        int ruth = 1;
        int rances = 5 * ruth; //使用之前定义的变量 
    }
    12.1.5 寄存器变量
    变量通常存储于计算机内存中。如果幸运的话,寄存器变量存储在CPU的寄存器中,或者概括地说,存储在最快的可用内存中。与普通变量相比,访问和处理这种变量的速度更快。由于寄存器变量存储在寄存器而非内存中,所以无法获取寄存器的地址。绝大多数方面,寄存器变量和自动变量一样。也就是说,它们都是块作用域、无链接和自动存储期。使用存储类型说明符register便可声明寄存器变量:
    int main( void ){
        register int quick;
    刚才说“如果幸运的话”,是因为声明变量为register类别与直接命令相比更像是一种请求。编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,所以可能不会如你所愿。这种情况下,寄存器变量就变成普通的自动变量。即使是这样,仍然不能对该变量使用地址运算符。
    在函数头中使用关键字register,便可请求形参是寄存器变量:
    void macho( register int n )
    可声明为register的数据类型有限。例如,处理器中的寄存器可能没有足够大的空间来存储double类型的值。
    12.1.6 块作用域的静态变量
    静态变量(static variable)听起来自相矛盾,像是一个不可变的变量。实际上,静态的意义是该变量在内存中原地不动,并不是它的值不变。具有文件作用域的变量自动具有(也必须是)静态存储期。可以创建具有静态存储期、块作用域的局部变量。这些变量和普通变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。也就是说,这种变量具有块作用域、无链接、但是具有静态存储期。计算机在多次调用之间会记录它们的值。在块中以存储类别说明符static(提供静态存储期)声明这种变量。
    /* loc_stat.c -- using a local static variable */
    #include
    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++);
    }
    /* 输出:

    */ 

    stay只在编译trystat()是被初始化一次。如果未显式初始化,它们会被初始化为0。
    下面两个声明很相似:
    int fade = 1;
    static int stay = 1;
    第1条声明确实是trystat()函数的一部分,每次调用该函数时都会执行这条声明。这是运行时行为。第2条声明实际上不是trystat()函数的一部分。如果逐步调试该程序会发现,程序似乎跳过了这条声明。这是因为静态变量和外部变量在程序被载入内存时已执行完毕。 把这条声明放在trystat()函数中是为了告诉编译器只有trystat()函数才能看到该变量。这条声明并未在运行时执行。
    不能在函数的形参中使用static:
    int wontwork( static int flu ); //不允许
    “局部静态变量”是描述具有块作用域的静态变量的另一个术语。阅读一些老的C文献时,这种有存储类别被称为内部静态存储类别(internal static storage class)。这里的内部指的是函数内部,而非内部链接。
    12.1.7 外部链接和静态变量
    外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类别有时称为外部存储类别(external storage class),属于该类别的变量称为外部变量(external variable)。把变量的定义性声明(defining declaraction)放在所有函数的外面便创建了外部变量。当然,为了指出该函数使用了外部变量,可以在函数中用关键字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 ){
        ...
    }
    注意,在main()中声明Up数组时(这是可选声明)不用指明数组大小,因为第1次声明已经提供了数组大小信息。main()中的两条extern声明完全可以省略,因为外部变量具有文件作用域,所以Errupt和Up从声明处到文件结尾处都可见。它们出现在那里,仅为了说明main()函数要使用这两个变量。
    如果省略掉函数中的extern关键字,相当于创建了一个自动变量。
    简而言之,在执行块中的语句时,块作用域的变量将“隐藏”文件作用域中的同名变量。如果不得已要使用与外部变量同名的局部变量,可以在局部变量的声明中使用auto存储类别说明符明确表达这种意图。
    外部变量具有静态存储期。
    演示外部和自动变量的一些使用情况。
    /*示例1*/
    int Hocus;    //对其之后的所有函数都可见 
    int magic();
    int main( void ){
        extern int Hocus; //Hocus之前已声明为外部变量
        ... 

    int magic(){
        extern int Hocus; //与上面的Hocus是同一变量 
        ... 
    }
    /*示例2*/
    int Hocus;
    int magic();
    int main( void ){
        extern int Hocus; //Hocus之前已声明为外部变量
        ... 

    int magic(){
        ...             //并未在该函数中声明Hocus,但是仍可使用该变量 
    }
    /*示例3*/
    int Hocus;      
    int magic();
    int main( void ){
        int Hocus; //声明为Hocus,默认是自动变量 
        ... 

    int Pocus;  //外部变量,对main()函数不可见 
    int magic(){
        auto int Hocus; //把局部变量Hocus显示声明为自动变量 
        ...             
    }
    1.初始化外部变量
    外部变量和自动变量类似,也可以被显式初始化。与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为0。这一原则也适用外部定义的数组元素。与自动变量的情况不同,只能使用常量表达式初始化文件作用域变量:
    int x = 10; //没问题,10是常量
    int y = 3 + 20; //没问题,用于初始化的是常量表达式
    size_t z = sizeof(int); //没问题,用于初始化的是常量表达式
    int x2 = 2 * x;            //不行,x是变量
    (只要不是变长数组,sizeof表达式可被视为常量表达式)
    2.使用外部变量
    演示外部变量的工作原理,并非它的典型用法
    /* global.c  -- uses an external variable */
    #include
    int units = 0;         /* an external variable      */
    void critic(void);
    int main(void)
    {
        extern int units;  /* an optional redeclaration */
        
        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)
    {
        /* optional redeclaration omitted */
        printf("No luck, my friend. Try again.\n");
        scanf("%d", &units);

    /* 输出:

    */ 

    3.外部名称 
    C99和C11标准都要求编译器识别局部标识符的前63个字符和外部标识符的前31个字符。这修订了以前的标准,即编译器识别标识符前31个字符和外部标识符前6个字符。外部变量名比内部变量名的规则严格,是因为外部变量名还要遵循局部环境规则,所受的限制更多。
    4.定义的声明
    int tern = 1; /*tern被定义*/
    main() {
        extern int tern; //使用在别处定义的tern 

    这里,tern被声明了两次。第1次声明为变量预留了存储空间,该声明构成了变量的定义。第2次声明只告诉编译器使用之前已创建的tern变量,所以这不是定义。第1次声明被称为定义式声明(defining declaration),第2次声明被称为引用式声明(referencing declaration)。关键字extern表明该声明不是定义,因为它指示编译器去别处查询其定义。
    假设可以这样写:
    extern int tern;
    int main( void ){

    编译器会假设tern实际的定义在该程序的别处,也许在别的文件中。该声明并不会引起分配存储空间。因此,不要用关键字extern创建外部定义,只用它来引用现有的外部定义。
    外部变量只能初始化一次,且必须在定义该变量时进行。例如:
    //file_one.c
    char permis = 'N';
    ...
    //file_two.c
    extern char permis = 'Y'; //错误
    12.1.8 内部链接的静态变量
    该存储类别的变量具有静态存储期、文件作用域和内部链接。用存储类别说明符static定义的变量具有这种存储类别。
    static int svil = 1; //静态变量,内部链接
    这种变量过去称为外部静态变量(external static variable),但是这个术语有点自相矛盾(这些变量具有内部链接)。但是,没有合适的新简称,所以只能用内部链接的静态变量(static variable with internal linkage)。普通的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数,可以使用存储类别说明符extern,在函数中重复声明任何具有文件作用域的变量。这样的声明并不会改变其链接属性。例如下面的代码:
    int traveler = 1; //外部链接
    static int stayhome = 1; //内部链接
    int main() {
        extern int traveler; //使用定义在别处的travelerf,链接属性为外部链接 
        extern int stayhome; //使用定义在别处的stayhome,链接属性为内部链接 

    12.1.9 多文件
    只有当程序由多个翻译单元组成时,才体现区别内部链接和外部链接的重要性。
    复杂的C程序通常是由多个单独的源代码文件组成。有时,这些文件可能要共享一个外部变量。C通过在一个文件中进行定义时声明,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用extern关键字。而且,只有定义声明才能初始化变量。
    注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用extern关键字)。也就是说,在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能直接使用它。
    12.1.10 存储类别说明符
    关键字static和extern的含义取决于上下文。C语言有6个关键字作为存储类别说明符:auto、register、static、extern、Thread local和typedef。typedef关键字与任何内存存储无关,把它归于此类有一些语法上的原因。尤其是,在绝大多数情况下,不能在声明中使用多个存储类别说明符,所以这意味着不能使用多个存储类别说明符作为typedef的一部分。唯一例外的是_Thread_local,它可以和static或extern一起使用。
    auto说明符表明变量是自动存储期,只能用于块作用域的变量声明中。由于在块中声明的变量本身就具有自动存储期,所以使用auto主要是为了明确表达要使用与外部变量同名的局部变量的意图。
    register说明符也只用于块作用域的变量,它把变量归为寄存器存储类别,请求最快速度访问该变量。同时,还保护了该变量的地址不被获取。
    static说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果static用于文件作用域声明,作用域受限于该文件。如果static用于块作用域声明,作用域则受限于该块。因此,只要程序在运行对象就存在并保留其值,但是只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接。文件作用域的静态变量具有内部链接。
    小结:存储类别
    自动变量具有块作用域、无链接、自动存储期。它们是局部变量、属于其定义所在块(通常指函数)私有。寄存器变量的属性和自动变量相同,但是编译器会使用更快的内存或寄存器存储它们。不能获取寄存器变量的地址。
    具有静态存储期的变量可以具有外部链接、内部链接或无链接。在同一个文件所有函数的外部声明的变量是外部变量,具有文件作用域、外部链接和静态存储期。如果在这种声明前面加上关键字static,那么其声明的变量具有文件作用域、内部链接和静态存储期。如果在函数中用static声明一个变量,则该变量具有块作用域、无链接、静态存储期。
    具有自动存储期的变量,程序在进入该变量的声明所在块时才为其分配内存,在退出该块时释放之前分配的内存。如果未初始化,自动变量中是垃圾值。程序在编译时为具有静态存储期的变量分配内存,并在程序的运行过程中一直保留这块内存。如果未初始化,这样的变量会被设置为0。
    具有块作用域的变量是局部的,属于包含该块的块私有。具有文件作用域的变量对文件(或翻译单元)中位于其声明后面的所有函数可见。具有外部链接的文件作用域变量,可用于该程序的其他翻译单元。具有内部链接的文件作用域变量,只能用于其声明所在的文件内。
    好的设计可以不需要使用文件作用域变量。
    // parta.c --- various storage classes
    // compile with partb.c
    #include
    void report_count();
    void accumulate(int k);
    int count = 0;       // file scope, external linkage

    int main(void)
    {
        int value;       // automatic variable
        register int i;  // register variable
        
        printf("Enter a positive integer (0 to quit): ");
        while (scanf("%d", &value) == 1 && value > 0)
        {
            ++count;     // use file scope variable
            for (i = value; i >= 0; i--)
                accumulate(i);
            printf("Enter a positive integer (0 to quit): ");
        }
        report_count();
        
        return 0;
    }

    void report_count()
    {
        printf("Loop executed %d times\n", count);

    // partb.c -- rest of the program
    // compile with parta.c
    #include

    extern int count;       // reference declaration, external linkage

    static int total = 0;   // static definition, internal linkage
    void accumulate(int k); // prototype

    void accumulate(int k)  // k has block scope, no linkage
    {
        static int subtotal = 0;  // static, no linkage
        
        if (k <= 0)
        {
            printf("loop cycle: %d\n", count);
            printf("subtotal: %d; total: %d\n", subtotal, total);
            subtotal = 0;
        }
        else
        {
            subtotal += k;
            total += k;
        }
    }

    /* 输出:

    */ 

    由于parta.c调用了accumulate()函数,所以必须包含accumulate()函数的原型。而partb.c只包含了accumulate()函数的定义,并未在文件中调用该函数。所以其原型为可选(即省略原型也不影响使用)。 
    12.1.11 存储类别和函数
    函数也有存储类别,可以是外部函数(默认)或静态函数。C99新增了第3种类别---内联函数。外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。假设一个文件中包含了以下函数原型:
    double gamma( double ); /*该函数默认为外部函数*/
    static double beta( int, int ); 
    extern double delta( double, int );
    在同一个程序中,其他文件中的函数可以调用gamma()和delta(),但是不能调用beta(),因为以static存储类别说明符创建的函数属于特定模块私有。这样做避免了名称冲突的问题,由于beta()受限于它所在的文件,所以在其他文件中可以使用与之同名的函数。
    通常的做法是:用extern关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数定义在别处。除非使用static关键字,否则一般函数声明都默认为extern。
    12.1.12 存储类别的选择
    对于“使用哪种存储类别”的回答绝大多数是“自动存储类别”,要知道默认存储类别就是自动存储类别。初学者可能认为外部存储类别很不错,为何不把所有的变量都设置为外部变量,这样就不必使用参数和指针在函数间传递信息了。然后,这背后隐藏着一个陷阱。如果这样做,A()函数可能违背你的意图,私下修改B()函数使用的变量。多年来,无数程序员的经验表明,随意使用外部存储类别的变量导致的后果远远超过它所带来的便利。
    唯一例外的是const数据。因为它们在初始化后就不会被修改,所以不用担心它们被意外篡改:
    const int DAYS = 7;
    const char *MSGS[3] = {"Yes", "No", "Maybe"};
    保护性程序设计的黄金法则是:“按需知道”原则。尽量在函数内部解决该函数的任务,只共享那些需要共享的变量。除自动存储类别外,其他存储类别也很有用。不过,在使用某类别之前要先考虑一下是否有必要那样做。

  • 相关阅读:
    Apache安装使用时的错误及解决办法
    Java 基础知识面试题总结
    saltstack学习1入门基础
    mysql和redis的同步
    ElementUI实现登录注册+axios全局配置+CORS跨域
    抖音文案怎么写合适|成都聚华祥
    python和shell脚本,每隔五分钟将远端服务器中的文件夹数据下载到跳板机
    Docker实践经验:Docker 上部署 mysql8 主从复制
    【audio】alsa pcm音频路径
    微服务之Nacos
  • 原文地址:https://blog.csdn.net/weixin_40186813/article/details/126288617