`
kfy31kfy
  • 浏览: 17016 次
社区版块
存档分类
最新评论

C语言内存管理(二)

 
阅读更多

C语言内存管理(二)
2011年04月15日
  
label 5
  calloc
  void * calloc( size_t nmemb,size_t size);
  calloc的作用是分配并初始化内存块,返回一个指向nmemb块数组的指针,每块大小为size个字节。它和malloc的主要不同之处是会初始化(清零)分配到的内存。
  示范代码如下:
  #include
  #include
  void main( void )
  {
  long *buffer;
  buffer = (long *)calloc( 40, sizeof( long ) );
  if( buffer != NULL )
  printf( "Allocated 40 long integers\n" );
  else
  printf( "Can't allocate memory\n" );
  free( buffer );
  }
  label 6
  Realloc
  void * realloc( void *ptr, size_t size );
  realloc以ptr所指地址为首址,分配size个字节的内存,并返回ptr所指地址。realloc不会初始化分配到的内存块,如果ptr为NULL则相当于malloc,如果size为NULL则相当于free(ptr)。不能分配返回NULL。
  示范代码如下:
  #include
  #include
  #include
  void main( void )
  {
  long *buffer;
  size_t size;
  if( (buffer = (long *)malloc( 1000 * sizeof( long ) )) == NULL )
  exit( 1 );
  size = _msize( buffer );
  printf( "Size of block after malloc of 1000 longs: %u\n", size );
  /* Reallocate and show new size: */
  if( (buffer = realloc( buffer, size + (1000 * sizeof( long )) ))
  ==  NULL )
  exit( 1 );
  size = _msize( buffer );
  printf( "Size of block after realloc of 1000 more longs: %u\n",
  size );
  free( buffer );
  exit( 0 );
  }
  label 7
  通过上面的学习,我们知道:alloca、calloc、malloc、realloc 负责分配内存,free 负责释放内存。其中alloca 是在栈中分配内存,而calloc、malloc、realloc 是在堆中分配内存,也就是说 alloca 的内存分配,是有作用域的,不需要释放,而calloc、malloc、realloc内存是没有作用域的,需要调用 free 主动释放分配内存区域。alloca,malloc,realloc只负责分配内存,并不初始化分配内存空间,而calloc不仅分配内存,还负责初始化分配内存为0。realloc 是以传入指针为基址,分配指定大小的内存区域。
  当读者阅读到此时的时候,可能觉得内存管理其实很简单,无非是分配内存释放内存而已。大家不妨看看如下一个程序:
  void MyGetMemory(int iSize)
  {
  char * szTemp=(char *)malloc(iSize);
  if(!GetString(szTemp,iSize))
  {
  printf("getstring failed!\n");//应该在分配失败后也释放内存
  return;
  }
  ...
  free(szTemp);
  }
  相信大家能很快发现上面在GetString 函数返回失败的情况下,内存没有释放,将产生内存泄露。如果我们再更改一下,可能这个错误稍微隐蔽一点。
  char * MyGetMemory(int iSize)
  {
  char * szTemp=(char *)malloc(iSize);
  if(!GetString(szTemp,iSize))
  {
  printf("getstring failed!\n");
  return NULL;
  }
  return szTemp;     
  }
  void Test()
  {
  char * szMalloc=MyGetMemory(23);
  if(szMalloc)
  {
  printf("out : %s \n",szMalloc);
  free(szMalloc);
  szMalloc=NULL;
  }
  }
  这个程序的内存泄露同样是在GetString 失败的时候产生,我们单存分析Test 函数是发现不了内存泄露的。在实际项目中,由于较为复杂,可能忘记释放内容了,也有可能释放内容后再次释放内容等等,这些错误要么是程序运行时间越久,所耗内存越大,要么直接出现异常。如果分配了内存忘记释放,那样就产生了内存泄漏。为了防止内存泄漏,一些项目甚至要求对分配、释放内存进行跟踪,以避免内存泄漏。最简单的方法就是封装内存分配和释放函数,实际分配中并不直接调用alloca、calloc、malloc、realloc来分配内存,也不直接调用函数free来释放内存。另外,在服务器上,由于程序需要长期执行,频繁的分配内存资源会导致内存碎片的增多,这样可以使用内存池来解决这些问题。
  既然内存管理错误这么频繁,后果这么严重,那么作为一个新手程序应该如何来避免这些问题呢?在下一节我们将详细介绍。
  1-3   C语言内存使用要点及常见错误
  在介绍内存使用要点之前,我们先看看使用C语言内存管理中经常出现的错误,尤其是新手。
  label 8
  1、  内存分配后没有校验,使得内存未成功,却正常使用。
  2、  内存分配成功后,没有初始化就使用。
  3、  内存分配成功,也进行了初始化,可是使用内存时出现了越界(缓冲区溢出)。这种错误如果被黑客成功利用,最严重的后果就是整个操作系统被黑客完全控制。
  4、  内存分配成功后,忘记释放内存,产生内存泄漏。
  5、  内存分配成功后,也正常释放内存,可是在释放内存后又继续使用。
  6、  混淆指针和数组。
  上面的这些问题,不仅仅是新手容易犯,一个工作多年的老程序员依然可能犯这样的错误。如果有一天,您发现您的程序在debug下可以成功运行,可是在release下却不能成功运行,一种可能就是您的数据没有被初始化。如果有一天,您的程序出现一个访问一个非常内存地址的错误,那么你应该检查一下是否产生了越界错误等等。总而言之,上面的任何一种错误出现了,就极有可能不好定位错误,尤其是访问越界、释放后继续使用的错误。
  内存分配后不校验直接使用主要是新手犯这种错误,由于没有意识到内存分配会不成功。这种问题的解决办法很简单,判断指针是否为空就可以了,在上节中的例子比比皆是,就不列举出来了。另外一种情况是函数的入参为空,可以使用assert(p!=NULL) 来简单检查,也可以使用if(p!=NULL) 来判断。这类错误只要养成好习惯,是完全可以避免的。
  内存分配后没有初始化,这种情况也是属于粗心引起,也通常是新手犯的错误。从上节中我们知道,alloca,malloc,realloc是只负责分配内存而不负责初始化内存的,完全可以想象,不初始化直接使用会导致不可预知的错误。其实,内存没有初始化是出现所有内存分配的情况下,可能是全局的,也有可能出现在栈上,当然更多是出现在堆分配上。比如如下的例子就是一个堆分配后没有初始化出现的错误,其实在实际项目中,肯定没有这么明显,中途隔了很多代码,所以往往不容易发现。
  int iTimes;
  for(iTimes=0;iTimes0)
  {
  …
  iTimes--;
  }
  后来由于程序需要我们注释掉了那段for循环,结果变成了
  int iTimes;
  /*
  for(iTimes=0;iTimes0)
  {
  …
  iTimes--;
  }
  当然上面的错误实在是太明显了。实际上这种例子主要是修改了程序的某个逻辑后才出现的,尤其是删出逻辑后。比如在c++类的构造删除中没有初始化成员指针变量,我们先前必须调用该类的某个函数来分配并初始化,可是后来我们去掉了这个函数,而我们在析构函数中还保留着该指针的释放,这样当然会导致错误。总之,不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
  访问内存越界错误可以说是一类非常严重的错误,因为这种错误非常难找。黑客也通常利用这种漏洞来入侵系统。这种错误出现几率最常见的就是文件路径,我们可能想当然的认为文件名不会超过255,因为是我们自己的文件名,认为完全可以控制,可是由于种种原因我们的文件名大于了255,那么会产生什么样的错误呢?由于我们的文件名需要使用memcpy复制内容,那么有可能我们的一部份代码数据可能被覆盖了,会出现什么错误只有天知道,尤其是每次的执行流程未知,产生的后果每次都不一样,这样的错误通常是很难定位的。鉴于此,微软在vc2005中增加了一系列_l的函数,例如_sprintf_l 增加了缓冲区长度,以减少这类问题。也就是说,对于程序员,无论是新手还是老手,在使用内存拷贝等情况下一定要考虑越界问题。
  常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
  对于忘记释放内存的问题,也是一个比较热门的话题。出现这种情况的后果是,所占内存越来越大,系统越来越慢。尤其是对于服务器程序,由于需要长时间运行,所以内存泄漏就变得非常重要了。所以我们在程序设计时一定要保证malloc/free、new/delete成对使用。这类问题还有可能出现在函数中分配内存的场合。例子如下:
  void getpathname(char * szPath)
  {
  if(szPath == NULL)
  {
  szPath=(char *)malloc(1024);
  }
  memcpy(szPath,”mycopybuffer”,11);
  }
  void callf()
  {
  char * szPath;
  szPath=NULL;
  getpathname (szPath);
  if(szPath != NULL)
  {
  Free(szPath);
  szPath=NULL;
  }
  }
  对于上面这个例子,曾经有人还说过,我分配了,也释放了,可是就是有内存泄漏。不过更多的情况是发现数据不正确,还不知道怎么回事,其实就是对指针理解不够。对于上面的例子,解决办法通常有三个,一是直接返回指针,二是传入 char **,三是如果是c++可以传入 char * &。另外,需要避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
  释放后继续使用内存指针的问题也是一个非常严重的问题,如果加上逻辑复杂,测试不能遍历所有路径的话,极有可能成为一个小BUG。那么出现这种问题有哪些情况呢?
  1、  程序逻辑异常复杂,释放之后并没有将所有指向这块内存区域的指针清0。
  2、  在函数中返回了栈内存的指针或引用。
  3、  使用free或delete释放了内存后,没有将指针设置为NULL,导致产生“野指针”。还有就是存在多个这向该内存的指针,释放时没有将所有指向该内存地址的指针设置为NULL。
  程序逻辑异常的例子不太好找,但最容易理解。避免这类情况的方法就是尽量避免多个指针同时指向一个内存地址空间。如果的确不可避免,一定要在释放内存后,将所有指向该地址的指针都设置成NULL。
  函数种返回了栈内存地址或引用这个例子比较容易理解。如下:
  char * GetTemp()
  {
  char szTemp[]=”hello”;
  return szTemp;
  }
  在上面的例子中,由于szTemp属于栈内分配内存,在函数执行完成后,将自动释放szTemp分配的栈内存,所以调用GetTemp 函数取得的指针指向内存地址是一个无效地址,该内存中存储内容是不可预见的。
  使用“野指针”的例子比较好理解。如下:
  void test()
  {
  char * pTemp;
  pTemp = (char *) malloc(256);
  if(pTemp != NULL)
  {
  strcpy(pTemp,”mytemp1”);
  printf(“%s\n”,pTemp);
  }
  free(pTemp);
  if(pTemp != NULL)
  {
  strcpy(pTemp,”mytemp2”);
  printf(“%s\n”,pTemp);
  }
  }
  上面的例子在 strcpy(pTemp,”mytemp2”); 就会出错,因为此时内存已经释放了。我们上面的例子非常容易察觉这个问题。可是我们在实际工程项目中,由于代码量大,程序非常复杂,如果不养成一中良好的习惯,极有可能会出现上面的问题,而且还不太好定位。有人也许会说,我检查下 free函数不就可以了么。关键的问题是,那是您知道是没有将这个指针在释放时设置为 NULL。那么我们在实际项目中,如何杜绝“野指针”呢?
  1、指针变量必须进行初始化。
  char *p = NULL;
  char *str = (char *) malloc(100);
  2、指针被释放以后,必须将所有指向该块内存区域的指针全部设置为NULL。
  3、指针指向的内容是栈分配的内存时,一定要注意作用域的问题。
  void Test(void)
  {
    char * p;
  p=NULL;
    {
      char szTemp[] =”hello”;
  p=szTemp; // 注意 szTemp 的生命期
   }
   printf(“%s\n”, p); // p是“野指针”
  }
  另外,由于指针和数组在很多情况下可以互换,导致混淆,容易犯一些小小的错误。其实,数组要么在静态存储区被创建,要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有内容可以改变。指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。下面列举几种容易出错的例子。
  例1:
  char arr[] = "demo";
  arr[0] = 'W';
  cout 不能发现该错误
  cout 不能发现这个错误。
  例2:
  char arr[] = "demodemo";
  char *pointer = arr;
  cout不能计算出指针的容量,除非您在分配内存的时候记住它。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics