天天看點

常見記憶體洩露及解決方案

常見記憶體洩露及解決方案

常見記憶體洩露及解決方案-選自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