常見記憶體洩露及解決方案
常見記憶體洩露及解決方案-選自ood啟示錄
new/delete, array new/arrray delete比對
case 1:
在類的構造函數與析構函數中沒有比對地調用 new/delete!
解決方法:檢查構造函數,在出現new的情況下,按相反的順序在析構函數中比對添加delete!
這裡有兩個意思:
1〉new與delete比對,array new/array delete比對;
2〉出現在前面的new要比出現在後面的new後比對各自的delete;
比如:
構造函數:
m_x = new int[10];
...
m_y = new CString;
則析構函數:
delete m_y;
...
delete []m_x; // 對于基本資料類型,用delete也可以,但為了統一,還 // 是用array delete
case 2:
沒有正确地清除嵌套的對象指針
也就是說,某個對象以引用語義(指針)了包含另一個對象,而不是以值的方式。
解決辦法:
1〉養成好的成對編碼習慣:
在外部函數配置設定的堆記憶體,不要在調用函數裡面釋放,而在外部函數内釋放;
2〉盡量在構造函數裡面配置設定記憶體,并注意不要犯case 1錯誤;
3〉在基類/繼承類各管各的記憶體;(具體解析見下面的case 8)
for example:
#include <iostream>
#include <string>
// Melon : 甜瓜,西瓜;
class Melon
{
public:
Melon(char * var);
~Melon();
void print(void);
protected:
private:
char * m_variety;
};
Melon::Melon(char * var)
{
m_variety = new char[strlen(var) + 1];
strcpy(m_variety, var);
}
Melon::~Melon()
{
delete m_variety;
}
void Melon::print()
{
std::cout << "I'm a " << m_variety << "Melon/n";
}
// Meal : 進餐;
class Meal
{
public:
Meal(char * var, char * res);
~Meal();
void print(void);
protected:
private:
char * m_reastaurant; // 飯店
Melon * m_pMelon;
// 方法2
// Melon m_Melon;
};
Meal::Meal(char * var, char * res)
// 方法2:改引用為值包含;
// : m_Melon(var)
{
m_pMelon = new Melon(var);
m_reastaurant = new char[strlen(res) + 1];
strcpy(m_reastaurant, res);
}
Meal::~Meal()
{
delete m_reastaurant;
delete m_pMelon; // 修改方法1;
}
void Meal::print()
{
std::cout << "I'am a Meal owned by ";
m_pMelon->print();
// 方法2
//m_Melon.print();
}
int main(...)
{
cout << "case 2:/n";
Meal m1("Honeydew", "Four Seasons"); // 蜜汁,四季飯店;
Meal m2("Cantaloup", "Brook Manor Pub"); // 香瓜, 小溪家園酒吧;
m1.print();
m2.print();
return 0;
}
case 3:在釋放對象數組時,沒有使用delete [];
1>對于單個對象,單個基本類型(如int,double等)的變量,我們肯定采用delete,不會出錯;
2>對于基本類型數組,由于不需要大小參數,因而,采用delete或array delete(delete []),均可以,如上例中,我便直接采用了delete m_variety,建議為了統一,采用delete []m_variety;
3>對于自定義的對象所組成的對象數組,則一定要采用array delete,這樣編譯器才會在釋放記憶體前調用每個對象的析構函數,并調用
free釋放對象數組空間;
for example:
#include <iostream>
#include <string>
class Point
{
public:
Point(int x = 0, int y = 0, char *col = "Red");
~Point();
protected:
private:
int m_x;
int m_y;
char *m_color;
};
Point::Point(int x, int y, char *col)
: m_x(x), m_y(y)
{
m_color = new char[strlen(col) + 1];
strcpy(m_color, col);
}
Point::~Point()
{
delete []m_color;
std::cout << "In the deconstuctor of Point!/n";
}
int main(int argc, char *argv[])
{
cout << "case 3:/n";
Point *p = new Point[5];
delete p;
// 正确方法:
// delete []p;
return 0;
}
case 4:
指向由指向對象的指針構成的數組不等同于與對象數組。
也就是說,數組的基本類型是指向對象的指針,此時,是用delete 還是delete [](array delete),并不重要,關鍵是指針并沒有析構函數,必須使用者自己調用delete語句.
for example:
// Point類和case 3一樣;
int main(int argc, char *argv[])
{
cout << "case 4:/n";
Point **pPtrAry = new Point*[10];
// 循環為每個指針配置設定一個Point對象;
int i = 0;
for (; i < 10; ++i)
{
pPtrAry[i] = new Point(i, i, "Green");
}
// 下面語句并沒有釋放10個Point對象,釋放的隻是他們的指針所組成的數組
// 占用的10*sizeof(Point*) 空間,造成了記憶體洩露
// (180 = 10*sizeof(Point) + 10* 6; (6= sizeof("Green")))
// delete []pPtrAry;
// 正确的方法:
for (i = 0; i < 10; ++i)
{
delete pPtrAry[i];
}
delete []pPtrAry; // 或者delete pPtrAry;
return 0;
}
case 5:
缺少拷貝構造函數
這沒什麼好說的,主要是解決編譯器預設添加的拷貝構造函數不足!預設的拷貝構造函數采用位拷貝,
如下代碼:
Point x;
Point y(x);
這樣會導緻兩個Point對象 x,y的 m_color指向同一個"Red"字元串;
當某個對象釋放後,另外一個對象的 m_color變成懸空指針,進而導緻程式異常;
解決方法:
編寫自己的拷貝構造函數;
對于Point類,編寫如下:
Point::Point(const Point& y)
: m_x(y.m_x), m_y(y.m_y)
{
m_color = new char[strlen(y.m_color) + 1];
::strcpy(m_color, y.m_color);
}
case 6:
缺少重載指派運算符,理由和上面一樣!
需要注意其實作的細節差別:
1> 拷貝構造函數編譯器會自動阻止自己構造自己,比如:
Point x(x); // 出錯;
但是,指派操作不會;
Point x = x; // 編譯期不會出錯,但運作期會出錯!
上面的錯誤原因在于,編譯器雖然為x配置設定了記憶體,但調用拷貝構造函數時,m_color還沒初始化;
建議,盡量不要用這種方法初始化,以便将錯誤在編譯期間顯示出來;
2> 指派運算符必須差別是否自身指派;
3> 在指派前必須釋放原有new操作配置設定的資源(當然,其他檔案等資源也要釋放,這裡隻讨論記憶體溢出,略過不提!)
最後實作如下:
const Point& Point::operator =(const Point& rhs)
{
// 防止自己複制自己
// 這裡采用簡單的位址比較法,比較安全的是采用COM相同的方法編一個唯一編碼生成函數;
if (this != &rhs)
{
m_x = rhs.m_x;
m_y = rhs.m_y;
// 删除原有資源空間;
// 必須牢記;
delete m_color;
m_color = new char[strlen(rhs.m_color) + 1];
strcpy(m_color, rhs.m_color);
}
return *this;
}
注意,最左邊的const聲明可以不要,要得話是為了阻止如下語句:
(x = y) = z;
但由于基本類型也支援,為了與基本類型一緻,可以去掉const限制;
case 7:
關于nonmodifying運算符重載的常見錯誤;
所謂nonmodifying運算符就是不改變操作數的值,并且傳回結果類型與操作數一樣;比如數學運算符;
而關系運算符則不滿足,因為其結果為bool型;
指派運算符也不是(=, += ,<<=等等);
主要原因是,大家可能将結果儲存到一個局部變量裡面,而傳回結果為了效率采用了引用(&);
解決方法:
1> 利用static, 将臨時變量作為類的内部存儲單元;
不足,不适合嵌套使用和多線程,比如 w = x+y+z;
for example:
// case 7,解決方法1:static
const Point& Point::operator +(const Point& rhs) const
{
static Point temp;
temp.m_x = this->m_x + rhs.m_x;
temp.m_y = this->m_y + rhs.m_y;
// 釋放前一個值的資源;
delete temp.m_color;
temp.m_color = new char[strlen(this->m_color) + strlen(rhs.m_color) + 1];
sprintf(temp.m_color, "%s%s", this->m_color, rhs.m_color);
return temp;
}
注意,這裡為了簡單,并沒有考慮類型轉換,實際中二進制運算符通常采用友元函數形式實作,具體判斷方法請看Effective c++ Item 19;
2> 改引用語義為值語義;(最好辦法,但會降低效率)
注意,有人也許會用指針方法,比如如下:
Point *temp = new Point;
...
return (*temp);
這樣會産生一個無名對象,并且位于堆上,進而造成記憶體洩露;
const Point Point::operator +(const Point& rhs) const
{
Point temp;
temp.m_x = this->m_x + rhs.m_x;
temp.m_y = this->m_y + rhs.m_y;
// 釋放前一個值的資源;
delete temp.m_color;
temp.m_color = new char[strlen(this->m_color) + strlen(rhs.m_color) + 1];
sprintf(temp.m_color, "%s%s", this->m_color, rhs.m_color);
return temp;
}
case 8:
沒用将基類的析構函數定義成虛函數;
解決方法:
将基類的析構函數定義為虛函數;
這種情況主要出現在下面情況:
基類指針指向派生類;
for example:
Apple is a kind of fruit, and banana also is;
so someone write such codes:
Fruit *basket[20];
for (int i = 0; i < 10; ++i)
{
basket[i] = new Apple;
// 輸入水果資訊;
...
}
for (; i < 20; ++i)
{
basket[i] = new Banana;
// 輸入香蕉資訊;
...
}
// 如果Fruitde析構函數不是虛函數,則會造成記憶體溢出(假設Apple或Banana的構造函數中有new語句,否則不會)
for (i = 0; i < 20; ++i)
{
delete basket[i];
}
具體實作略!
注意:
1> 該錯誤具有隐蔽性,當所有派生類均沒有新的new操作時,不會産生記憶體溢出;因而,最好遵循以下原則:
将基類構造函數定義為非虛函數,則該類不允許擴充;
2> 如果不是虛函數,則釋放基類指針不會調用派生類的析構函數,即使它指向一個派生類對象;
3> 不管是不是虛函數,釋放派生類指針均會調用基類的析構函數,且調用順序不變;
4> 如果為虛函數,則釋放基類指針且該指針指向一個派生類,則會先調用派生類的析構函數,再調用基内的析構函數!
轉載自:http://hi.baidu.com/zhujian0622/blog/item/8ccf46d7d5986adca044dfd1.html