2.2 内存管理

无论是C或C++,内存分配都只有三种方式:

(1)从静态存储区域分配。内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量、static变量。

(2)在栈上创建。执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3)从堆上分配,也称动态内存分配。程序在运行的时候用malloc()或new()申请任意大小的内存,由程序员决定何时用free()或delete()释放。因此,动态内存的生存期由程序员决定,使用非常灵活,但也是编程出现问题最多的地方。

C语言中与内存申请相关的函数主要有:alloca()、calloc()、malloc()、free()、realloc()、sbrk()等。

其中,alloca()是向栈申请内存,因此无需释放;malloc()分配的内存是位于堆中的,并且没有初始化内存的内容,因此基本上使用malloc()分配之后,都会调用函数memset()来初始化这部分的内存空间;calloc()则初始化这部分内存,并将其设置为0;realloc()则对malloc()申请的内存大小进行调整,申请的内存最终需要通过函数free()来释放;而sbrk()则是增加数据段的大小。

malloc()/calloc()/free()基本上都是C函数库实现的,与操作系统(以下简称OS)无关。C函数库内部通过一定的结构来保存当前有多少可用内存。如果程序使用malloc()分配的内存大小超出库里所留存的空间,那么将首先调用sbrk()增加可用空间,然后再分配空间。使用free()释放的内存并不立即返回给OS,而是保留在内部结构中。可以打个比方:sbrk()类似于批发,一次性向OS申请大块的内存,而malloc()等函数则类似于零售,只要满足程序运行时的要求即可,这套机制类似于缓冲。

之所以使用这套机制,是因为系统调用不能支持任意大小的内存分配(有的系统调用只支持固定大小以及其倍数的内存申请),这样的话,对于小内存的分配会造成浪费。系统调用申请内存代价昂贵,涉及到用户态和核心态的转换。

函数malloc()和calloc()都可以用来分配动态内存空间,但两者稍有区别。

malloc()函数有一个参数,即要分配的内存空间的大小,函数原型:

    void *malloc(size_t size);

calloc()函数有两个参数,分别为元素的数目和每个元素的大小,这两个参数的乘积就是要分配的内存空间的大小,函数原型:

    void *calloc(size_t numElements,size_t sizeOfElement);

如果调用成功,函数malloc()和calloc()都将返回所分配内存空间的首地址。

malloc() 函数和calloc()函数的主要区别是前者不能初始化所分配的内存空间。如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之,如果这部分内存空间曾经被分配、释放和重新分配,则其中可能遗留各种各样的数据,这也就是使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常运行,但经过一段时间后(内存空间已被重新分配)可能会出现问题的原因所在。

calloc() 函数会将所分配内存空间中的每一位都初始化为0,也就是说,如果为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果为指针类型的元素分配内存,那么这些元素通常(但无法保证)会被初始化为空指针;如果为实数类型的元素分配内存,那么这些元素可能(只在某些计算机中)会被初始化为浮点型的0。

malloc() 函数和calloc()函数的另一点区别是:calloc()函数会返回一个由某种对象组成的数组,但malloc()函数只返回一个对象。为了明确是为一个数组分配内存空间,有些程序员会选用calloc()函数。但是,除了是否初始化所分配的内存空间这一点之外,绝大多数程序员认为以下两种函数的调用方式没有区别:

    calloc(numElements,sizeOfElement);
    malloc(numElements *sizeOfElement);

需要解释的一点是,理论上(按照ANSI C标准),指针的算术运算只能在一个指定的数组中进行,但是在实践中,即使C编译程序或翻译器遵循这种规定,许多C程序还是冲破了这种限制。因此,尽管malloc()函数并不能返回一个数组,它所分配的内存空间仍然能供一个数组使用(对realloc()函数来说同样如此,尽管它也不能返回一个数组)。

总之,当在calloc()函数和malloc()函数之间作选择时,只需考虑是否要初始化所分配的内存空间,而不用考虑函数是否能返回一个数组。

程序运行过程中调用malloc()分配内存,但是没有调用free()释放已分配的内存,会造成内存泄漏。即使申请的部分内存没有被使用,但是由于没有调用free()进行内存释放,因此系统认为这部分内存仍在使用,造成不断的向系统申请内存,导致系统可用内存不断减少。内存泄漏仅仅发生在程序运行阶段,当程序退出时,OS将回收所有的资源,这也就是为何有时重启一下程序后软件会重新正常运行的一个原因。

