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

发布于 2021-09-17  72 次阅读


12.1.8 内部链接的静态变量

该存储类别的变量具有静态存储期、文件作用域和内部链接。在所有函数外部(这点与外部变量相同),用存储类别说明符static定义的变量具有这种存储类别:
static int svi = 1; //静态变量,内部存储
int main(void)
{

}
这种变量过去称为外部静态变量(external static variable),但是这个术语有点自相矛盾(这些变量具有内部链接)。但是,没有合适 的新简称,所以只能用内部链接的静态变量(static valiable with internal linkage)。普通的外部变量可以用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。可以使用存储类别说明符extern,在函数中重复声明任何具有文件作用域的变量。这样的声明并不会改变其链接属性。
int traveler = 1;   //外部链接
static int stayhome = 1;    //内部链接
int main()
{
    extern int traveler;    //使用定义在别处的traveler
    extern int stayhome;    //使用定义在别处的stayhome
    。。。
}
//对于该程序所在的翻译单元,traveler和stayhome都具有文9件作用域,但是只有traveler可用于其他翻译单元(因为它具有外部链接)。这两个声明都使用了extern关键词,指明了main()中使用的两个变量的定义都在别处,但是这并未改变stayhome的内部链接属性。

12.1.9 多文件

只有当程序由多个翻译单元组成时,才体现区别内部链接和外部链接的重要性。
复杂的C程序通常由多个单独的源代码文件组成。有时,这些文件可能要共享一个外部变量。C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用extern关键字。而且,只有定义声明才能初始化变量。
注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前先必须声明它(用extern关键字)。也就是说,在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能直接使用它。
过去,不同的编译器遵寻不同的规则。例如,许多UNIX系统允许在多个文件中不使用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用于块作用域声明,作用域则受限于该块。因此,只要程序在运行对象就存在并保留其值,但是只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接。文件作用域的静态变量具有内部链接。
extern说明符表明声明的变量定义在别处。如果包含extern的声明具有文件作用域,则应用的变量必须具有外部链接。如果包含extern的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这接取决于改变量的定义式声明。

小结

自动变量具有块作用域、无链接、自动存储期。因为它们是局部变量,属于其定义所在块(通常指函数)私有。寄存器变量的属性和自动变量相同,但是编辑器会使用更快的内存或寄存器存储他们,不能获取寄存器变量的地址。
具有静态存储期的变量可以具有外部链接、内部链接或者无链接。在同一个文件所有函数的外部声明的变量是外部变量,具有文件作用域、外部链接和静态存储期。如果在这种声明前面加上关键字static,那么其声明的变量具有文件作用域、内部链接和静态存储期。如果在函数中用static声明一个变量,则改变量具有块作用域、无链接、静态存储期。
具有自动存储期的变量,程序在进入该变量的声明所在块时才为其分配内存,在退出该块时释放之前分配的内存。如果未初始化,自动变量中是垃圾值。程序在编译时为具有静态存储期的变量分配内存,并在程序的运行过程中一直保留这块内存。如果未初始化,这样的变量会被设置为0。
具有块作用域的变量是局部的,属于包含该声明的块私有,具有文件作用域的变量对文件(或翻译单元)中位于其声明后面的所有函数可见。具有外部链接的文件作用域变量,可用于该程序的其他翻译单元。具有内部链接的文件作用域变量,只能用于其声明所在的文件内。
下面用一个简短的程序使用了5种存储类别。该程序包含了两个文件,所以必须使用多文件编译。该示例仅为了让读者熟悉5种存储类别的用法,并不是提供设计模型,好的设计模型可以不需要使用文件作用域变量。

file

file

