C 语言学习 第八章 存储类、链接和内存管理(二)

 上章入口:【C 语言学习 第八章 存储类、链接和内存管理(一)】

 

  •  2. 存储类和函数

  •         函数也具有存储类。函数可以是外部的 (默认情况下) 或者静态的 (C99 增加了第三种可能性,即内联函数)。外部函数可被其他文件中的函数调用,而静态函数只可以在定义它的文件中使用。例如,考虑一个包含如下函数声明的文件:
    double gamma (); /*默认为外部的*/
    static double beta ();
    extern double delta ();
           函数 gamma () 和 delta () 可被程序的其他文件中的函数使用,而 beta () 则不可以。因为beta () 被限定在一个文件内,故可在其他文件中使用具有相同名称的不同函数。使用 static 存储类的原因之一就是创建为一个特定模块所私有的函数,从而避免可能的名字冲突。 通常使用关键字 extern 来声明在其他文件中定义的函数。这一习惯做法主要是为了使程序更清晰,因为除非函数声明使用了关键字 static , 否则认为它是 extern 的 。
  • 使用哪种存储类
           一般而言,自动类型是函数的默认类型。尽管外部存储乍看起来很有诱惑力——把变量都设成外部变量,就不用为使用参数和指针在函数之间传递数据而费心了。然而,这存在着一种不十分明显的缺陷。您将不得不为函数 A () 违背您的 意图,偷偷修改了函数 B () 所用的变量而焦急。多年来,无数程序员的经验给出了无可置疑的证据,证明随意使用外部变量带来的这一不十分明显的危险远比它所带来的表面吸引力量要。
           保护性程序设计中一个非常重要的规则就是 “需要知道” 原则。尽可能保持每个函数的内部工作对该函数的私有性,只共享那些需要共享的变量。除了自动类型以外,其他类型也是有用的,并且可用。但请在使用一个类型前,问问自己是否必须那样做。
  • 3. 分配内存:malloc/free

    •        这 5 种存储类有一个共同之处: 在决定了使用哪一存储类之后,就自动决定了作用域和存储时期。您的选择服从预先制定的内存管理规则。然而,还有另一个选择给您更多灵活性。这一选择就是使用 <stdlib.h> 库中的 malloc() 和 free() 函数来分配和管理内存。而这被称为动态内存分配。

    • 3.1 malloc 函数

    • void* malloc (size_t size);

             分配内存块。分配一个 size 字节的内存块,返回指向该块开头的指针。新分配的内存块的内容不会初始化,保留不确定的值。如果 size 为零,则返回值取决于特定的库实现(它可能是也可能不是 null 指针),但返回的指针不应被取消引用。

    • 3.2 free 函数

    • void free (void* ptr);

             解除分配内存块。解除 ptr 所指向的由 malloc 分配的内存块,用于重新分配。如果未指向使用上述函数分配的内存块,则会导致未定义的行为。如果为 null 指针,则该函数不执行任何操作。
             请注意,此函数不会更改自身的值,因此它仍然指向相同(现在无效)的位置。

    • 3.3 realloc 函数

    • void* realloc (void* ptr, size_t size);

              重新分配内存块。该函数可以将内存块移动到新位置(其地址由函数返回)。内存块的内容将保留到新大小和旧大小中的较小者,即使将内存块移动到新位置也是如此。如果新部分较大,则新分配部分的值不确定。如果这是一个空指针,则该函数的行为类似于 ,分配一个新的字节块并返回指向其开头的指针。

    • 3.4 calloc函数

    • void* calloc (size_t num, size_t size);

             分配和零初始化数组。为元素数组分配一个内存块,num 个元素,每个元素的字节长度为 size,并将其所有位初始化为零。有效结果是分配一个零初始化的内存块的字节。如果为零,则返回值取决于特定的库实现(它可能是也可能不是 null 指针),但返回的指针不应被取消引用。

    • 3.5 动态内存分配

    •        变⻓数组 (Variable-Length Array, VLA) 与 malloc() 在功能上有些一致。例如,它们都可以用来创建一个大小在运行时决定的数组:
      int viamal ()
      {
          int n;
          int * pi;
          scanf ("%d", &n);
          pi =(int *) malloc (n * sizeof (int));
          int ar [n]; / / 变 ⻓ 数 组
          pi[2] = ar[2] = -5;
      ...
      }
             一个区别在于 VLA 是自动存储的。自动存储的结果之一就是VLA 所用内存空间在运行完定义部分之后会自动释放。在本例中,就是函数 vlamal ()  终止的时候。因此不必使用 free ()。另一方面,使用由 malloc () 创建的数组不必局限在一个函数中。程序 5 是一个示范案例。
    • 3.6 注意事项

    •        1. 一个 malloc() / calloc() 必须对应一个 free() 函数。若缺少 free() 函数容易出现内存泄漏。
             2. free() 函数只能释放动态分配的内存块。不可以用与静态内存分配的内存块。
             3. 使用动态内存分配时,至少要保留一个及其以上的指针指向 malloc() / calloc() 分配的内存块。不能随意丢失分配好内存块的地址。
             4. 在使用 free() 后,不可再次使用被释放的内存块,否则会出现非法访问。这里建议在 free() 后把指针指向 NULL。
    • 程序 5:
      #include<stdio.h>
      #include<stdlib.h>
      #include<string.h>
      void del(char*s,char *p);
      
      int main(void)
      {
          char str[20],*p;
          printf("Enter a string:\n");
          gets(str);
          if((p=(char*)malloc(strlen(str)+1))==NULL)
          {
              printf("\n not enough memory to allocate buffer! ");
              exit(1);
          }
          del(str,p);
          strcpy(str,p);
          free(p);
          p=NULL;
          printf("%s\n",str);
          getchar();
          return 0;
      }
      void del(char*s,char*p)
      {
          while(*s&&*s=='*')
          {
              *p=*s;
              p++;
              s++;
          }
          while(*s)
          {
              if(*s!='*')
              {
                  *p=*s;
                  p++;
              }
              s++;
          }
          *p='\0';
      }

      结果:
      ****A*BC**DEF****
      ****ABCDEF
       

  • 3. ANSI C 的类型限定词

    • 4.1 类型限定词 const

    • 4.1.1 在指针和参量声明中使用 const
    •        在声明一个简单变量和数组时使用关键字 const 很简单。指针则要复杂 一些,因为不得不把让指针本身成为 const 与让指针指向的值成为 const 区分开来。下面的声明表明pf 指向的值必须是不变的:
      const float *pf; /* pf 指向一个常量浮点数值 */
             但 pf 本身的值可以改变。例如,它可以指向另一个 const 值。相反,下面的声明表明指针 pt 本身的值不可以改变:
      float *const pt; /* pt 是一个常量指针 */
              它必须总是指向同一个地址,但所指向的值可以改变。最后,下面的声明:
      const float * const ptr;

             意味着 ptr 必须总是指向同一个位置,并且它所指位置存储的值也不能改变。
             还有第三种放置 const 关键字的方法:

      float const* pfc; // 等同于:const float * pfc
    • 4.1.2 对全局数据使用 const

    •        回忆一下,使用全局变量被认为是一个冒险的方法,因为它暴露了数据,使程序的任何部分都可以错误地修改数据。如果数据是 const 的,这种危险就不存在了,因此对全局数据使用 const 限定词是很合理的。可以有 const 变量、const 数组和 const 结构。 然而,在文件之间共享const 数据时要小心。可以使用两个策略。首先是遵循外部变量的惯用规则: 在一个文件中进行定义声明,在其他文件中进行引用声明 (使用关键字extem ):

      //file.c 定义一些全局变量
      const double PI = 3.14159;
      const char * MONTHS[12]={
          "January","February","March","April","May","June","July",
          "August","September","October","November","December"
      };
      //file2.c 使用其他文件中定义的全局变量
      extern const double PI;
      extern const *MONTHS[];

             其次是将常量放在一个 include 文件中。这时还必须使用静态外部存储类:

      //file.h 定义一些全局变量
      const double PI = 3.14159;
      const char * MONTHS[12]={
          "January","February","March","April","May","June","July",
          "August","September","October","November","December"
      };
      //file.c 使用其他文件中定义的全局变量
      #include"file.h"
      //file2.c 使用其他文件中定义的全局变量
      #include"file.h"

             使用头文件的好处是不必惦记着在一个文件中进行定义声明,在下一个文件中进行引用声明: 全部文件都包含同一个头文件。缺点在于复制了数据。在前述的例子中,这不构成一个真正的问题;但如果常量数据包含着巨大的数组,它可能就是一个问题了。

    • 4.2 类型限定词 volatile

    •         限定词 volatile 告诉编译器该变量除了可被程序改变以外还可被其他代理改变。典型地,它被用于硬件地址和与其他并行运行的程序共享的数据。例如,一个地址中可能保存着当前的时钟时间。不管程序做些什么,该地址的值都会随着时间而改变。另一种情况是一个地址被用来接收来自其他计算机的信息。 语法同const:
      volatile int locl;      // locl 是一个易变的位置
      volatile int * ploc;    // ploc 指向一个易变的位置

             一个值可以同时是 const 和 volatile。例如,硬件时钟一般设定为不能由程序改变,这一点使它成为 const; 但它被程序以外的代理改变,这使它成为 volatile 的。只需在声明中同时使用这两个限定词,如下所示;顺序并不重要:

      volatile const int loc;
      const volatile int * ploc;
    • 4.3 类型限定词 restrict

    •        关键字 restrict 通过允许编译器优化某几种代码增强了计算支持。它只可用于指针,并表明指针是访问个数据对象的惟一且初始的方式。为了清楚为何这样做有用,我们需要看一些例子。考虑下面的例子: 
      int ar [10];
      int * restrict restar = (int *) mallos (10 * sizeof (int));
      int *par=ar;
      
             这里,指针 restar 是访问由 malloc() 分配的内存的惟一旦初始的方式。因此,它可以由关键字 restrict 限定。然而,par 指针既不是初始的,也不是访问数组 ar 中数据的惟一方式,因此不可以把它限定为 restrict。
             现在考虑下面这个更加复杂的例子 , 其 中 n 是一个 int:
      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; /* 给 出 错 误 的 结 果 */
             出现错误结果的原因是循环在 par 两次访问同一个数据之间,使用 ar 改变了该数据的值。 没有关键字 restrict,编译器将不得不设想比较糟的那种情形,也就是在两次使用指针之间,其他标识符可能改变了数据的值 。使用了关键字 restrict 以后,编译器可以放心地寻找计算的捷径。 可以将关键字 restrict 作为指针型函数参量的限定词使用。这意味着编译器可以假定在函数体内没有其他标识符修改指针指问的数据,因而可以试着优化代码,反之则不然。
             关键字 restrict 有两个读者。一个是编译器,它告诉编译器可以自由地做一些有关优化的假定。另一个读者是用户,它告诉用户仅使用满足 restrict 要求的参数。 一般,编译器无法检查您是否进循了这一限制, 如果您蔑视它也就是在让自己冒险。
    • 4.4 旧关键词的新位置

    •         C99 允许将类型限定词和存储类限定词 static 放在函数原型和函数头部的形式参量所属的初始方括号内。 对于类型限定词的情形,这样做为已有功能提供了一个可选语法。例如,下面是 一个使用旧语法的声明:
      void ofmouth ( int* const a1 , int* restrict a2 , int n ); // 以前的⻛格
             它表明 a1 是一个指问 int 的 const 指针。回忆一下,这意味着该指针是不变的,而不是它所指向的数据不变。还表明 a2 是一个受限指针,如上一节所述。等价的新语法如下:
      void ofmouth ( int a1[const] , int a2 [restrict] , int n ); // C99 允许
             static 的情形是不同的,因为它引发了一些新问题。例如,考虑如下原型:
       double stick (double ar [static 20]);
              使用 static 表明在函数调用中,实际参数将是一个指向数组首元素的指针,该数组至少具有 20 个元素 。 这样做的目的是允许编译器使用这个信息来优化函数的代码。
              与 restrict 相同,关键字 static 有两个读者。一个是编译器,它告诉编译器可以自由地做一些有关优化的假定。另一个是用户,它告诉用户仅使用满足 static 要求的参数。

 参考书籍:《C Primer Plus》【美】 Stephen Prata 著
                  《程序设计教程 用 C/C++ 语言编程》 周纯杰 何顶新 周凯波 彭刚 张惕远 编著 

  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值