《C Primer Plus》第十二章 - 存储类别、链接和内存管理(作用域、链接、存储期、存储类别)

本章理论知识较多,要仔细反复阅读,理解概念。见加粗部分

存储类别、链接和内存管理

存储类别

  • C提供了多种不同的模型或存储类别(storage class)在内存中存储数据。

    int entity = 3;

  • 该声明创建了一个名为entity的标识符(identifier)。标识符是一个名称,在这种情况下,标识符可以用来指定(designate)特定对象的内容。标识符遵循变量的命名规则(第2章介绍过)。在该例中,标识符entity即是软件(即C程序)指定硬件内存中的对象的方式。该声明还提供了存储在对象中的值。

  • 变量名不是指定对象的唯一途径。考虑下面的声明:

    int * pt = &entity;

    int ranks[10];

  • 第1行声明中,pt是一个标识符,它指定了一个存储地址的对象。但是,表达式*pt不是标识符,因为它不是一个名称。然而,它确实指定了一个对象,在这种情况下,它与entity指定的对象相同。一般而言,那些指定对象的表达式被称为左值(第5章介绍过)。所以,entity既是标识符也是左值;*pt既是表达式也是左值。按照这个思路,ranks+2 * entity既不是标识符(不是名称),也不是左值(它不指定内存位置上的内容)。但是表达式*(ranks+2 * entity)是一个左值,因为它的确指定了特定内存位置的值,即ranks数组的第7个元素。顺带一提,ranks的声明创建了一个可容纳10个int类型元素的对象,该数组的每个元素也是一个对象。

  • 可以用存储期(storage duration)描述对象,所谓存储期是指对象在内存中保留了多长时间。标识符用于访问对象,可以用作用域(scope)和链接(linkage)描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。不同的存储类别具有不同的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数中的某部分使用。对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以在特定线程的执行期存在。可以通过函数调用的方式显式分配和释放内存。

作用域

  • 作用域描述程序中可访问标识符的区域。

  • 一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。

  • 块是用一对花括号括起来的代码区域。

  • 块作用域变量的可见范围是从定义处到包含该定义的块的末尾。

  • 以前,具有块作用域的变量都必须声明在块的开头。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);

  • 函数原型作用域的范围是从形参定义处到原型声明结束。

  • 变量的定义在函数的外面,具有文件作用域(file scope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。考虑下面的例子:

    #include <stdio.h>
    int units = 0;    /* 该变量具有文件作用域 */
    void critic(void);
    int main(void)
    {
       ...
    }
    void critic(void)
    {
       ...
    }
    

    这里,变量units具有文件作用域,main()和critic()函数都可以使用它(更准确地说,units具有外部链接文件作用域,稍后讲解)。由于这样的变量可用于多个函数,所以文件作用域变量也称为全局变量

链接

编译器把源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元(translation unit)。描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。

  • C变量有3种链接属性:外部链接、内部链接或无链接。具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用。

  • C标准用“内部链接的文件作用域”描述仅限于一个翻译单元(即一个源代码文件和它所包含的头文件)的作用域,用“外部链接的文件作用域”描述可延伸至其他翻译单元的作用域。但是,对程序员而言这些术语太长了。一些程序员把“内部链接的文件作用域”简称为“文件作用域”,把“外部链接的文件作用域”简称为“全局作用域”或“程序作用域”。

  • 查看外部定义中是否使用了存储类别说明符static:

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

    该文件和同一程序的其他文件都可以使用变量giants。而变量dodgers属文件私有,该文件中的任意函数都可使用它。

存储期

  • C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。

  • 如果对象具有静态存储期,那么它在程序的执行期间一直存在。文件作用域变量具有静态存储期。

  • 对于文件作用域变量,关键字static表明了其链接属性,而非存储期。以static声明的文件作用域变量具有内部链接。但是无论是内部链接还是外部链接,所有的文件作用域变量都具有静态存储期。

  • 线程存储期用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。

  • 块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。

  • 变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。

  • 然而,块作用域变量也能具有静态存储期。为了创建这样的变量,要把变量声明在块中,且在声明前面加上关键字static

  • 剩下5种存储类别:自动、寄存器、静态块作用域、静态外部链接、静态内部链接。现在,我们已经介绍了作用域、链接和存储期,接下来将详细讨论这些存储类别。

自动变量

  • 属于自动存储类别的变量具有自动存储期、块作用域且无链接。默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。

  • 为了更清楚地表达你的意图(例如,为了表明有意覆盖一个外部变量定义,或者强调不要把该变量改为其他存储类别),可以显式使用关键字auto,如下所示:

    int main(void)

    {

    auto int plox;

  • 关键字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 已经消失
}
  • 如果内层块中声明的变量与外层块中的变量同名会怎样?内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用域又回到了原来的作用域。
// hiding.c -- 块中的变量
#include <stdio.h>
int main()
{
   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,隐藏了原始的 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;
}
  • 前面提到一个C99特性:作为循环或if语句的一部分,即使不使用花括号({}),也是一个块。更完整地说,整个循环是它所在块的子块(sub-block),循环体是整个循环块的子块。与此类似,if语句是一个块,与其相关联的子语句是if语句的子块。这些规则会影响到声明的变量和这些变量的作用域。
\#include <stdio.h>
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;
}
/* 假设编译器支持C语言的这个新特性,该程序的输出如下:
Initially, n = 8 at 0x7fff5fbff8c8
loop 1: n = 1 at 0x7fff5fbff8c4
loop 1: n = 2 at 0x7fff5fbff8c4
After loop 1, n = 8 at 0x7fff5fbff8c8
loop 2 index n = 1 at 0x7fff5fbff8c0
loop 2: n = 6 at 0x7fff5fbff8bc
loop 2 index n = 2 at 0x7fff5fbff8c0
loop 2: n = 6 at 0x7fff5fbff8bc
After loop 2, n = 8 at 0x7fff5fbff8c8 */
  • 第1个for循环头中声明的n,其作用域作用至循环末尾,而且隐藏了原始的n。但是,离开循环后,原始的n又起作用了。
  • 第2个for循环头中声明的n作为循环的索引,隐藏了原始的n。然后,在循环体中又声明了一个n,隐藏了索引n。结束一轮迭代后,声明在循环体中的n消失,循环头使用索引n进行测试。当整个循环结束时,原始的n又起作用了。

