常見內存泄露及解決方案-選自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