西西軟件下載最安全的下載網(wǎng)站、值得信賴的軟件下載站!

首頁編程開發(fā)VC|VC++ → 從內(nèi)存管 理、內(nèi)存泄漏、內(nèi)存回收探討C++內(nèi)存管理

從內(nèi)存管 理、內(nèi)存泄漏、內(nèi)存回收探討C++內(nèi)存管理

相關(guān)軟件相關(guān)文章發(fā)表評論 來源:西西整理時(shí)間:2013/1/7 22:44:58字體大。A-A+

作者:西西點(diǎn)擊:0次評論:0次標(biāo)簽: 內(nèi)存管理

  • 類型:電子教程大。438KB語言:中文 評分:6.0
  • 標(biāo)簽:
立即下載
2 頁 內(nèi)存泄漏

2 內(nèi)存泄漏

2.1 C++中動態(tài)內(nèi)存分配引發(fā)問題的解決方案

假設(shè)我們要開發(fā)一個String類,它可以方便地處理字符串?dāng)?shù)據(jù)。我們可以在類中聲明一個數(shù)組,考慮到有時(shí)候字符串極長,我們可以把數(shù)組大小設(shè)為200,但一般的情況下又不需要這么多的空間,這樣是浪費(fèi)了內(nèi)存。對了,我們可以使用new操作符,這樣是十分靈活的,但在類中就會出現(xiàn)許多意想不到的問題,本文就是針對這一現(xiàn)象而寫的,F(xiàn)在,我們先來開發(fā)一個String類,但它是一個不完善的類。的確,我們要刻意地使它出現(xiàn)各種各樣的問題,這樣才好對癥下藥。好了,我們開始吧!

/* String.h */
#ifndef STRING_H_
#define STRING_H_
class String
{
private:
char * str; //存儲數(shù)據(jù)
int len; //字符串長度
public:
String(const char * s); //構(gòu)造函數(shù)
String(); // 默認(rèn)構(gòu)造函數(shù)
~String(); // 析構(gòu)函數(shù)
friend ostream & operator<<(ostream & os,const String& st);
};
#endif
/*String.cpp*/
#include <iostream>
#include <cstring>
#include "String.h"
using namespace std;
String::String(const char * s)
{
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
}//拷貝數(shù)據(jù)
String::String()
{
len =0;
str = new char[len+1];
str[0]='"0';
}
String::~String()
{
cout<<"這個字符串將被刪除:"<<str<<'"n';//為了方便觀察結(jié)果,特留此行代碼。
delete [] str;
}
ostream & operator<<(ostream & os, const String & st)
{
os << st.str;
return os;
}
/*test_right.cpp*/
#include <iostream>
#include <stdlib.h>
#include "String.h"
using namespace std;
int main()
{
String temp("天極網(wǎng)");
cout<<temp<<'"n';
system("PAUSE");
return 0;
}

  

  運(yùn)行結(jié)果:

  天極網(wǎng)
  請按任意鍵繼續(xù). . .

  大家可以看到,以上程序十分正確,而且也是十分有用的?墒,我們不能被表面現(xiàn)象所迷惑!下面,請大家用test_String.cpp文件替換test_right.cpp文件進(jìn)行編譯,看看結(jié)果。有的編譯器可能就是根本不能進(jìn)行編譯!

test_String.cpp:

#include <iostream>
#include <stdlib.h>
#include "String.h"
using namespace std;
void show_right(const String&);
void show_String(const String);//注意,參數(shù)非引用,而是按值傳遞。
int main()
{
String test1("第一個范例。");
String test2("第二個范例。");
String test3("第三個范例。");
String test4("第四個范例。");
cout<<"下面分別輸入三個范例:"n";
cout<<test1<<endl;
cout<<test2<<endl;
cout<<test3<<endl;
String* String1=new String(test1);
cout<<*String1<<endl;
delete String1;
cout<<test1<<endl; //在Dev-cpp上沒有任何反應(yīng)。
cout<<"使用正確的函數(shù):"<<endl;
show_right(test2);
cout<<test2<<endl;
cout<<"使用錯誤的函數(shù):"<<endl;
show_String(test2);
cout<<test2<<endl; //這一段代碼出現(xiàn)嚴(yán)重的錯誤!
String String2(test3);
cout<<"String2: "<<String2<<endl;
String String3;
String3=test4;
cout<<"String3: "<<String3<<endl;
cout<<"下面,程序結(jié)束,析構(gòu)函數(shù)將被調(diào)用。"<<endl;
return 0;
}
void show_right(const String& a)
{
cout<<a<<endl;
}
void show_String(const String a)
{
cout<<a<<endl;
}

  運(yùn)行結(jié)果:

  下面分別輸入三個范例:
  第一個范例。
  第二個范例。
  第三個范例。
  第一個范例。
  這個字符串將被刪除:第一個范例。
  使用正確的函數(shù):
  
  第二個范例。
  第二個范例。
  使用錯誤的函數(shù):
  第二個范例。
  這個字符串將被刪除:第二個范例。
  這個字符串將被刪除:?=
  ?=
  String2: 第三個范例。
  String3: 第四個范例。
  下面,程序結(jié)束,析構(gòu)函數(shù)將被調(diào)用。
  這個字符串將被刪除:第四個范例。
  這個字符串將被刪除:第三個范例。
  這個字符串將被刪除:?=
  這個字符串將被刪除:x =
  這個字符串將被刪除:?=
  這個字符串將被刪除:

現(xiàn)在,請大家自己試試運(yùn)行結(jié)果,或許會更加慘不忍睹呢!下面,我為大家一一分析原因。

首先,大家要知道,C++類有以下這些極為重要的函數(shù):

一:復(fù)制構(gòu)造函數(shù)。

二:賦值函數(shù)。