寄存器变量

  • 绝大多数方面,寄存器变量和自动变量都一样。也就是说,它们都是块作用域、无链接和自动存储期。使用存储类别说明符register便可声明寄存器变量:

    int main(void)

    {

    register int quick;

  • 声明变量为register类别与直接命令相比更像是一种请求。编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,所以可能不会如你所愿。在这种情况下,寄存器变量就变成普通的自动变量。即使是这样,仍然不能对该变量使用地址运算符。

块作用域的静态变量

  • 静态的意思是该变量在内存中原地不动,并不是说它的值不变。具有文件作用域的变量自动具有(也必须是)静态存储期。
  • 可以创建具有静态存储期、块作用域的局部变量。这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。也就是说,这种变量具有块作用域、无链接,但是具有静态存储期。
/* 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*/
  • 每次调用trystat()都会初始化fade,但是stay只在编译trystat()时被初始化一次。如果未显式初始化静态变量,它们会被初始化为0。

    int fade = 1;

    static int stay = 1;

  • 第1条声明确实是trystat()函数的一部分,每次调用该函数时都会执行这条声明。这是运行时行为。

  • 第2条声明实际上并不是trystat()函数的一部分。如果逐步调试该程序会发现,程序似乎跳过了这条声明。这是因为静态变量和外部变量在程序被载入内存时已执行完毕。把这条声明放在trystat()函数中是为了告诉编译器只有trystat()函数才能看到该变量。这条声明并未在运行时执行。
    不能在函数的形参中使用static:

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

外部链接的静态变量

  • 外部链接的静态变量具有文件作用域、外部链接和静态存储期。

  • 该类别有时称为外部存储类别(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 Errupt;   /* 可选的声明*/
   extern double Up[];  /* 可选的声明*/
   ...
}

  • 注意,在main()中声明Up数组时(这是可选的声明)不用指明数组大小,因为第1次声明已经提供了数组大小信息。main()中的两条extern声明完全可以省略,因为外部变量具有文件作用域,所以Errupt和Up从声明处到文件结尾都可见。它们出现在那里,仅为了说明main()函数要使用这两个变量。

  • 如果省略掉函数中的extern关键字,相当于创建了一个自动变量。去掉下面声明中的extern:

    extern int Errupt;

  • 便成为:

    int Errupt;

  • 这使得编译器在main()中创建了一个名为Errupt的自动变量。它是一个独立的局部变量,与原来的外部变量Errupt不同。该局部变量仅main()中可见,但是外部变量Errupt对于该文件的其他函数(如next())也可见。简而言之,在执行块中的语句时,块作用域中的变量将“隐藏”文件作用域中的同名变量。如果不得已要使用与外部变量同名的局部变量,可以在局部变量的声明中使用auto存储类别说明符明确表达这种意图。

  • 外部变量具有静态存储期。因此,无论程序执行到main()、next()还是其他函数,数组Up及其值都一直存在。

  • 外部变量的作用域是:从声明处到文件结尾。

  • 外部变量和自动变量类似,也可以被显式初始化。与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为0。这一原则也适用于外部定义的数组元素。与自动变量的情况不同,只能使用常量表达式初始化文件作用域变量:

int x = 10;        // 没问题,10是常量
int y = 3 + 20;      // 没问题,用于初始化的是常量表达式
size_t z = sizeof(int);  //没问题,用于初始化的是常量表达式
int x2 = 2 * x;      // 不行,x是变量
  • 下面进一步介绍定义变量和声明变量的区别。考虑下面的例子:
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’; /* 错误 */

  • file_two中的声明是错误的,因为file_one.c中的定义式声明已经创建并初始化了permis。

内部链接的静态变量

  • 该存储类别的变量具有静态存储期、文件作用域和内部链接。

    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
   ...

多文件

  • 复杂的C程序通常由多个单独的源代码文件组成。有时,这些文件可能要共享一个外部变量。C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用extern关键字。而且,只有定义式声明才能初始化变量。
  • 注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用extern关键字)。也就是说,在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能直接使用它。

存储类别说明符

  • auto说明符表明变量是自动存储期,只能用于块作用域的变量声明中。由于在块中声明的变量本身就具有自动存储期,所以使用auto主要是为了明确表达要使用与外部变量同名的局部变量的意图。
  • register说明符也只用于块作用域的变量,它把变量归为寄存器存储类别,请求最快速度访问该变量。同时,还保护了该变量的地址不被获取。
  • 用static说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果static用于文件作用域声明,作用域受限于该文件。如果static用于块作用域声明,作用域则受限于该块。因此,只要程序在运行对象就存在并保留其值,但是只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接。文件作用域的静态变量具有内部链接。
  • extern说明符表明声明的变量定义在别处。如果包含extern的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含extern的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这接取决于该变量的定义式声明。
// parta.c --- 不同的存储类别
// 与 partb.c 一起编译
#include <stdio.h>
    void
    report_count();
void accumulate(int k);
int count = 0; // 文件作用域,外部链接
int main(void)
{
    int value;      // 自动变量
    register int i; // 寄存器变量
    printf("Enter a positive integer (0 to quit): ");
    while (scanf("%d", &value) == 1 && value > 0)
    {
        ++count; // 使用文件作用域变量
        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);
}
程序清单12 .6 partb.c程序
// partb.c -- 程序的其余部分
// 与 parta.c 一起编译
#include <stdio.h>
    extern int count;   // 引用式声明,外部链接
static int total = 0;   // 静态定义,内部链接
void accumulate(int k); // 函数原型
void accumulate(int k)  // k 具有块作用域,无链接
{
    static int subtotal = 0; // 静态,无链接
    if (k <= 0)
    {
        printf("loop cycle: %d\n", count);
        printf("subtotal: %d; total: %d\n", subtotal, total);
        subtotal = 0;
    }
    else
    {
        subtotal += k;
        total += k;
    }
}
/* 下面是程序的运行示例:
    Enter a positive integer(0 to quit):5 loop cycle:1
subtotal:15;
total : 15 Enter a positive integer(0 to quit) : 10 loop cycle : 2 subtotal : 55;
total : 70 Enter a positive integer(0 to quit) : 2 loop cycle : 3 subtotal : 3;
total : 73 Enter a positive integer(0 to quit) : 0 Loop executed 3 times */

