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> 如果爲虛函數,則釋放基類指針且該指針指向一個派生類,則會先調用派生類的析構函數,再調用基內的析構函數!

轉載自:http://hi.baidu.com/zhujian0622/blog/item/8ccf46d7d5986adca044dfd1.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章