我們先來講復(fù)制構(gòu)造函數(shù)。什么是復(fù)制構(gòu)造函數(shù)呢?比如,我們可以寫下這樣的代碼:String test1(test2);這是進(jìn)行初始化。我們知道,初始化對象要用構(gòu)造函數(shù)?蛇@兒呢?按理說,應(yīng)該有聲明為這樣的構(gòu)造函數(shù):String(const String &);可是,我們并沒有定義這個構(gòu)造函數(shù)呀?答案是,C++提供了默認(rèn)的復(fù)制構(gòu)造函數(shù),問題也就出在這兒。

(1):什么時(shí)候會調(diào)用復(fù)制構(gòu)造函數(shù)呢?(以String類為例。)

  在我們提供這樣的代碼:String test1(test2)時(shí),它會被調(diào)用;當(dāng)函數(shù)的參數(shù)列表為按值傳遞,也就是沒有用引用和指針作為類型時(shí),如:void show_String(const String),它會被調(diào)用。其實(shí),還有一些情況,但在這兒就不列舉了。

(2):它是什么樣的函數(shù)。

它的作用就是把兩個類進(jìn)行復(fù)制。拿String類為例,C++提供的默認(rèn)復(fù)制構(gòu)造函數(shù)是這樣的:

String(const String& a)
{
str=a.str;
len=a.len;
}

在平時(shí),這樣并不會有任何的問題出現(xiàn),但我們用了new操 作符,涉及到了動態(tài)內(nèi)存分配,我們就不得不談?wù)劀\復(fù)制和深復(fù)制了。以上的函數(shù)就是實(shí)行的淺復(fù)制,它只是復(fù)制了指針,而并沒有復(fù)制指針指向的數(shù)據(jù),可謂一點(diǎn) 兒用也沒有。打個比方吧!就像一個朋友讓你把一個程序通過網(wǎng)絡(luò)發(fā)給他,而你大大咧咧地把快捷方式發(fā)給了他,有什么用處呢?我們來具體談?wù)劊?/p>

假如,A對象中存儲了這樣的字符串:“C++”。它的地址為2000,F(xiàn)在,我們把A對象賦給B對象:String B=A。現(xiàn)在,A和B對象的str指針均指向2000地址?此瓶梢允褂茫绻鸅對象的析構(gòu)函數(shù)被調(diào)用時(shí),則地址2000處的字符串“C++”已經(jīng)被從內(nèi)存中抹去,而A對象仍然指向地址2000。這時(shí),如果我們寫下這樣的代碼:cout<<A<<endl;或是等待程序結(jié)束,A對象的析構(gòu)函數(shù)被調(diào)用時(shí),A對象的數(shù)據(jù)能否顯示出來呢?只會是亂碼。而且,程序還會這樣做:連續(xù)對地址2000處使用兩次delete操作符,這樣的后果是十分嚴(yán)重的!

本例中,有這樣的代碼:

String* String1=new String(test1);
cout<<*String1<<endl;
delete String1;

  假設(shè)test1中str指向的地址為2000,而String中str指針同樣指向地址2000,我們刪除了2000處的數(shù)據(jù),而test1對象呢?已經(jīng)被破壞了。大家從運(yùn)行結(jié)果上可以看到,我們使用cout<<test1時(shí),一點(diǎn)反應(yīng)也沒有。而在test1的析構(gòu)函數(shù)被調(diào)用時(shí),顯示是這樣:“這個字符串將被刪除:”。

再看看這段代碼:

cout<<"使用錯誤的函數(shù):"<<endl;
show_String(test2);
cout<<test2<<endl;//這一段代碼出現(xiàn)嚴(yán)重的錯誤!

show_String函數(shù)的參數(shù)列表void show_String(const String a)是按值傳遞的,所以,我們相當(dāng)于執(zhí)行了這樣的代碼:String a=test2;函數(shù)執(zhí)行完畢,由于生存周期的緣故,對象a被析構(gòu)函數(shù)刪除,我們馬上就可以看到錯誤的顯示結(jié)果了:這個字符串將被刪除:?=。當(dāng)然,test2也被破壞了。解決的辦法很簡單,當(dāng)然是手工定義一個復(fù)制構(gòu)造函數(shù)嘍!人力可以勝天!

String::String(const String& a)
{
len=a.len;
str=new char(len+1);
strcpy(str,a.str);
}

  我們執(zhí)行的是深復(fù)制。這個函數(shù)的功能是這樣的:假設(shè)對象A中的str指針指向地址2000,內(nèi)容為“I am a C++ Boy!”。我們執(zhí)行代碼String B=A時(shí),我們先開辟出一塊內(nèi)存,假設(shè)為3000。我們用strcpy函數(shù)將地址2000的內(nèi)容拷貝到地址3000中,再將對象B的str指針指向地址3000。這樣,就互不干擾了。

大家把這個函數(shù)加入程序中,問題就解決了大半,但還沒有完全解決,問題在賦值函數(shù)上。我們的程序中有這樣的段代碼:

String String3;
String3=test4;

  經(jīng)過我前面的講解,大家應(yīng)該也會對這段代碼進(jìn)行尋根摸底:憑什么可以這樣做:String3=test4???原因是,C++為了用戶的方便,提供的這樣的一個操作符重載函數(shù):operator=。所以,我們可以這樣做。大家應(yīng)該猜得到,它同樣是執(zhí)行了淺復(fù)制,出了同樣的毛病。比如,執(zhí)行了這段代碼后,析構(gòu)函數(shù)開始大展神威^_^。由于這些變量是后進(jìn)先出的,所以最后的String3變量先被刪除:這個字符串將被刪除:第四個范例。很正常。最后,刪除到test4的時(shí)候,問題來了:這個字符串將被刪除:?=。原因我不用贅述了,只是這個賦值函數(shù)怎么寫,還有一點(diǎn)兒學(xué)問呢!大家請看:

平時(shí),我們可以寫這樣的代碼:x=y=z。(均為整型變量。)而在類對象中,我們同樣要這樣,因?yàn)檫@很方便。而對象A=B=C就是A.operator=(B.operator=(c))。而這個operator=函數(shù)的參數(shù)列表應(yīng)該是:const String& a,所以,大家不難推出,要實(shí)現(xiàn)這樣的功能,返回值也要是String&,這樣才能實(shí)現(xiàn)A=B=C。我們先來寫寫看:

String& String::operator=(const String& a)
{
delete [] str;//先刪除自身的數(shù)據(jù)
len=a.len;
str=new char[len+1];
strcpy(str,a.str);//此三行為進(jìn)行拷貝
return *this;//返回自身的引用
}

是不是這樣就行了呢?我們假如寫出了這種代碼:A=A,那么大家看看,豈不是把A對象的數(shù)據(jù)給刪除了嗎?這樣可謂引發(fā)一系列的錯誤。所以,我們還要檢查是否為自身賦值。只比較兩對象的數(shù)據(jù)是不行了,因?yàn)閮蓚對象的數(shù)據(jù)很有可能相同。我們應(yīng)該比較地址。以下是完好的賦值函數(shù):

String& String::operator=(const String& a)
{
if(this==&a)
return *this;
delete [] str;
len=a.len;
str=new char[len+1];
strcpy(str,a.str);
return *this;
}

把這些代碼加入程序,問題就完全解決,下面是運(yùn)行結(jié)果:

  下面分別輸入三個范例:
  第一個范例
  第二個范例
  第三個范例
  第一個范例
  這個字符串將被刪除:第一個范例。
  第一個范例
   使用正確的函數(shù):
  第二個范例。
  第二個范例。
   使用錯誤的函數(shù):
  第二個范例。
  這個字符串將被刪除:第二個范例。
  第二個范例。
  String2: 第三個范例。
  String3: 第四個范例。
  下面,程序結(jié)束,析構(gòu)函數(shù)將被調(diào)用。
  這個字符串將被刪除:第四個范例。
  這個字符串將被刪除:第三個范例。
  這個字符串將被刪除:第四個范例。
  這個字符串將被刪除:第三個范例。
  這個字符串將被刪除:第二個范例。
  這個字符串將被刪除:第一個范例。

2.2 如何對付內(nèi)存泄漏?

寫出那些不會導(dǎo)致任何內(nèi)存泄漏的代碼。很明顯,當(dāng)你的代碼中到處充滿了new 操作、delete操作和指針運(yùn)算的話,你將會在某個地方搞暈了頭,導(dǎo)致內(nèi)存泄漏,指針引用錯誤,以及諸如此類的問題。這和你如何小心地對待內(nèi)存分配工作其實(shí)完全沒有關(guān)系:代碼的復(fù)雜性最終總是會超過你能夠付出的時(shí)間和努力。于是隨后產(chǎn)生了一些成功的技巧,它們依賴于將內(nèi)存分配(allocations)與重新分配(deallocation)工作隱藏在易于管理的類型之后。標(biāo)準(zhǔn)容器(standard containers)是一個優(yōu)秀的例子。它們不是通過你而是自己為元素管理內(nèi)存,從而避免了產(chǎn)生糟糕的結(jié)果。想象一下,沒有string和vector的幫助,寫出這個:

#include<vector>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
int main() // small program messing around with strings
{
 cout << "enter some whitespace-separated words:"n";
 vector<string> v;
 string s;
 while (cin>>s) v.push_back(s);
 sort(v.begin(),v.end());
 string cat;
 typedef vector<string>::const_iterator Iter;
 for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+";
 cout << cat << ’"n’;
}

  你有多少機(jī)會在第一次就得到正確的結(jié)果?你又怎么知道你沒有導(dǎo)致內(nèi)存泄漏呢?

  注意,沒有出現(xiàn)顯式的內(nèi)存管理,宏,造型,溢出檢查,顯式的長度限制,以及指針。通過使用函數(shù)對象和標(biāo)準(zhǔn)算法(standard algorithm),我可以避免使用指針——例如使用迭代子(iterator),不過對于一個這么小的程序來說有點(diǎn)小題大作了。

  這些技巧并不完美,要系統(tǒng)化地使用它們也并不總是那么容易。但是,應(yīng)用它們產(chǎn)生了驚人的差異,而且通過減少顯式的內(nèi)存分配與重新分配的次數(shù),你甚至可以使余下的例子更加容易被跟蹤。早在1981年,我就指出,通過將我必須顯式地跟蹤的對象的數(shù)量從幾萬個減少到幾打,為了使程序正確運(yùn)行而付出的努力從可怕的苦工,變成了應(yīng)付一些可管理的對象,甚至更加簡單了。

  如果你的程序還沒有包含將顯式內(nèi)存管理減少到最小限度的庫,那么要讓你程序完成和正確運(yùn)行的話,最快的途徑也許就是先建立一個這樣的庫。

  模板和標(biāo)準(zhǔn)庫實(shí)現(xiàn)了容器、資源句柄以及諸如此類的東西,更早的使用甚至在多年以前。異常的使用使之更加完善。

  如果你實(shí)在不能將內(nèi)存分配/重新分配的操作隱藏到你需要的對象中時(shí),你可以使用資源句柄(resource handle),以將內(nèi)存泄漏的可能性降至最低。這里有個例子:我需要通過一個函數(shù),在空閑內(nèi)存中建立一個對象并返回它。這時(shí)候可能忘記釋放這個對象。畢竟,我們不能說,僅僅關(guān)注當(dāng)這個指針要被釋放的時(shí)候,誰將負(fù)責(zé)去做。使用資源句柄,這里用了標(biāo)準(zhǔn)庫中的auto_ptr,使需要為之負(fù)責(zé)的地方變得明確了。