存储类别和函数

  • 函数也有存储类别,可以是外部函数(默认)或静态函数。

  • 外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。

    double gamma(double); /* 该函数默认为外部函数 */

    static double beta(int, int);

    extern double delta(double, int);

  • 在同一个程序中,其他文件中的函数可以调用gamma()和delta(),但是不能调用beta(),因为以static存储类别说明符创建的函数属于特定模块私有。这样做避免了名称冲突的问题,由于beta()受限于它所在的文件,所以在其他文件中可以使用与之同名的函数。

  • 通常的做法是:用extern关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数被定义在别处。除非使用static关键字,否则一般函数声明都默认为extern。

存储类别的选择

  • 对于“使用哪种存储类别”的回答绝大多数是“自动存储类别”,要知道默认类别就是自动存储类别。初学者会认为外部存储类别很不错,为何不把所有的变量都设置成外部变量,这样就不必使用参数和指针在函数间传递信息了。然而,这背后隐藏着一个陷阱。如果这样做,A()函数可能违背你的意图,私下修改B()函数使用的变量。多年来,无数程序员的经验表明,随意使用外部存储类别的变量导致的后果远远超过了它所带来的便利。

随机数函数和静态变量

  • 使用内部链接的静态变量的函数:随机数函数。
/* rand0.c --生成随机数*/
/* 使用 ANSI C 可移植算法 */
static unsigned long int next = 1; /* 种子 */
unsigned int rand0(void)
{
   /* 生成伪随机数的魔术公式 */
   next = next * 1103515245 + 12345;
   return (unsigned int) (next / 65536) % 32768;
}
  • 在程序清单12.7中,静态变量next的初始值是1,其值在每次调用rand0()函数时都会被修改(通过魔术公式)。
/* r_drive0.c -- 测试 rand0()函数 */
/* 与 rand0.c 一起编译*/
#include <stdio.h>
extern unsigned int rand0(void);
int main(void)
{
   int count;
   for (count = 0; count < 5; count++)
     printf("%d\n", rand0());
   return 0;
}
/* 16838
5758
10113
17515
31051
  程序输出的数字看上去是随机的,再次运行程序后,输出如下:
16838
5758
10113
17515
31051 */
/* 程序清单12.9给出了修改后的文件。
程序清单12.9 s_and_r.c文件程序 */
/* s_and_r.c -- 包含 rand1() 和 srand1() 的文件  */
/*        使用 ANSI C 可移植算法   */
static unsigned long int next = 1; /* 种子 */
int rand1(void)
{
   /*生成伪随机数的魔术公式*/
   next = next * 1103515245 + 12345;
   return (unsigned int) (next / 65536) % 32768;
}
void srand1(unsigned int seed)
{
   next = seed;
}
  • next是具有内部链接的文件作用域静态变量。这意味着rand1()和srand1()都可以使用它,但是其他文件中的函数无法访问它。
/* r_drive1.c -- 测试 rand1() 和 srand1() */
/* 与 s_and_r.c 一起编译 */
#include <stdio.h>
#include <stdlib.h>
extern void srand1(unsigned int x);
extern int rand1(void);
int main(void)
{
   int count;
   unsigned seed;
   printf("Please enter your choice for seed.\n");
   while (scanf("%u", &seed) == 1)
   {
     srand1(seed);  /* 重置种子 */
     for (count = 0; count < 5; count++)
        printf("%d\n", rand1());
     printf("Please enter next seed (q to quit):\n");
   }
   printf("Done\n");
   return 0;
}

#include <time.h> /* 提供time()的ANSI原型*/*

srand1((unsigned int) time(0)); /* 初始化种子 */

  • 一般而言,time()接受的参数是一个time_t类型对象的地址,而时间值就存储在传入的地址上。当然,也可以传入空指针(0)作为参数,这种情况下,只能通过返回值机制来提供值。
  • 可以把这个技巧应用于标准的ANSI C函数srand()和rand()中。如果使用这些函数,要在文件中包含stdlib.c头文件。

掷骰子

  • 我们想获得1~6的随机数。然而,rand()生成的随机数在0~RAND_MAX之间。RAND_MAX被定义在stdlib.h中,其值通常是INT_MAX。因此,需要进行一些调整,方法如下。
    • 1.把随机数求模6,获得的整数在0~5之间。
    • 2.结果加1,新值在1~6之间。
    • 3.为方便以后扩展,把第1步中的数字6替换成骰子面数。
#include <stdlib.h> /* 提供rand()的原型 */
int rollem(int sides)
{
   int roll;
   roll = rand() % sides + 1;
   return roll;
}

分配内存:malloc()和free()

  • C能做的不止这些。可以在程序运行时分配更多的内存。主要的工具是malloc()函数,该函数接受一个参数:所需的内存字节数。malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说,malloc()分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。因为char表示1字节,malloc()的返回类型通常被定义为指向char的指针。然而,从ANSI C标准开始,C使用一个新的类型:指向void的指针。该类型相当于一个“通用指针”。malloc()函数可用于返回指向数组的指针、指向结构的指针等,所以通常该函数的返回值会被强制转换为匹配的类型。在ANSI C中,应该坚持使用强制类型转换,提高代码的可读性。然而,把指向void的指针赋给任意类型的指针完全不用考虑类型匹配的问题。如果malloc()分配内存失败,将返回空指针。

    double * ptd;

    ptd = (double *) malloc(30 * sizeof(double));

  • 以上代码为30个double类型的值请求内存空间,并设置ptd指向该位置。注意,指针ptd被声明为指向一个double类型,而不是指向内含30个double类型值的块。

  • 我们有3种创建数组的方法。

    • ·声明数组时,用常量表达式表示数组的维度,用数组名访问数组的元素。可以用静态内存或自动内存创建这种数组。
    • ·声明变长数组(C99新增的特性)时,用变量表达式表示数组的维度,用数组名访问数组的元素。具有这种特性的数组只能在自动内存中创建。
    • ·声明一个指针,调用malloc(),将其返回值赋给指针,使用指针访问数组的元素。该指针可以是静态的或自动的。
  • 通常,malloc()要与free()配套使用。free()函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配的内存。因此,动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。