//运行有点问题。。
在该程序中,块作用域的静态变量subtotal统计每次while循环传入accumulate()函数的总数,具有文件作用域,内部链接的变量total和subtotal的值,并在报告后重置subtotal为0.为了parta.c调用了accumulate()函数,所以必须包含accumulate()函数原型。而partb.c只包含了accumulate()的函数定义,并未在文件中调用该函数所以原型为可选。该函式使用了外部变量count统计main()中的while循环迭代次数。在parta.c中main()和report_count()共享count。

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"};
保护性程序的设计的黄金法则是:“按需知道”原则。精良在函数内部解决该函数的任务,职工想哪些需要共享的变量。处子动存储类别外,其他存储类别也很有用。不过,在使用某些类别之前先要考虑以下是否有必要这样做。

12.2 随机数函数和静态变量

学习了不同存储类别的概念后,看几个相关的程序。
首先看一个使用内部链接的静态变量的函数:随机数函数。ANSI C库提供了rand()函数生成随机数。生成随机数有多种算法,ANSI C允许C实现针对特定机器使用最佳算法。然而,ANSI C标准还提供了一个可移植的标准算法,在不同系统中生成相同的随机数。实际上,rand()是“伪随机数生成器”,意思是可预测生成数字的实际序列。但是,数字在其取值范围内均匀分布。
为了看清楚程序的内部的情况,使用可以移植的ANSI 版本,而不是编译器内置的rand()函数,可移植的版本的方案开始于一个种子数字。该函数使用该种子生成新的数,这个新数又成为新的种子,然后以此类推。该方案要行之有效,随机数函数必须记录它上一次被调用时所使用的种子。这里需要一个静态版本。

file

在程序清单rand0.c中,静态变量next的初始值是1,其值在每次调用rand0()函数时都会被修改(通过魔术公式)。该函数是用于返回一个0~32767之间的值。注意,next是具有内部链接的静态变量(并非无链接)。供同一个文件中的其他函数共享。
下述是上述函数的一个简单驱动程序:

file

file

根据输出结果发现,几次输出结果完全相同,这体现了“伪随机”的一个方面,每次主程序运行,都开始于相同的种子1,可以引入另一个函数srand1()重置种子来解决这个问题。关键是让next成为只供rand1()和srand1()访问的内部链接静态变量(srand1()相当于C库中的srand()函数)。把srand1()加入rand1()所在的文件中。

file

设置seed的值为1,输出结果与前面程序相同。但是设置seed的值为513后就得到了新的结果。
注意: 自动重置的种子
    如果C实现允许访问一些可变的量(如,时钟系统),可以用这些值(可能会被阶段)初始化种子值。例如:ANSI C有一个time()函数返回系统时间。虽然时间单元因系统而异,但是重点是该返回值是一个可进行运算的类型,而且伴随着时间变化而变化。time()返回值的类型名是time_t,具体类型与系统有关,这没关系,我们可以使用强制类型转换:
#include<stdio.h> //提供time()的ANSI 原型
srand1((unsigned int)time(0));  //初始化种子
一般而言,time()接受的参数是一个time_t类型对象的地址,而时间值就储存在传入的地址上。当然,也可以传入指针(0)作为参数,这种情况下,只能通过返回值机制来提供值。
可以把这个技巧应用于标准的ANSI C函数srand()和rand()中。如果使用这些函数,要在文件中包含stdlib.c函数。既然已经明白了srand1()和rand1()如何使用内部链接的静态变量,你也可以使用编译器提供的版本。

12.3 掷骰子

最普遍使用的是两个6面的骰子。
在一些冒险游戏中会有这5种骰子:4面、6面、8面、12面和20面只有这5种骰子具有相同的形状和大小。各种不同类型的骰子就是根据这些正多面体发展而来的。也可以做成其他面数的,但是其所有的面不会相等,因此各面朝上的几率不同。
计算机可以不用考虑几何的限制,所以可以设计任意面数的骰子。
6面:想要获得1~6的随机数。然而,rand()生成的随机数在0~RAND_MAX之间。RAND_MAX被定义在stdlib.h中,其值通常是INT_MAX。因此需要做如下调整:
1.把随机数求模6,获得的整数在0~5之间
2.结果+1,新值在1~6之间。
3.为方便以后拓展,把第一步中的数字6替换成骰子面数
下面的代码实现了这三个步骤
#include<stdio.h>
#include<stdlib.h>
int rollem(int sides)
{
    int roll;

    roll = rand() % side + 1;
    return roll;
}
//通过一个函数提示用户选择任意面数的骰子,并返回点数总和。