#include<memory>
#include<iostream>
using namespace std;
struct S {
 S() { cout << "make an S"n"; }
 ~S() { cout << "destroy an S"n"; }
 S(const S&) { cout << "copy initialize an S"n"; }
 S& operator=(const S&) { cout << "copy assign an S"n"; }
};
S* f()
{
 return new S; // 誰該負(fù)責(zé)釋放這個S?
};
auto_ptr<S> g()
{
 return auto_ptr<S>(new S); // 顯式傳遞負(fù)責(zé)釋放這個S
}
int main()
{
 cout << "start main"n";
 S* p = f();
 cout << "after f() before g()"n";
 // S* q = g(); // 將被編譯器捕捉
 auto_ptr<S> q = g();
 cout << "exit main"n";
 // *p產(chǎn)生了內(nèi)存泄漏
 // *q被自動釋放
}

  在更一般的意義上考慮資源,而不僅僅是內(nèi)存。

如果在你的環(huán)境中不能系統(tǒng)地應(yīng)用這些技巧(例如,你必須使用別的地方的代碼,或者你的程序的另一部分簡直是原始人類(譯注:原文是Neanderthals,尼安德特人,舊石器時(shí)代廣泛分布在歐洲的猿人)寫的,如此等等),那么注意使用一個內(nèi)存泄漏檢測器作為開發(fā)過程的一部分,或者插入一個垃圾收集器(garbage collector)。

2.3淺談C/C++內(nèi)存泄漏及其檢測工具

  對于一個c/c++程序員來說,內(nèi)存泄漏是一個常見的也是令人頭疼的問題。已經(jīng)有許多技術(shù)被研究出來以應(yīng)對這個問題,比如Smart Pointer,Garbage Collection等。Smart Pointer技術(shù)比較成熟,STL中已經(jīng)包含支持Smart Pointer的class,但是它的使用似乎并不廣泛,而且它也不能解決所有的問題;Garbage Collection技術(shù)在Java中已經(jīng)比較成熟,但是在c/c++領(lǐng)域的發(fā)展并不順暢,雖然很早就有人思考在C++中也加入GC的支持,F(xiàn)實(shí)世界就是這樣的,作為一個c/c++程序員,內(nèi)存泄漏是你心中永遠(yuǎn)的痛。不過好在現(xiàn)在有許多工具能夠幫助我們驗(yàn)證內(nèi)存泄漏的存在,找出發(fā)生問題的代碼。

2.3.1 內(nèi)存泄漏的定義

一般我們常說的內(nèi)存泄漏是指堆內(nèi)存的泄漏。堆內(nèi)存是指程序從堆中分配的,大小任意的(內(nèi)存塊的大小可以在程序運(yùn)行期決定),使用完后必須顯示釋放的內(nèi)存。應(yīng)用程序一般使用malloc,realloc,new等函數(shù)從堆中分配到一塊內(nèi)存,使用完后,程序必須負(fù)責(zé)相應(yīng)的調(diào)用free或delete釋放該內(nèi)存塊,否則,這塊內(nèi)存就不能被再次使用,我們就說這塊內(nèi)存泄漏了。以下這段小程序演示了堆內(nèi)存發(fā)生泄漏的情形:

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

  當(dāng)函數(shù)GetStringFrom()返回零的時(shí)候,指針p指向的內(nèi)存就不會被釋放。這是一種常見的發(fā)生內(nèi)存泄漏的情形。程序在入口處分配內(nèi)存,在出口處釋放內(nèi)存,但是c函數(shù)可以在任何地方退出,所以一旦有某個出口處沒有釋放應(yīng)該釋放的內(nèi)存,就會發(fā)生內(nèi)存泄漏。

  廣義的說,內(nèi)存泄漏不僅僅包含堆內(nèi)存的泄漏,還包含系統(tǒng)資源的泄漏(resource leak),比如核心態(tài)HANDLE,GDI Object,SOCKET, Interface等,從根本上說這些由操作系統(tǒng)分配的對象也消耗內(nèi)存,如果這些對象發(fā)生泄漏最終也會導(dǎo)致內(nèi)存的泄漏。而且,某些對象消耗的是核心態(tài)內(nèi)存,這些對象嚴(yán)重泄漏時(shí)會導(dǎo)致整個操作系統(tǒng)不穩(wěn)定。所以相比之下,系統(tǒng)資源的泄漏比堆內(nèi)存的泄漏更為嚴(yán)重。

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;
}

  當(dāng)函數(shù)Something()返回非零的時(shí)候,程序在退出前沒有把pOldBmp選回pDC中,這會導(dǎo)致pOldBmp指向的HBITMAP對象發(fā)生泄漏。這個程序如果長時(shí)間的運(yùn)行,可能會導(dǎo)致整個系統(tǒng)花屏。這種問題在Win9x下比較容易暴露出來,因?yàn)閃in9x的GDI堆比Win2k或NT的要小很多。

2.3.2 內(nèi)存泄漏的發(fā)生方式

  以發(fā)生的方式來分類,內(nèi)存泄漏可以分為4類:

  1. 常發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼會被多次執(zhí)行到,每次被執(zhí)行的時(shí)候都會導(dǎo)致一塊內(nèi)存泄漏。比如例二,如果Something()函數(shù)一直返回True,那么pOldBmp指向的HBITMAP對象總是發(fā)生泄漏。

  2. 偶發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只有在某些特定環(huán)境或操作過程下才會發(fā)生。比如例二,如果Something()函數(shù)只有在特定環(huán)境下才返回True,那么pOldBmp指向的HBITMAP對象并不總是發(fā)生泄漏。常發(fā)性和偶發(fā)性是相對的。對于特定的環(huán)境,偶發(fā)性的也許就變成了常發(fā)性的。所以測試環(huán)境和測試方法對檢測內(nèi)存泄漏至關(guān)重要。

3. 一次性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只會被執(zhí)行一次,或者由于算法上的缺陷,導(dǎo)致總會有一塊僅且一塊內(nèi)存發(fā)生泄漏。比如,在類的構(gòu)造函數(shù)中分配內(nèi)存,在析構(gòu)函數(shù)中卻沒有釋放該內(nèi)存,但是因?yàn)檫@個類是一個Singleton,所以內(nèi)存泄漏只會發(fā)生一次。另一個例子:

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