/* dyn_arr.c -- 动态分配数组 */
#include <stdio.h>
#include <stdlib.h> /* 为 malloc()、free()提供原型 */
int main(void)
{
   double * ptd;
   int max;
   int number;
   int i = 0;
   puts("What is the maximum number of type double entries?");
   if (scanf("%d", &max) != 1)
   {
     puts("Number not correctly entered -- bye.");
     exit(EXIT_FAILURE);
   }
   ptd = (double *) malloc(max * sizeof(double));
   if (ptd == NULL)
   {
     puts("Memory allocation failed. Goodbye.");
     exit(EXIT_FAILURE);
   }
   /* ptd 现在指向有max个元素的数组 */
   puts("Enter the values (q to quit):");
   while (i < max && scanf("%lf", &ptd[i]) == 1)
     ++i;
   printf("Here are your %d entries:\n", number = i);
   for (i = 0; i < number; i++)
   {
     printf("%7.2f ", ptd[i]);
     if (i % 7 == 6)
        putchar('\n');
   }
   if (i % 7 != 0)
     putchar('\n');
   puts("Done.");
   free(ptd);
   return 0;
}
  • 下面是该程序的运行示例。程序通过交互的方式让用户先确定数组的大小,我们设置数组大小为5。虽然我们后来输入了6个数,但程序也只处理前5个数。

    What is the maximum number of entries?5

    Enter the values (q to quit):20 30 35 25 40 80

    Here are your 5 entries:

    20.00 30.00 35.00 25.00 40.00

    Done.

  • free()函数位于程序的末尾,它释放了malloc()函数分配的内存。free()函数只释放其参数指向的内存块。一些操作系统在程序结束时会自动释放动态分配的内存,但是有些系统不会。为保险起见,请使用free(),不要依赖操作系统来清理。

free()的重要性

..
int main()
{
   double glad[2000];
   int i;
   ...
   for (i = 0; i < 1000; i++)
     gobble(glad, 2000);
   ...
}
void gobble(double ar[], int n)
{
   double * temp = (double *) malloc( n * sizeof(double));
   ... /* free(temp); // 假设忘记使用free() */
}
  • 第1次调用gobble()时,它创建了指针temp,并调用malloc()分配了16000字节的内存(假设double为8字节)。假设如代码注释所示,遗漏了free()。当函数结束时,作为自动变量的指针temp也会消失。但是它所指向的16000字节的内存却仍然存在。由于temp指针已被销毁,所以无法访问这块内存,它也不能被重复使用,因为代码中没有调用free()释放这块内存。
  • 第2次调用gobble()时,它又创建了指针temp,并调用malloc()分配了16000字节的内存。第1次分配的16000字节内存已不可用,所以malloc()分配了另外一块16000字节的内存。当函数结束时,该内存块也无法被再访问和再使用。
  • 循环要执行1000次,所以在循环结束时,内存池中有1600万字节被占用。实际上,也许在循环结束之前就已耗尽所有的内存。这类问题被称为内存泄漏(memory leak)。在函数末尾处调用free()函数可避免这类问题发生。

calloc()函数

  • 分配内存还可以使用calloc(),典型的用法如下:

    long * newmem;

    newmem = (long *)calloc(100, sizeof (long));

  • calloc()函数还有一个特性:它把块中的所有位都设置为0(注意,在某些硬件系统中,不是把所有位都设置为0来表示浮点值0)。

动态内存分配和变长数组

  • 不同的是,变长数组是自动存储类型。因此,程序在离开变长数组定义所在的块时(该例中,即vlamal()函数结束时),变长数组占用的内存空间会被自动释放,不必使用free()。

  • 另一方面,用malloc()创建的数组不必局限在一个函数内访问。例如,可以这样做:被调函数创建一个数组并返回指针,供主调函数访问,然后主调函数在末尾调用free()释放之前被调函数分配的内存。另外,free()所用的指针变量可以与malloc()的指针变量不同,但是两个指针必须存储相同的地址。但是,不能释放同一块内存两次。

  • 对多维数组而言,使用变长数组更方便。当然,也可以用malloc()创建二维数组,但是语法比较繁琐。如果编译器不支持变长数组特性,就只能固定二维数组的维度,如下所示:

    int n = 5;
    int m = 6;
    int ar2[n][m]; // n×m的变长数组(VLA)
    int (* p2)[6]; // C99之前的写法
    int (* p3)[m]; // 要求支持变长数组
    p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // n×6 数组
    p3 = (int (*)[m]) malloc(n * m * sizeof(int)); // n×m 数组(要求支持变长数组)
    ar2[1][2] = p2[1][2] = 12;
    

    int ( p2)[6]; // C99之前的写法*

  • 表明p2指向一个内含6个int类型值的数组。因此,p2[i]代表一个由6个整数构成的元素,p2[i][j]代表一个整数。

存储类别和动态内存分配

  • 我们来看一个理想化模型。可以认为程序把它可用的内存分为3部分:一部分供具有外部链接、内部链接和无链接的静态变量使用;一部分供自动变量使用;一部分供动态内存分配。
  • 静态存储类别所用的内存数量在编译时确定,只要程序还在运行,就可访问存储在该部分的数据。该类别的变量在程序开始执行时被创建,在程序结束时被销毁。
  • 然而,自动存储类别的变量在程序进入变量定义所在块时存在,在程序离开块时消失。因此,随着程序调用函数和函数结束,自动变量所用的内存数量也相应地增加和减少。这部分的内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁。
  • 动态分配的内存在调用malloc()或相关函数时存在,在调用free()后释放。这部分的内存由程序员管理,而不是一套规则。所以内存块可以在一个函数中创建,在另一个函数中销毁。正是因为这样,这部分的内存用于动态内存分配会支离破碎。也就是说,未使用的内存块分散在已使用的内存块之间。另外,使用动态内存通常比使用栈内存慢。