file

该文件加入了新元素。第一,rollem()函数属于该文件私有,它是roll_n_dice()的辅助函数。第2为了演示外部链接的特性,该文件声明了一个外部变量roll_count.该变量统计调用rollem()函数的次数第3,该文件包含以下预处理指令:
#include"diceroll.h"
如果使用标准库函数,如rand(),要在当前文件中包含标准头文件(对rand()要包含stdlib.h),而不是声明该函数。因为头文件中已经包含了正确的函数原型。效仿这一点,把roll_n_dice()函数的原型放在diceroll.h头文件中,把文件名放在双引号中而不是尖括号中,指示编译器在本地查找文件,而不是到编译器存放标准头文件的位置去查找文件。
“本地查找”的含义取决于具体的实现。一些常见的实现把头文件与源码文件或者工程文件放在相同的目录或者文件中。

file

该头文件中包含了一个函数原型和一个extern声明。由于direroll.c文件包含了该文件,direroll.c实际上包含了roll_count的两个声明
;
extern int roll_count;  //头文件中的声明(引用式声明)
int roll_count = 0; //源代码文件中的声明(定义式声明)
这样做没问题。一个变量只能有一个定义声明,但是extern的声明是引用式声明,可以有多个引用式声明。
使用roll_n_dice()函数的程序都要包含diceroll.h头文件。包含该头文件后,程序便可以使用roll_n_dice()函数和roll_count变量。
//manydice.c    --多次掷骰子的模拟程序
//与diceroll.c一起编译
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include"dirceroll.h"
int main(void)
{
    int dice,roll;
    int sides;
    int status;

    srand((unsigned int)time(0));   //随机种子
    printf("Enter the number of sides per die,0 to stop.\n");
    while(scanf("%d",&sides) == 1 && sides > 0)
    {
        printf("How many dice?\n");
        if((status = scanf("%d",&dice)) != 1)
        {
            if(status == EOF)
                break;
            else
            {
                printf("You should have entered an integer.");
                printf("Let's begin again.\n");
                while(getchar()!='\n')
                    continue;   //处理错误的输入
                printf("How many sides?Enter 0 to stop.\n");
                continue;   //进入循环的下一轮迭代
            }
        }
    roll = roll_n_dice(dice,sides);
    printf("You should rolled a %d using %d %d-sided dice.\n",roll,dice,sides);
    printf("How many sides? Enter 0 to stop.\n");
    }
    printf("The rollem() function was called %d time.\n",roll_count);
    printf("Good FORTUEN TO YOU!\n");

    return 0;
}

file

因为该程序使用了srand()随机生成数种子
(C语言获取当前系统时间的函数,以秒作单位,代表当前时间自Unix标准时间戳(1970年1月1日0点0分0秒,GMT)经过了多少秒。)所以在绝大数情况下,即使输入相同也很难得到相同的输出。manydice.c中的main()访问了定义在diceroll.c中的roll_count变量。
有三种情况可以导致外层while循环结束:side小于1、输入类型不匹配(此时scanf()返回0),遇到文件结尾(返回值是EOF)。对于读取骰子的点数,该处理文件结尾的方式(退出while循环)与处理类型不匹配(进入下一轮迭代)的情况不同。
可以通过多种方式使用roll_n_dice()。side等于2时,程序模仿投掷硬币,“正面朝上为2”,反面朝上为“1”.很容易修改该程序单独显示点数的结果,或者构建一个骰子模拟器,如果要骰多次,也可以很容易修改。
rand1()或rand()还可以用来创建一个猜数字游戏。

擦肩而过的概率