  如果程序在結(jié)束的時(shí)候沒有釋放g_lpszFileName指向的字符串,那么,即使多次調(diào)用SetFileName(),總會有一塊內(nèi)存,而且僅有一塊內(nèi)存發(fā)生泄漏。

4. 隱式內(nèi)存泄漏。程序在運(yùn)行過程中不停的分配內(nèi)存,但是直到結(jié)束的時(shí)候才釋放內(nèi)存。嚴(yán)格的說這里并沒有發(fā)生內(nèi)存泄漏,因?yàn)樽罱K程序釋放了所有申請的內(nèi)存。但是對于一個服務(wù)器程序,需要運(yùn)行幾天,幾周甚至幾個月,不及時(shí)釋放內(nèi)存也可能導(dǎo)致最終耗盡系統(tǒng)的所有內(nèi)存。所以,我們稱這類內(nèi)存泄漏為隱式內(nèi)存泄漏。舉一個例子:

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;
};

  假設(shè)在Client從Server端斷開后,Server并沒有呼叫OnClientDisconnected()函數(shù),那么代表那次連接的Connection對象就不會被及時(shí)的刪除(在Server程序退出的時(shí)候,所有Connection對象會在ConnectionManager的析構(gòu)函數(shù)里被刪除)。當(dāng)不斷的有連接建立、斷開時(shí)隱式內(nèi)存泄漏就發(fā)生了。

從用戶使用程序的角度來看,內(nèi)存泄漏本身不會產(chǎn)生什么危害,作為一般的用戶,根本感覺不到內(nèi)存泄漏的存在。真正有危害的是內(nèi)存泄漏的堆積,這會最終消耗盡系統(tǒng)所有的內(nèi)存。從這個角度來說,一次性內(nèi)存泄漏并沒有什么危害,因?yàn)樗粫逊e,而隱式內(nèi)存泄漏危害性則非常大,因?yàn)檩^之于常發(fā)性和偶發(fā)性內(nèi)存泄漏它更難被檢測到。

2.3.3 檢測內(nèi)存泄漏

  檢測內(nèi)存泄漏的關(guān)鍵是要能截獲住對分配內(nèi)存和釋放內(nèi)存的函數(shù)的調(diào)用。截獲住這兩個函數(shù),我們就能跟蹤每一塊內(nèi)存的生命周期,比如,每當(dāng)成功的分配一塊內(nèi)存后,就把它的指針加入一個全局的list中;每當(dāng)釋放一塊內(nèi)存,再把它的指針從list中刪除。這樣,當(dāng)程序結(jié)束的時(shí)候,list中剩余的指針就是指向那些沒有被釋放的內(nèi)存。這里只是簡單的描述了檢測內(nèi)存泄漏的基本原理,詳細(xì)的算法可以參見Steve Maguire的<<Writing Solid Code>>。

  如果要檢測堆內(nèi)存的泄漏,那么需要截獲住malloc/realloc/free和new/delete就可以了(其實(shí)new/delete最終也是用malloc/free的,所以只要截獲前面一組即可)。對于其他的泄漏,可以采用類似的方法,截獲住相應(yīng)的分配和釋放函數(shù)。比如,要檢測BSTR的泄漏,就需要截獲SysAllocString/SysFreeString;要檢測HMENU的泄漏,就需要截獲CreateMenu/ DestroyMenu。(有的資源的分配函數(shù)有多個,釋放函數(shù)只有一個,比如,SysAllocStringLen也可以用來分配BSTR,這時(shí)就需要截獲多個分配函數(shù))

  在Windows平臺下,檢測內(nèi)存泄漏的工具常用的一般有三種,MS C-Runtime Library內(nèi)建的檢測功能;外掛式的檢測工具,諸如,Purify,BoundsChecker等;利用Windows NT自帶的Performance Monitor。這三種工具各有優(yōu)缺點(diǎn),MS C-Runtime Library雖然功能上較之外掛式的工具要弱,但是它是免費(fèi)的;Performance Monitor雖然無法標(biāo)示出發(fā)生問題的代碼,但是它能檢測出隱式的內(nèi)存泄漏的存在,這是其他兩類工具無能為力的地方。

  以下我們詳細(xì)討論這三種檢測工具:

2.3.3.1 VC下內(nèi)存泄漏的檢測方法

  用MFC開發(fā)的應(yīng)用程序,在DEBUG版模式下編譯后,都會自動加入內(nèi)存泄漏的檢測代碼。在程序結(jié)束后,如果發(fā)生了內(nèi)存泄漏,在Debug窗口中會顯示出所有發(fā)生泄漏的內(nèi)存塊的信息,以下兩行顯示了一塊被泄漏的內(nèi)存塊的信息:

E:"TestMemLeak"TestDlg.cpp(70) : {59} normal block at 0x00881710, 200 bytes long.

Data: <abcdefghijklmnop> 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70

  第一行顯示該內(nèi)存塊由TestDlg.cpp文件,第70行代碼分配,地址在0x00881710,大小為200字節(jié),{59}是指調(diào)用內(nèi)存分配函數(shù)的Request Order,關(guān)于它的詳細(xì)信息可以參見MSDN中_CrtSetBreakAlloc()的幫助。第二行顯示該內(nèi)存塊前16個字節(jié)的內(nèi)容,尖括號內(nèi)是以ASCII方式顯示,接著的是以16進(jìn)制方式顯示。

  一般大家都誤以為這些內(nèi)存泄漏的檢測功能是由MFC提供的,其實(shí)不然。MFC只是封裝和利用了MS C-Runtime Library的Debug Function。非MFC程序也可以利用MS C-Runtime Library的Debug Function加入內(nèi)存泄漏的檢測功能。MS C-Runtime Library在實(shí)現(xiàn)malloc/free,strdup等函數(shù)時(shí)已經(jīng)內(nèi)建了內(nèi)存泄漏的檢測功能。

