C++中避免記憶體洩露常見的解決方式
常見記憶體洩露及解決方式-選自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> 假設為虛擬函式,則釋放基類指標且該指標指向一個派生類,則會先呼叫派生類的解構函式,再呼叫基內的解構函式!