// where.c -- 数据被存储在何处?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int static_store = 30;
const char * pcg = "String Literal";
int main()
{
   int auto_store = 40;
   char auto_string [] = "Auto char Array";
   int * pi;
   char * pcl;
   pi = (int *) malloc(sizeof(int));
   *pi = 35;
   pcl = (char *) malloc(strlen("Dynamic String") + 1);
   strcpy(pcl, "Dynamic String");
   printf("static_store: %d at %p\n", static_store, &static_store);
   printf(" auto_store: %d at %p\n", auto_store, &auto_store);
   printf("     *pi: %d at %p\n", *pi, pi);
   printf(" %s at %p\n", pcg, pcg);
   printf(" %s at %p\n", auto_string, auto_string);
   printf(" %s at %p\n", pcl, pcl);
   printf("  %s at %p\n", "Quoted String", "Quoted String");
   free(pi);
   free(pcl);
   return 0;
}
/* 在我们的系统中,该程序的输入如下:
static_store: 30 at 00378000
auto_store: 40 at 0049FB8C
*pi: 35 at 008E9BA0
String Literal at 00375858
Auto char Array at 0049FB74
Dynamic String at 008E9BD0
Quoted String at 00375908 */
  • 如上所示,静态数据(包括字符串字面量)占用一个区域,自动数据占用另一个区域,动态分配的数据占用第3个区域(通常被称为内存堆或自由内存)。

ANSI C类型限定符

  • C90还新增了两个属性:恒常性(constancy)和易变性(volatility)。这两个属性可以分别用关键字const和volatile来声明,以这两个关键字创建的类型是限定类型(qualified type)。

  • C99标准新增了第3个限定符:restrict,用于提高编译器优化。C11标准新增了第4个限定符:_Atomic。C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支持项。

    const const const int n = 6; // 与 const int n = 6;相同

const类型限定符

const int nochange; /* 限定nochange的值不能被修改 /

nochange = 12;

/* 不允许 编译器会报错。但是,可以初始化const变量。因此,下面的代码没问题: */

const int nochange = 12; /* 没问题 */

const float * pf; /* pf 指向一个float类型的const值 */

  • 创建的pf指向不能被改变的值,而pf本身的值可以改变。例如,可以设置该指针指向其他const值。相比之下,下面的声明:

    float * const pt; /* pt 是一个const指针 */

  • 创建的指针pt本身的值不能更改。pt必须指向同一个地址,但是它所指向的值可以改变。

    const float * const ptr;

  • 表明ptr既不能指向别处,它所指向的值也不能改变。

  • 还可以把const放在第3个位置:

    float const * pfc; // 与const float * pfc;相同

  • 简而言之,const放在*左侧任意位置,限定了指针指向的数据不能改变;const放在*的右侧,限定了指针本身不能改变。

    void display(const int array[], int limit);

  • 在函数原型和函数头,形参声明const int array[]与const int * array相同,所以该声明表明不能更改array指向的数据。

  • 在文件间共享const数据要小心。可以采用两个策略。第一,遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用extern关键字):

    /* file1.c -- 定义了一些外部const变量 */
    const double PI = 3.14159;
    const char * MONTHS[12] = { "January", "February", "March", "April", "May", 
                  "June", "July","August", "September", "October",  
                  "November", "December" };
    /* file2.c -- 使用定义在别处的外部const变量 */
    extern const double PI;
    extern const * MONTHS [];
    

    另一种方案是,把const变量放在一个头文件中,然后在其他文件中包含该头文件:

    /* constant.h --定义了一些外部const变量*/
    static const double PI = 3.14159;
    static const char * MONTHS[12] ={"January", "February", "March", "April", "May",
                     "June", "July","August", "September", "October",
                     "November", "December"};
    /* file1.c --使用定义在别处的外部const变量*/
    #include "constant.h"
    /* file2.c --使用定义在别处的外部const变量*/
    #include "constant.h"
    

    这种方案必须在头文件中用关键字static声明全局const变量。如果去掉static,那么在file1.c和file2.c中包含constant.h将导致每个文件中都有一个相同标识符的定义式声明,C标准不允许这样做

  • 头文件方案的好处是,方便你偷懒,不用惦记着在一个文件中使用定义式声明,在其他文件中使用引用式声明。所有的文件都只需包含同一个头文件即可。但它的缺点是,数据是重复的。对于前面的例子而言,这不算什么问题,但是如果const数据包含庞大的数组,就不能视而不见了。

volatile类型限定符

  • volatile限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。例如,一个地址上可能存储着当前的时钟时间,无论程序做什么,地址上的值都随时间的变化而改变。或者一个地址用于接受另一台计算机传入的信息。
    volatile的语法和const一样:

    volatile int loc1; /* loc1 是一个易变的位置 */

    volatile int * ploc; /* ploc 是一个指向易变的位置的指针 */

  • 可以同时用const和volatile限定一个值。例如,通常用const把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用volatile。只能在声明中同时使用这两个限定符,它们的顺序不重要

  • 如下所示:

    volatile const int loc;

    const volatile int * ploc;

restrict类型限定符

  • restrict关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。要弄明白为什么这样做有用,先看几个例子。考虑下面的代码:

    int ar[10];

    int * restrict restar = (int *) malloc(10 * sizeof(int));

    int * par = ar;

  • 这里,指针restar是访问由malloc()所分配内存的唯一且初始的方式。因此,可以用restrict关键字限定它。而指针par既不是访问ar数组中数据的初始方式,也不是唯一方式。所以不用把它设置为restrict。

    for (n = 0; n < 10; n++)

    {

    par[n] += 5;

    restar[n] += 5;

    ar[n] *= 2;

    par[n] += 3;

    restar[n] += 3;

    }

  • 由于之前声明了restar是访问它所指向的数据块的唯一且初始的方式,编译器可以把涉及restar的两条语句替换成下面这条语句,效果相同:

    restar[n] += 8; /* 可以进行替换 */

  • 但是,如果把与par相关的两条语句替换成下面的语句,将导致计算错误:

    par[n] += 8; / * 给出错误的结果 */

  • 在本例中,如果未使用restrict关键字,编译器就必须假设最坏的情况(即,在两次使用指针之间,其他的标识符可能已经改变了数据)。如果用了restrict关键字,编译器就可以选择捷径优化计算。

  • restrict限定符还可用于函数形参中的指针。这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。

_Atomic类型限定符(C11)

  • 并发程序设计把程序执行分成可以同时执行的多个线程。这给程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。C11通过包含可选的头文件stdatomic.h和threads.h,提供了一些可选的(不是必须实现的)管理方法。值得注意的是,要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。

  • 例如,下面的代码:

    int hogs; // 普通声明

    hogs = 12; // 普通赋值

  • 可以替换成:

    _Atomic int hogs; // hogs 是一个原子类型的变量

    atomic_store(&hogs, 12); // stdatomic.h中的宏
    这里,在hogs中存储12是一个原子过程,其他线程不能访问hogs。
    编写这种代码的前提是,编译器要支持这一新特性。

旧关键字的新位置

void ofmouth(int * const a1, int * restrict a2, int n); // 以前的风格

  • 该声明表明a1是一个指向int的const指针,这意味着不能更改指针本身,可以更改指针指向的数据。除此之外,还表明a2是一个restrict指针,如上一节所述。新的等价语法如下:

    void ofmouth(int a1[const], int a2[restrict], int n); // C99允许

  • 根据新标准,在声明函数形参时,指针表示法和数组表示法都可以使用这两个限定符。

  • static的情况不同,因为新标准为static引入了一种与以前用法不相关的新用法。现在,static除了表明静态存储类别变量的作用域或链接外,新的用法告知编译器如何使用形式参数。例如,考虑下面的原型:

    double stick(double ar[static 20]);

  • static的这种用法表明,函数调用中的实际参数应该是一个指向数组首元素的指针,且该数组至少有20个元素。这种用法的目的是让编译器使用这些信息优化函数的编码。

复习题

  1. 哪些类别的变量可以成为它所在函数的局部变量?

  2. 哪些类别的变量在它所在程序的运行期一直存在?

  3. 哪些类别的变量可以被多个文件使用?哪些类别的变量仅限于在一个文件中使用?

  4. 块作用域变量具有什么链接属性?

  5. extern关键字有什么用途?

  6. 考虑下面两行代码,就输出的结果而言有何异同:

    int * p1 = (int *)malloc(100 * sizeof(int));

    int * p1 = (int *)calloc(100, sizeof(int));*

  7. 下面的变量对哪些函数可见?程序是否有误?

    /* 文件 1 */

    int daisy;

    int main(void)

    {

    int lily;

    …;

    }

    int petal()

    {

    extern int daisy, lily;

    …;

    }

    /* 文件 2 */

    extern int daisy;

    static int lily;

    int rose;

    int stem()

    {

    int rose;

    …;

    }

    void root()

    {

    …;

    }

  8. 下面程序会打印什么?

    #include <stdio.h>
    char color = 'B';
    void first(void);
    void second(void);
    int main(void)
    {
       extern char color;
       printf("color in main() is %c\n", color);
       first();
       printf("color in main() is %c\n", color);
       second();
       printf("color in main() is %c\n", color);
       return 0;
    }
    void first(void)
    {
       char color;
       color = 'R';
       printf("color in first() is %c\n", color);
    }
    void second(void)
    {
       color = 'G';
       printf("color in second() is %c\n", color);
    }
    
  9. 假设文件的开始处有如下声明:

    static int plink;

    int value_ct(const int arr[], int value, int n);

    a.以上声明表明了程序员的什么意图?
    b.用const int value和const int n分别替换int value和int n,是否对主调程序的值加强保护。


  1. 自动存储类别;寄存器存储类别;静态、无链接存储类别。

  2. 静态、无链接存储类别;静态、内部链接存储类别;静态、外部链接存储类别。

  3. 静态、外部链接存储类别可以被多个文件使用。静态、内部链接存储类别只能在一个文件中使用。

  4. 无链接。

  5. 关键字extern用于声明中,表明该变量或函数已定义在别处。

  6. 两者都分配了一个内含100个int类型值的数组。第2行代码使用calloc()把数组中的每个元素都设置为0。

  7. 默认情况下,daisy只对main()可见,以extern声明的daisy才对petal()、stem()和root()可见。文件2中的extern int daisy;声明使得daisy对文件2中的所有函数都可见。第1个lily是main()的局部变量。petal()函数中引用的lily是错误的,因为两个文件中都没有外部链接的lily。虽然文件2中有一个静态的lily,但是它只对文件2可见。第1个外部rose对root()函数可见,但是stem()中的局部rose覆盖了外部的rose。

  8. 下面是程序的输出:

    color in main() is B

    color in first() is R

    color in main() is B

    color in second() is G

    color in main() is G
    first()函数没有使用color变量,但是second()函数使用了。

  9. a.声明告诉我们,程序将使用一个变量plink,该文件包含的函数都可以使用这个变量。calu_ct()函数的第1个参数是指向一个整数的指针,并假定它指向内含n个元素的数组。这里关键是要理解该程序不允许使用指针arr修改原始数组中的值。
    b.不会。value和n已经是原始数据的备份,所以该函数无法更改主调函数中相应的值。这些声明的作用是防止函数修改value和n的值。例如,如果用const限定n,就不能使用n++表达式。