注意觀察一下由MFC Application Wizard生成的項(xiàng)目,在每一個cpp文件的頭部都有這樣一段宏定義:

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

有了這樣的定義,在編譯DEBUG版時(shí),出現(xiàn)在這個cpp文件中的所有new都被替換成DEBUG_NEW了。那么DEBUG_NEW是什么呢?DEBUG_NEW也是一個宏,以下摘自afx.h,1632行

#define DEBUG_NEW new(THIS_FILE, __LINE__)

所以如果有這樣一行代碼:

char* p = new char[200];

經(jīng)過宏替換就變成了:

char* p = new( THIS_FILE, __LINE__)char[200];

根據(jù)C++的標(biāo)準(zhǔn),對于以上的new的使用方法,編譯器會去找這樣定義的operator new:

void* operator new(size_t, LPCSTR, int)

我們在afxmem.cpp 63行找到了一個這樣的operator new 的實(shí)現(xiàn)

void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine)
{
 return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine);
}
void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)
{
 …
 pResult = _malloc_dbg(nSize, nType, lpszFileName, nLine);
 if (pResult != NULL)
  return pResult;
 …
}

  第二個operator new函數(shù)比較長,為了簡單期間,我只摘錄了部分。很顯然最后的內(nèi)存分配還是通過_malloc_dbg函數(shù)實(shí)現(xiàn)的,這個函數(shù)屬于MS C-Runtime Library 的Debug Function。這個函數(shù)不但要求傳入內(nèi)存的大小,另外還有文件名和行號兩個參數(shù)。文件名和行號就是用來記錄此次分配是由哪一段代碼造成的。如果這塊內(nèi)存在程序結(jié)束之前沒有被釋放,那么這些信息就會輸出到Debug窗口里。

  這里順便提一下THIS_FILE,__FILE和__LINE__。__FILE__和__LINE__都是編譯器定義的宏。當(dāng)碰到__FILE__時(shí),編譯器會把__FILE__替換成一個字符串,這個字符串就是當(dāng)前在編譯的文件的路徑名。當(dāng)碰到__LINE__時(shí),編譯器會把__LINE__替換成一個數(shù)字,這個數(shù)字就是當(dāng)前這行代碼的行號。在DEBUG_NEW的定義中沒有直接使用__FILE__,而是用了THIS_FILE,其目的是為了減小目標(biāo)文件的大小。假設(shè)在某個cpp文件中有100處使用了new,如果直接使用__FILE__,那編譯器會產(chǎn)生100個常量字符串,這100個字符串都是飧?/SPAN>cpp文件的路徑名,顯然十分冗余。如果使用THIS_FILE,編譯器只會產(chǎn)生一個常量字符串,那100處new的調(diào)用使用的都是指向常量字符串的指針。

  再次觀察一下由MFC Application Wizard生成的項(xiàng)目,我們會發(fā)現(xiàn)在cpp文件中只對new做了映射,如果你在程序中直接使用malloc函數(shù)分配內(nèi)存,調(diào)用malloc的文件名和行號是不會被記錄下來的。如果這塊內(nèi)存發(fā)生了泄漏,MS C-Runtime Library仍然能檢測到,但是當(dāng)輸出這塊內(nèi)存塊的信息,不會包含分配它的的文件名和行號。

要在非MFC程序中打開內(nèi)存泄漏的檢測功能非常容易,你只要在程序的入口處加入以下幾行代碼:

int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );
tmpFlag |= _CRTDBG_LEAK_CHECK_DF;
_CrtSetDbgFlag( tmpFlag );

  這樣,在程序結(jié)束的時(shí)候,也就是winmain,main或dllmain函數(shù)返回之后,如果還有內(nèi)存塊沒有釋放,它們的信息會被打印到Debug窗口里。

如果你試著創(chuàng)建了一個非MFC應(yīng)用程序,而且在程序的入口處加入了以上代碼,并且故意在程序中不釋放某些內(nèi)存塊,你會在Debug窗口里看到以下的信息:

{47} normal block at 0x00C91C90, 200 bytes long.
Data: < > 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

  內(nèi)存泄漏的確檢測到了,但是和上面MFC程序的例子相比,缺少了文件名和行號。對于一個比較大的程序,沒有這些信息,解決問題將變得十分困難。

  為了能夠知道泄漏的內(nèi)存塊是在哪里分配的,你需要實(shí)現(xiàn)類似MFC的映射功能,把new,maolloc等函數(shù)映射到_malloc_dbg函數(shù)上。這里我不再贅述,你可以參考MFC的源代碼。

  由于Debug Function實(shí)現(xiàn)在MS C-RuntimeLibrary中,所以它只能檢測到堆內(nèi)存的泄漏,而且只限于malloc,realloc或strdup等分配的內(nèi)存,而那些系統(tǒng)資源,比如HANDLE,GDI Object,或是不通過C-Runtime Library分配的內(nèi)存,比如VARIANT,BSTR的泄漏,它是無法檢測到的,這是這種檢測法的一個重大的局限性。另外,為了能記錄內(nèi)存塊是在哪里分配的,源代碼必須相應(yīng)的配合,這在調(diào)試一些老的程序非常麻煩,畢竟修改源代碼不是一件省心的事,這是這種檢測法的另一個局限性。

