C++内存管理与内存泄漏及其检测

一、内存错误的分类
a.内存访问错误
对内存进行读或写时发生的错误,可能是读未被初始化的内存单元,也可能是读写错误的内存单元。  
b.内存使用错误
主要是在动态请求内存之后没有正确释放产生的错误。

二、内存剖析(典型的c++内存模型)

 

BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。(其实我不太明白既然都是存全局变量的,那为什么要把已初始化的和未初始化的分开在两个段中进行管理)

代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)

栈(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

c++不同于C#、Java的一个地方是它可以动态管理内存,但鱼与熊掌两者不可兼得,灵活性的代价是程序员需要花费更多的精力保证代码不发生内存错误。

三、常见的内存访问错误和内存使用错误

具体来说,内存访问错误有下面这几种:访问未被初始化的内存单元、数组访问错误、访问无效的内存单元(0x000000,0x000005等)、写无效内存。

而内存使用错误有:1、请求内存之后没有将它释放,使new和delete成对出现可以避免这样的问题。2、释放一块内存后又再释放一次。


四、例子

 #include <iostream>   
using namespace std;  
int main()
{     
char* str1="four";     
char* str2=new char[4]; //not enough space      
char* str3=str2;     
cout<<str2<<endl;   //UMR      
strcpy(str2,str1);  //ABW      
cout<<str2<<endl;  //ABR     
delete str2;    
str2[0]+=2; //FMR and FMW     
delete str3;    //FFM   
}  
UMR:Uninitialized Memery Read.读未初始化内存   
ABW:Array Bound Write.数组越界写
FMR/W:Freed Memery Read/Write.读/写已被释放的内存
FFM:Free Freed Memery.释放已被释放的内存        

    由以上的程序,我们可以看到:在第5行分配内存时,忽略了字符串终止符"\0"所占空间导致了第8行的数组越界写(Array Bounds Write)和第9行的数组越界读(Array Bounds Read); 在第7行,打印尚未赋值的str2将产生访问未初始化内存错误(Uninitialized Memory Read);在第11行使用已经释放的变量将导致释放内存读和写错误(Freed Memory Read and Freed Memory Write);最后由于str3和str2所指的是同一片内存,第12行又一次释放了已经被释放的空间 (Free Freed Memory)。

这个包含许多错误的程序可以编译连接,而且可以在很多平台上运行。但是这些错误就像定时炸弹,会在特殊配置下触发,造成不可预见的错误。这就是内存错误难以发现的一个主要原因。

 

内存泄漏的定义

  一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。以下这段小程序演示了堆内存发生泄漏的情形:

 void MyFunction(int nSize)
{
 char* p= new char[nSize];
 if( !GetStringFrom( p, nSize ) ){
  MessageBox(“Error”);
  return;
 }
 …//using the string pointed by p;
 delete p;
}
例一

  当函数GetStringFrom()返回零的时候,指针p指向的内存就不会被释放。这是一种常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存,但是c函数可以在任何地方退出,所以一旦有某个出口处没有释放应该释放的内存,就会发生内存泄漏。

  广义的说,内存泄漏不仅仅包含堆内存的泄漏,还包含系统资源的泄漏(resource leak),比如核心态HANDLE,GDI Object,SOCKET, Interface等,从根本上说这些由操作系统分配的对象也消耗内存,如果这些对象发生泄漏最终也会导致内存的泄漏。而且,某些对象消耗的是核心态内存,这些对象严重泄漏时会导致整个操作系统不稳定。所以相比之下,系统资源的泄漏比堆内存的泄漏更为严重。

  GDI Object的泄漏是一种常见的资源泄漏:

 void CMyView::OnPaint( CDC* pDC )
{
 CBitmap bmp;
 CBitmap* pOldBmp;
 bmp.LoadBitmap(IDB_MYBMP);
 pOldBmp = pDC->SelectObject( &bmp );
 …
 if( Something() ){
  return;
 }
 pDC->SelectObject( pOldBmp );
 return;
}
  例二

  当函数Something()返回非零的时候,程序在退出前没有把pOldBmp选回pDC中,这会导致pOldBmp指向的HBITMAP对象发生泄漏。这个程序如果长时间的运行,可能会导致整个系统花屏。这种问题在Win9x下比较容易暴露出来,因为Win9x的GDI堆比Win2k或NT的要小很多。

  内存泄漏的发生方式:

  以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。比如例二,如果Something()函数一直返回True,那么pOldBmp指向的HBITMAP对象总是发生泄漏。

  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。比如例二,如果Something()函数只有在特定环境下才返回True,那么pOldBmp指向的HBITMAP对象并不总是发生泄漏。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,但是因为这个类是一个Singleton,所以内存泄漏只会发生一次。另一个例子:

 

 char* g_lpszFileName = NULL;

void SetFileName( const char* lpcszFileName )
{
 if( g_lpszFileName ){
  free( g_lpszFileName );
 }
 g_lpszFileName = strdup( lpcszFileName );
}
  例三

  如果程序在结束的时候没有释放g_lpszFileName指向的字符串,那么,即使多次调用SetFileName(),总会有一块内存,而且仅有一块内存发生泄漏。

  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。举一个例子:

class Connection

{
 public:
  Connection( SOCKET s);
  ~Connection();
  …
 private:
  SOCKET _socket;
  …
};

class ConnectionManager
{
 public:
  ConnectionManager(){}
  ~ConnectionManager(){
   list::iterator it;
   for( it = _connlist.begin(); it != _connlist.end(); ++it ){
    delete (*it);
   }
   _connlist.clear();
  }
  void OnClientConnected( SOCKET s ){
   Connection* p = new Connection(s);
   _connlist.push_back(p);
  }
  void OnClientDisconnected( Connection* pconn ){
   _connlist.remove( pconn );
   delete pconn;
  }
 private:
  list _connlist;
};
  例四

  假设在Client从Server端断开后,Server并没有呼叫OnClientDisconnected()函数,那么代表那次连接的Connection对象就不会被及时的删除(在Server程序退出的时候,所有Connection对象会在ConnectionManager的析构函数里被删除)。当不断的有连接建立、断开时隐式内存泄漏就发生了。


文章出处:http://www.diybl.com/course/3_program/c++/cppjs/20081124/152540_2.html

 


文章出处:http://www.diybl.com/course/3_program/c++/cppjs/20081124/152540.html

posted @ 2009-04-15 15:32  traveller  阅读(547)  评论(0编辑  收藏  举报