55.条件判断语句导致指针重复释放

例64:

    1  #include <stdlib.h>
    2  int  main()
    3  {
    4   int  i;
    5   struct  student
    6  {
    7    int  num;
    8    char  name[20];
    9    char  sex;
    10   int  age;
    11 };
    12 struct  student *p;
    13  p=(struct student*)calloc(1,sizeof(struct student));
    14  if(p==null)
    15  return 0;
    16  scanf("%d",&i);
    17  if(i!=1000)
    18  {
    19   p->num=1000;
    20   free(p);
    21  }
    22 else
    23 {
    24  p->num=i;
    25 }
    26 free(p);
    27 return(1);
    28 }

例64中,第17行语句当if语句判断条件为真时,指针p将被释放,被释放的指针p在后面将被再次释放,导致安全漏洞。

56.指针被重复释放

例65:

    1  struct  st
    2   {
    3      int  number;
    4      char  *s;
    5      char  scores;
    6    }
    7  void  release(st *t)
    8    {
    9      free(t);
    10   }
    11  int  main()
    12   {
    13     st  *p=(st*)malloc(sizeof(st));
    14     if(p==null) return 0;
    15     release(p);
    16     free(p);
    17     return  1;
    18    }

例65中,第15行语句通过调用release()函数释放指针p,被释放的指针p在后面被free()函数再次释放,导致安全漏洞。

57.函数使用已释放的指针作为参数

例66:

    1  #include<stlib.h>
    2   struct  st
    3   {
    4    int  number;
    5    char  *s;
    6    char  scores;
    7    }
    8    f(st *t)
    9   {
    10    int  j;
    11    scanf("%d",&j);
    12    (t->scores)= (t->scores)+j;
    13  }
    14  int  main()
    15  {
    16     int  i;
    17     st  *p;
    18     st  *p=(st*)malloc(sizeof(st));
    19     if(p==null) return 0;
    20     free(p);
    21     f(p); //释放了内存后的指针p成为“野指针”,再继续使
用将非常危险。
    22   }

例66中,第20行语句对指针p进行了释放,释放后的指针p仍被作为参数传递给函数f()。

58.条件判断语句导致函数返回被释放的指针

例67:

    1  f(char *p)
    2   {
    3    return p;
    4   }
    5  char  *main()
    6  {
    7   char *x=malloc(1);
    8   char *y;
    9   y=x;
    10  if (y!=null)
    11  free(y);  //释放了内存后的指针y成为“野指针”。
    12  f(y);
    13 }

例67中第10行语句,当if语句为真时,指针y被释放,在这种情况下,程序中调用函数f(),函数f()将返回已经被释放的指针y,导致程序错误。

59.函数返回已释放的指针

例68:

    1  f(char *p)
    2  {return p;}
    3  main()
    4  {
    5    char * x = malloc(1);
    6    free(x);
    7    f(x);
    8   }

例68中,第6行语句释放了指针x,随后调用函数f(),函数f()将返回已经被释放的指针,导致程序错误。

60.条件判断语句导致使用已释放的内存

例69:

    1   #include<stlib.h>
    2   main()
    3   {
    4      int   *x=malloc(4);
    5      scanf("%d",&j);
    6      if(j>0)
    7        {
    8          free(x);
    9        }
    10     x=&j;
    11  }

例69中,当条件判断语句成立时,指针x将被释放(其指向的内存被释放),第10行语句给x赋值将产生使用释放内存的错误。

61.使用已释放的内存

例70:

    1  char *GetMemory(void)
    2  {
    3     char p[] = "hello world";
    4     return p;
    5  }
    6  void Test(void)
    7  {
    8     char *str= NULL;
    9     str = GetMemory();
    10    printf(str);
    11 }

例70中,GetMemory()函数返回的是指向“栈内存”的指针,str变量通过GetMemory()函数将得不到字符串“hello world”。

62.内存分配和释放函数不匹配

指针内存分配和释放函数正确匹配是malloc/free,new/delete,new[]/delete[]。