對于開發(fā)一個大型的程序,MS C-Runtime Library提供的檢測功能是遠(yuǎn)遠(yuǎn)不夠的。接下來我們就看看外掛式的檢測工具。我用的比較多的是BoundsChecker,一則因?yàn)樗墓δ鼙容^全面,更重要的是它的穩(wěn)定性。這類工具如果不穩(wěn)定,反而會忙里添亂。到底是出自鼎鼎大名的NuMega,我用下來基本上沒有什么大問題。

2.3.3.2 使用BoundsChecker檢測內(nèi)存泄漏

  BoundsChecker采用一種被稱為 Code Injection的技術(shù),來截獲對分配內(nèi)存和釋放內(nèi)存的函數(shù)的調(diào)用。簡單地說,當(dāng)你的程序開始運(yùn)行時(shí),BoundsChecker的DLL被自動載入進(jìn)程的地址空間(這可以通過system-level的Hook實(shí)現(xiàn)),然后它會修改進(jìn)程中對內(nèi)存分配和釋放的函數(shù)調(diào)用,讓這些調(diào)用首先轉(zhuǎn)入它的代碼,然后再執(zhí)行原來的代碼。BoundsChecker在做這些動作的時(shí),無須修改被調(diào)試程序的源代碼或工程配置文件,這使得使用它非常的簡便、直接。

  這里我們以malloc函數(shù)為例,截獲其他的函數(shù)方法與此類似。

  需要被截獲的函數(shù)可能在DLL中,也可能在程序的代碼里。比如,如果靜態(tài)連結(jié)C-Runtime Library,那么malloc函數(shù)的代碼會被連結(jié)到程序里。為了截獲住對這類函數(shù)的調(diào)用,BoundsChecker會動態(tài)修改這些函數(shù)的指令。

以下兩段匯編代碼,一段沒有BoundsChecker介入,另一段則有BoundsChecker的介入:

126: _CRTIMP void * __cdecl malloc (
127: size_t nSize
128: )
129: {
00403C10 push ebp
00403C11 mov ebp,esp
130: return _nh_malloc_dbg(nSize, _newmode, _NORMAL_BLOCK, NULL, 0);
00403C13 push 0
00403C15 push 0
00403C17 push 1
00403C19 mov eax,[__newmode (0042376c)]
00403C1E push eax
00403C1F mov ecx,dword ptr [nSize]
00403C22 push ecx
00403C23 call _nh_malloc_dbg (00403c80)
00403C28 add esp,14h
131: }

以下這一段代碼有BoundsChecker介入:

126: _CRTIMP void * __cdecl malloc (
127: size_t nSize
128: )
129: {
00403C10 jmp 01F41EC8
00403C15 push 0
00403C17 push 1
00403C19 mov eax,[__newmode (0042376c)]
00403C1E push eax
00403C1F mov ecx,dword ptr [nSize]
00403C22 push ecx
00403C23 call _nh_malloc_dbg (00403c80)
00403C28 add esp,14h
131: }

  當(dāng)BoundsChecker介入后,函數(shù)malloc的前三條匯編指令被替換成一條jmp指令,原來的三條指令被搬到地址01F41EC8處了。當(dāng)程序進(jìn)入malloc后先jmp到01F41EC8,執(zhí)行原來的三條指令,然后就是BoundsChecker的天下了。大致上它會先記錄函數(shù)的返回地址(函數(shù)的返回地址在stack上,所以很容易修改),然后把返回地址指向?qū)儆贐oundsChecker的代碼,接著跳到malloc函數(shù)原來的指令,也就是在00403c15的地方。當(dāng)malloc函數(shù)結(jié)束的時(shí)候,由于返回地址被修改,它會返回到BoundsChecker的代碼中,此時(shí)BoundsChecker會記錄由malloc分配的內(nèi)存的指針,然后再跳轉(zhuǎn)到到原來的返回地址去。

  如果內(nèi)存分配/釋放函數(shù)在DLL中,BoundsChecker則采用另一種方法來截獲對這些函數(shù)的調(diào)用。BoundsChecker通過修改程序的DLL Import Table讓table中的函數(shù)地址指向自己的地址,以達(dá)到截獲的目的。

截獲住這些分配和釋放函數(shù),BoundsChecker就能記錄被分配的內(nèi)存或資源的生命周期。接下來的問題是如何與源代碼相關(guān),也就是說當(dāng)BoundsChecker檢測到內(nèi)存泄漏,它如何報(bào)告這塊內(nèi)存塊是哪段代碼分配的。答案是調(diào)試信息(Debug Information)。當(dāng)我們編譯一個Debug版的程序時(shí),編譯器會把源代碼和二進(jìn)制代碼之間的對應(yīng)關(guān)系記錄下來,放到一個單獨(dú)的文件里(.pdb)或者直接連結(jié)進(jìn)目標(biāo)程序,通過直接讀取調(diào)試信息就能得到分配某塊內(nèi)存的源代碼在哪個文件,哪一行上。使用Code Injection和Debug Information,使BoundsChecker不但能記錄呼叫分配函數(shù)的源代碼的位置,而且還能記錄分配時(shí)的Call Stack,以及Call Stack上的函數(shù)的源代碼位置。這在使用像MFC這樣的類庫時(shí)非常有用,以下我用一個例子來說明:

void ShowXItemMenu()
{
 …
 CMenu menu;
 menu.CreatePopupMenu();
 //add menu items.
 menu.TrackPropupMenu();
 …
}
void ShowYItemMenu( )
{
 …
 CMenu menu;
 menu.CreatePopupMenu();
 //add menu items.
 menu.TrackPropupMenu();
 menu.Detach();//this will cause HMENU leak
 …
}
BOOL CMenu::CreatePopupMenu()
{
 …
 hMenu = CreatePopupMenu();
 …
}