编程练习

  1. 不使用全局变量,重写程序清单12.4。

  2. 在美国,通常以英里/加仑来计算油耗;在欧洲,以升/100公里来计算。下面是程序的一部分,提示用户选择计算模式(美制或公制),然后接收数据并计算油耗。

    // pe12-2b.c
    // 与 pe12-2a.c 一起编译
    #include <stdio.h>
    #include "pe12-2a.h"
    int main(void)
    {
       int mode;
       printf("Enter 0 for metric mode, 1 for US mode: ");
       scanf("%d", &mode);
       while (mode >= 0)
       {
         set_mode(mode);
         get_info();
         show_info();
         printf("Enter 0 for metric mode, 1 for US mode");
         printf(" (-1 to quit): ");
         scanf("%d", &mode);
       }
       printf("Done.\n");
       return 0;
    }
    /* 下面是是一些输出示例:
    Enter 0 for metric mode, 1 for US mode: 0
    Enter distance traveled in kilometers: 600
    Enter fuel consumed in liters: 78.8
    Fuel consumption is 13.13 liters per 100 km.
    Enter 0 for metric mode, 1 for US mode (-1 to quit): 1
    Enter distance traveled in miles: 434
    Enter fuel consumed in gallons: 12.7
    Fuel consumption is 34.2 miles per gallon.
    Enter 0 for metric mode, 1 for US mode (-1 to quit): 3
    Invalid mode specified. Mode 1(US) used.
    Enter distance traveled in miles: 388
    Enter fuel consumed in gallons: 15.3
    Fuel consumption is 25.4 miles per gallon.
    Enter 0 for metric mode, 1 for US mode (-1 to quit): -1
    Done. */
    

    如果用户输入了不正确的模式,程序向用户给出提示消息并使用上一次输入的正确模式。请提供pe12-2a.h头文件和pe12-2a.c源文件。源代码文件应定义3个具有文件作用域、内部链接的变量。一个表示模式、一个表示距离、一个表示消耗的燃料。get_info()函数根据用户输入的模式提示用户输入相应数据,并将其存储到文件作用域变量中。show_info()函数根据设置的模式计算并显示油耗。可以假设用户输入的都是数值数据。

  3. 重新设计编程练习2,要求只使用自动变量。该程序提供的用户界面不变,即提示用户输入模式等。但是,函数调用要作相应变化。

  4. 在一个循环中编写并测试一个函数,该函数返回它被调用的次数。

  5. 编写一个程序,生成100个1~10范围内的随机数,并以降序排列(可以把第11章的排序算法稍加改动,便可用于整数排序,这里仅对整数排序)。

  6. 编写一个程序,生成1000个1~10范围内的随机数。不用保存或打印这些数字,仅打印每个数出现的次数。用10个不同的种子值运行,生成的数字出现的次数是否相同?可以使用本章自定义的函数或ANSI C的rand()和srand()函数,它们的格式相同。这是一个测试特定随机数生成器随机性的方法。

  7. 编写一个程序,按照程序清单12.13输出示例后面讨论的内容,修改该程序。使其输出类似:

    Enter the number of sets; enter q to stop : 18

    How many sides and how many dice? 6 3

    Here are 18 sets of 3 6-sided throws.

    12 10 6 9 8 14 8 15 9 14 12 17 11 7 10

    13 8 14

    How many sets? Enter q to stop: q

  8. 下面是程序的一部分:

    // pe12-8.c
    #include <stdio.h>
    int * make_array(int elem, int val);
    void show_array(const int ar [], int n);
    int main(void)
    {
       int * pa;
       int size;
       int value;
       printf("Enter the number of elements: ");
       while (scanf("%d", &size) == 1 && size > 0)
       {
         printf("Enter the initialization value: ");
         scanf("%d", &value);
         pa = make_array(size, value);
         if (pa)
         {
            show_array(pa, size);
            free(pa);
         }
         printf("Enter the number of elements (<1 to quit): ");
       }
       printf("Done.\n");
       return 0;
    }
    

    提供make_array()和show_array()函数的定义,完成该程序。make_array()函数接受两个参数,第1个参数是int类型数组的元素个数,第2个参数是要赋给每个元素的值。该函数调用malloc()创建一个大小合适的数组,将其每个元素设置为指定的值,并返回一个指向该数组的指针。show_array()函数显示数组的内容,一行显示8个数。

  9. 编写一个符合以下描述的函数。首先,询问用户需要输入多少个单词。然后,接收用户输入的单词,并显示出来,使用malloc()并回答第1个问题(即要输入多少个单词),创建一个动态数组,该数组内含相应的指向char的指针(注意,由于数组的每个元素都是指向char的指针,所以用于存储malloc()返回值的指针应该是一个指向指针的指针,且它所指向的指针指向char)。在读取字符串时,该程序应该把单词读入一个临时的char数组,使用malloc()分配足够的存储空间来存储单词,并把地址存入该指针数组(该数组中每个元素都是指向char的指针)。然后,从临时数组中把单词拷贝到动态分配的存储空间中。因此,有一个字符指针数组,每个指针都指向一个对象,该对象的大小正好能容纳被存储的特定单词。下面是该程序的一个运行示例:

    How many words do you wish to enter? 5

    Enter 5 words now:

    I enjoyed doing this exercise

    Here are your words:

    I

    enjoyed

    doing

    this

    exercise


  1. /* 
    #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.\n");
         scanf("%d", &units);
    }
    下面是该程序的输出示例:
    How many pounds to a firkin of butter?14
    No luck, my friend. Try again.56
    You must have looked it up! */
    
    #include <stdio.h>
    void critic(int *units);
    int main(void)
    {
        int units;
        printf("How many pounds to a firkin of butter?\n");
        scanf("%d", &units);
        while (units != 56)
            critic(&units);
        printf("You must have looked it up!\n");
        return 0;
    }
    void critic(int *units)
    {
        printf("No luck, my friend. Try again.\n");
        scanf("%d", units);
    }
    
  2. // 2.h
    static int mode;
    static float distance;
    static float fuel;
    void set_mode(int m);
    void get_info(void);
    void show_info();
    // 2.c
    #include <stdio.h>
    #include "2.h"
    /* extern int mode;
    extern float distance;
    extern float fuel; */
    void set_mode(int m)
    {
        if (m == 0 || m == 1)
            mode = m;
        else
            printf("Invalid mode specified. Mode 1(US) used.\n");
    }
    void get_info(void)
    {
        if (mode == 0)
        {
            printf("Enter distance traveled in kilometers:");
            scanf("%f", &distance);
            printf("Enter fuel consumed in liters:");
            scanf("%f", &fuel);
        }
        else if (mode == 1)
        {
            printf("Enter distance traveled in miles : ");
            scanf("%f", &distance);
            printf("Enter fuel consumed in gallons : ");
            scanf("%f", &fuel);
        }
    }
    void show_info()
    {
        if (mode == 0)
            printf("Fuel consumption is %g liters per 100 km.\n", fuel / distance * 100); // mode=0
        else if (mode == 1)
            printf("Fuel consumption is %g miles per gallon.\n", distance / fuel); // mode=1
    }
    // 驱动程序
    #include <stdio.h>
    #include "2.h"
    int main(void)
    {
       int mode;
       printf("Enter 0 for metric mode, 1 for US mode: ");
       scanf("%d", &mode);
       while (mode >= 0)
       {
         set_mode(mode);
         get_info();
         show_info();
         printf("Enter 0 for metric mode, 1 for US mode");
         printf(" (-1 to quit): ");
         scanf("%d", &mode);
       }
       printf("Done.\n");
       return 0;
    }
    
  3. // 2.h
    #include<stdbool.h>
    bool set_mode(int m);
    void get_info(int mode, float *distance, float *fuel);
    void show_info(int mode, float distance, float fuel);
    
    // 2.c
    #include <stdio.h>
    #include <stdbool.h>
    #include "2.h"
    bool set_mode(int m)
    {
        bool res = true;
        if (m != 0 && m != 1)
        {
            printf("Invalid mode specified. Mode 1(US) used.\n");
            res = false;
        }
        return res;
    }
    void get_info(int mode,float * distance,float * fuel)
    {
        if (mode == 0)
        {
            printf("Enter distance traveled in kilometers:");
            scanf("%f", distance);
            printf("Enter fuel consumed in liters:");
            scanf("%f", fuel);
        }
        else if (mode == 1)
        {
            printf("Enter distance traveled in miles : ");
            scanf("%f", distance);
            printf("Enter fuel consumed in gallons : ");
            scanf("%f", fuel);
        }
    }
    void show_info(int mode, float distance, float fuel)
    {
        if (mode == 0)
            printf("Fuel consumption is %g liters per 100 km.\n", fuel / distance * 100); // mode=0
        else if (mode == 1)
            printf("Fuel consumption is %g miles per gallon.\n", distance / fuel); // mode=1
    }
    
    // 
    #include <stdio.h>
    #include "2.h"
    int main(void)
    {
        int mode;
        int temp;
        float distance, fuel;
        printf("Enter 0 for metric mode, 1 for US mode: ");
        scanf("%d", &mode);
        temp = mode;
        while (mode >= 0)
        {
            if (!set_mode(mode))
            {
                get_info(temp, &distance, &fuel);
                show_info(temp, distance, fuel);
            }
            get_info(mode, &distance, &fuel);
            show_info(mode, distance, fuel);
            printf("Enter 0 for metric mode, 1 for US mode");
            printf(" (-1 to quit): ");
            temp = mode;
            scanf("%d", &mode);
        }
        printf("Done.\n");
        return 0;
    }
    
  4. #include <stdio.h>
    int fun(void)
    {
        static int i = 0;
        i++;
        return i;
    }
    int main(void)
    {
        for (int i = 0; i < 10; i++)
        {
            printf("i = %d function: %d\n", i, fun());
        }
        return 0;
    }
    
  5. #include <stdio.h>
    #include <stdlib.h>
    int main(void)
    {
        int arr[100];
        for (int i = 0; i < 100; i++)
        {
            arr[i] = rand() % 10 + 1;
        }
        for (int i = 0; i < 99; i++)
        {
            for (int j = 0; j < 99 - i; j++)
            {
                if (arr[j] < arr[j + 1])
                {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        for (int i = 0; i < 100; i++)
            printf("%d ", arr[i]);
        printf("\n");
        return 0;
    }
    
  6. #include <stdio.h>
    #include <map>
    #include <stdlib.h>
    using namespace std;
    int main(void)
    {
        map<int, int> m;
        m[1] = 0;
        m[2] = 0;
        m[3] = 0;
        m[4] = 0;
        m[5] = 0;
        m[6] = 0;
        m[7] = 0;
        m[8] = 0;
        m[9] = 0;
        m[10] = 0;
        srand(1);
        for (int i = 0; i < 1000; i++)
        {
            m[rand()]++;
        }
        for (int i = 1; i < 11; i++)
        {
            printf("%d ", m[i]);
        }
        printf("\n");
        return 0;
    }
    
  7. // pe12-8.c
    #include <stdio.h>
    #include <stdlib.h>
    int *make_array(int elem, int val);
    void show_array(const int ar[], int n);
    int main(void)
    {
        int *pa;
        int size;
        int value;
        printf("Enter the number of elements: ");
        while (scanf("%d", &size) == 1 && size > 0)
        {
            printf("Enter the initialization value: ");
            scanf("%d", &value);
            pa = make_array(size, value);
            if (pa)
            {
                show_array(pa, size);
                free(pa);
            }
            printf("Enter the number of elements (<1 to quit): ");
        }
        printf("Done.\n");
        return 0;
    }
    int *make_array(int elem, int val)
    {
        int *arr = (int *)malloc(sizeof(int) * elem);
        for (int i = 0; i < elem; i++)
        {
            arr[i] = val;
        }
        return arr;
    }
    void show_array(const int ar[], int n)
    {
        for (int i = 0; i < n; i++)
        {
            printf("%d ", ar[i]);
            if ((i + 1) % 8 == 0)
                printf("\n");
        }
        printf("\n");
    }
    
  8. #include<iostream>
    #include <string>
    using namespace std;
    int main(void)
    {
    	int n;
    	printf("How many words do you wish to enter? ");
    	cin >> n;
    	printf("Enter %d words now:\n", n);
    	string word;
    	string *arr = new string[n];
    	for (int i = 0; i < n; i++)
    	{
    		cin >> word;
    		arr[i] = word;
    	}
    	printf("Here are your words:\n");
    	for (int i = 0; i < n; i++)
    	{
    		cout << arr[i] << endl;
    	}
    	return 0;
    }
    
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jian圣楠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值