例71:

    1  #include<stlib.h>
    2  main()
    3  {
    4   int  j;
    5   int  *p;
    6   scanf("%d",&j);
    7   p=(int*)malloc(sizeof(int));
    8   if(j>0)
    9    {
    10    delete(p);
    11    p=null;
    12    }
    13 if(p!=null)
    14 free(p);
    15 }

例71中,第7行语句使用malloc()函数为指针p申请内存空间,第8行语句中当if条件成立时,第10行语句中使用与malloc()函数不匹配的delete()函数释放指针p的内存空间。

例72:

    1  main()
    2  {
    3   int  *p;
    4   p=(int*)malloc(sizeof(int));
    5   delete p;
    6  }

例72中,第4行语句使用malloc()函数为指针p申请内存空间,在第5行语句中使用与malloc()函数不匹配的delete()函数释放指针p的空间。

63.内存申请和释放函数不匹配

类中需要成对使用operator new和operator delete,operator new[]和operator delete[]以及new和delete。

例73:

    1  #include <stdio.h>
    2  class A
    3  {
    4  public:
    5      A() {}
    6      void* operator new(size_t size);
    7  };

例73中,类A使用operator new申请内存,在该类中并没有使用operator delete释放申请的内存。

修改方法:在第6行语句后添加语句“void operator delete( void* );”释放operator new申请的内存。

64.使用delete[]删除单个对象的内存空间

C++中,通过delete函数回收new函数为单个对象分配的内存空间,用delete[]函数回收new[]分配的数组空间。

例74:

    1  class A {
    2  public:
    3      A( ) {}
    4  };
    5
    6  void foo( ) {
    7      A *a = new A;
    8      delete[] a;
    9  }

例74中,a是使用new为单个对象分配的内存空间,在第8行语句中应该使用语句“delete a;”释放该内存空间。

65.使用delete删除已分配的数组空间

C++中一定要配对使用不同形式的new和delete。

例75:

    1  class A
    2  {
    3      public:
    4      A() {}
    5  };
    6  void foo() {
    7       A *a = new A[100];
    8       delete a;
    9  }

例75中,a由new[]申请生成具有100个对象的数组空间,删除时应该使用和new[]配对的delete[]删除。

修改方法:将第8行语句改为:“delete[] a;”。

66.条件判断语句导致非堆内存被释放

例76:

    1  #include<stlib.h>
    2  main()
    3  {
    4    const int  i=1;
    5    int j;
    6    int *p;
    7    p=&i; //p是指向常量静态内存的指针;
    8    scanf("%d",&j);
    9    if(j>0)
    10   {
    11     free(p);
    12   }
    13   }

例76中,第9行语句,当if条件成立时,程序试图通过指针p释放常量内存。

67.静态内存被释放

例77:

    1  main()
    2  {
    3    const int  i=1;
    4    int *p;
    5    p=&i;
    6    free(p);
    7  }

例77中,第6行语句,程序通过指针释放常量内存。

68.条件判断语句导致释放未分配的内存

例78:

    1  #include<stlib.h>
    2  main()
    3  {
    4    int  j;
    5    scanf("%d",&j);
    6    void   *p=malloc(j); //p指向j个字节大小的存储区。
    7    if(j>5)
    8      {
    9        p+=10; //p指针指向,向后移10个字节;
    10     }
    11  free(p);
    12 }

例78中,当if条件判断语句成立时,第11行语句将释放未分配的内存。

69.释放未分配的内存

例79:

    1  main()
    2  {
    3     void *p=malloc(10);
    4      p+=10;
    5     free(p);
    6  }

例79中,第5行语句试图释放未分配的内存。

70.条件判断语句导致空指针被释放

例80:

    1  #include<stlib.h>
    2  int main()
    3  {
    4    int  t;
    5    scanf("%d",&t);
    6    char *x;
    7    x = NULL;
    8    if(t>0)
    9    free(x);
    10   return 0;
    11 }

例80中,当第8行语句中的if条件成立时,程序将在第9行语句释放空指针x。

71.释放空指针

例81:

    1  int main()
    2  {
    3     char *x;
    4     x = NULL;
    5     free(x);
    6     return 0;
    7  }

例81中,第5行语句释放空指针x。