當(dāng)調(diào)用ShowYItemMenu()時(shí),我們故意造成HMENU的泄漏。但是,對于BoundsChecker來說被泄漏的HMENU是在class CMenu::CreatePopupMenu()中分配的。假設(shè)的你的程序有許多地方使用了CMenu的CreatePopupMenu()函數(shù),如CMenu::CreatePopupMenu()造成的,你依然無法確認(rèn)問題的根結(jié)到底在哪里,在ShowXItemMenu()中還是在ShowYItemMenu()中,或者還有其它的地方也使用了CreatePopupMenu()?有了Call Stack的信息,問題就容易了。BoundsChecker會如下報(bào)告泄漏的HMENU的信息:

Function
File
Line
CMenu::CreatePopupMenu
E:"8168"vc98"mfc"mfc"include"afxwin1.inl
1009
ShowYItemMenu
E:"testmemleak"mytest.cpp
100

  這里省略了其他的函數(shù)調(diào)用

  如此,我們很容易找到發(fā)生問題的函數(shù)是ShowYItemMenu()。當(dāng)使用MFC之類的類庫編程時(shí),大部分的API調(diào)用都被封裝在類庫的class里,有了Call Stack信息,我們就可以非常容易的追蹤到真正發(fā)生泄漏的代碼。

  記錄Call Stack信息會使程序的運(yùn)行變得非常慢,因此默認(rèn)情況下BoundsChecker不會記錄Call Stack信息?梢园凑找韵碌牟襟E打開記錄Call Stack信息的選項(xiàng)開關(guān):

  1. 打開菜單:BoundsChecker|Setting…

  2. 在Error Detection頁中,在Error Detection Scheme的List中選擇Custom

  3. 在Category的Combox中選擇 Pointer and leak error check

  4. 鉤上Report Call Stack復(fù)選框

  5. 點(diǎn)擊Ok

  基于Code Injection,BoundsChecker還提供了API Parameter的校驗(yàn)功能,memory over run等功能。這些功能對于程序的開發(fā)都非常有益。由于這些內(nèi)容不屬于本文的主題,所以不在此詳述了。

盡管BoundsChecker的功能如此強(qiáng)大,但是面對隱式內(nèi)存泄漏仍然顯得蒼白無力。所以接下來我們看看如何用Performance Monitor檢測內(nèi)存泄漏。

2.3.3.3 使用Performance Monitor檢測內(nèi)存泄漏

  NT的內(nèi)核在設(shè)計(jì)過程中已經(jīng)加入了系統(tǒng)監(jiān)視功能,比如CPU的使用率,內(nèi)存的使用情況,I/O操作的頻繁度等都作為一個個Counter,應(yīng)用程序可以通過讀取這些Counter了解整個系統(tǒng)的或者某個進(jìn)程的運(yùn)行狀況。Performance Monitor就是這樣一個應(yīng)用程序。

  為了檢測內(nèi)存泄漏,我們一般可以監(jiān)視Process對象的Handle Count,Virutal Bytes 和Working Set三個Counter。Handle Count記錄了進(jìn)程當(dāng)前打開的HANDLE的個數(shù),監(jiān)視這個Counter有助于我們發(fā)現(xiàn)程序是否有Handle泄漏;Virtual Bytes記錄了該進(jìn)程當(dāng)前在虛地址空間上使用的虛擬內(nèi)存的大小,NT的內(nèi)存分配采用了兩步走的方法,首先,在虛地址空間上保留一段空間,這時(shí)操作系統(tǒng)并沒有分配物理內(nèi)存,只是保留了一段地址。然后,再提交這段空間,這時(shí)操作系統(tǒng)才會分配物理內(nèi)存。所以,Virtual Bytes一般總大于程序的Working Set。監(jiān)視Virutal Bytes可以幫助我們發(fā)現(xiàn)一些系統(tǒng)底層的問題; Working Set記錄了操作系統(tǒng)為進(jìn)程已提交的內(nèi)存的總量,這個值和程序申請的內(nèi)存總量存在密切的關(guān)系,如果程序存在內(nèi)存的泄漏這個值會持續(xù)增加,但是Virtual Bytes卻是跳躍式增加的。

  監(jiān)視這些Counter可以讓我們了解進(jìn)程使用內(nèi)存的情況,如果發(fā)生了泄漏,即使是隱式內(nèi)存泄漏,這些Counter的值也會持續(xù)增加。但是,我們知道有問題卻不知道哪里有問題,所以一般使用Performance Monitor來驗(yàn)證是否有內(nèi)存泄漏,而使用BoundsChecker來找到和解決。

  當(dāng)Performance Monitor顯示有內(nèi)存泄漏,而BoundsChecker卻無法檢測到,這時(shí)有兩種可能:第一種,發(fā)生了偶發(fā)性內(nèi)存泄漏。這時(shí)你要確保使用Performance Monitor和使用BoundsChecker時(shí),程序的運(yùn)行環(huán)境和操作方法是一致的。第二種,發(fā)生了隱式的內(nèi)存泄漏。這時(shí)你要重新審查程序的設(shè)計(jì),然后仔細(xì)研究Performance Monitor記錄的Counter的值的變化圖,分析其中的變化和程序運(yùn)行邏輯的關(guān)系,找到一些可能的原因。這是一個痛苦的過程,充滿了假設(shè)、猜想、驗(yàn)證、失敗,但這也是一個積累經(jīng)驗(yàn)的絕好機(jī)會。

    相關(guān)評論

    閱讀本文后您有什么感想? 已有人給出評價(jià)!

    • 8 喜歡喜歡
    • 3 頂
    • 1 難過難過
    • 5 囧
    • 3 圍觀圍觀
    • 2 無聊無聊

    熱門評論

    最新評論

    發(fā)表評論 查看所有評論(0)

    昵稱:
    表情: 高興 可 汗 我不要 害羞 好 下下下 送花 屎 親親
    字?jǐn)?shù): 0/500 (您的評論需要經(jīng)過審核才能顯示)
    推薦文章

    沒有數(shù)據(jù)

      沒有數(shù)據(jù)
    最新文章
      沒有數(shù)據(jù)