關注公眾號【高性能架構探索】,第一時間獲取干貨;回復【pdf】,免費獲取計算機經典書籍
本文節選自文章:
記憶體泄漏-原因、避免以及定位
本文總結常見記憶體泄漏的幾種方式,留意到這幾點,可以避免95+%以上的記憶體泄漏
未釋放
這種是很常見的,比如下面的代碼:
int fun() {
char * pBuffer = malloc(sizeof(char));
/* Do some work */
return 0;
}
上面代碼是非常常見的記憶體泄漏場景(也可以使用new來進行分配),我們申請了一塊記憶體,但是在fun函式結束時候沒有呼叫free函式進行記憶體釋放,
在C++開發中,還有一種記憶體泄漏,如下:
class Obj {
public:
Obj(int size) {
buffer_ = new char;
}
~Obj(){}
private:
char *buffer_;
};
int fun() {
Object obj;
// do sth
return 0;
}
上面這段代碼中,解構式沒有釋放成員變數buffer_指向的記憶體,所以在撰寫解構式的時候,一定要仔細分析成員變數有沒有申請動態記憶體,如果有,則需要手動釋放,我們重新撰寫了解構式,如下:
~Object() {
delete buffer_;
}
在C/C++中,對于普通函式,如果申請了堆資源,請跟進代碼的具體場景呼叫free/delete進行資源釋放;對于class,如果申請了堆資源,則需要在對應的解構式中呼叫free/delete進行資源釋放,
未匹配
在C++中,我們經常使用new運算子來進行記憶體分配,其內部主要做了兩件事:
- 通過operator new從堆上申請記憶體(glibc下,operator new底層呼叫的是malloc)
- 呼叫建構式(如果操作物件是一個class的話)
對應的,使用delete運算子來釋放記憶體,其順序正好與new相反:
- 呼叫物件的解構式(如果操作物件是一個class的話)
- 通過operator delete釋放記憶體
void* operator new(std::size_t size) {
void* p = malloc(size);
if (p == nullptr) {
throw("new failed to allocate %zu bytes", size);
}
return p;
}
void* operator new[](std::size_t size) {
void* p = malloc(size);
if (p == nullptr) {
throw("new[] failed to allocate %zu bytes", size);
}
return p;
}
void operator delete(void* ptr) throw() {
free(ptr);
}
void operator delete[](void* ptr) throw() {
free(ptr);
}
為了加深多這塊的理解,我們舉個例子:
class Test {
public:
Test() {
std::cout << "in Test" << std::endl;
}
// other
~Test() {
std::cout << "in ~Test" << std::endl;
}
};
int main() {
Test *t = new Test;
// do sth
delete t;
return 0;
}
在上述main函式中,我們使用new 運算子創建一個Test類指標
- 通過operator new申請記憶體(底層malloc實作)
- 通過placement new在上述申請的記憶體塊上呼叫建構式
- 呼叫ptr->~Test()釋放Test物件的成員變數
- 呼叫operator delete釋放記憶體
上述程序,可以理解為如下:
// new
void *ptr = malloc(sizeof(Test));
t = new(ptr)Test
// delete
ptr->~Test();
free(ptr);
好了,上述內容,我們簡單的講解了C++中new和delete運算子的基本實作以及邏輯,那么,我們就簡單總結下下產生記憶體泄漏的幾種型別,
new 和 free
仍然以上面的Test物件為例,代碼如下:
Test *t = new Test;
free(t)
此處會產生記憶體泄漏,在上面,我們已經分析過,new運算子會先通過operator new分配一塊記憶體,然后在該塊記憶體上呼叫placement new即呼叫Test的建構式,而在上述代碼中,只是通過free函式釋放了記憶體,但是沒有呼叫Test的解構式以釋放Test的成員變數,從而引起記憶體泄漏,
new[] 和 delete
int main() {
Test *t = new Test [10];
// do sth
delete t;
return 0;
}
在上述代碼中,我們通過new創建了一個Test型別的陣列,然后通delete運算子洗掉該陣列,編譯并執行,輸出如下:
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in ~Test
從上面輸出結果可以看出,呼叫了10次建構式,但是只呼叫了一次解構式,所以引起了記憶體泄漏,這是因為呼叫delete t釋放了通過operator new[]申請的記憶體,即malloc申請的記憶體塊,且只呼叫了t[0]物件的解構式,t[1…9]物件的解構式并沒有被呼叫,
虛析構
記得08年面谷歌的時候,有一道題,面試官問,std::string能否被繼承,為什么?
當時沒回答上來,后來過了沒多久,進行面試復盤的時候,偶然看到繼承需要父類解構式為virtual,才恍然大悟,原來考察點在這塊,
下面我們看下std::string的解構式定義:
~basic_string() {
_M_rep()->_M_dispose(this->get_allocator());
}
這塊需要特別說明下,std::basic_string是一個模板,而std::string是該模板的一個特化,即std::basic_string
typedef std::basic_string<char> string;
現在我們可以給出這個問題的答案:不能,因為std::string的解構式不為virtual,這樣會引起記憶體泄漏,
仍然以一個例子來進行證明,
class Base {
public:
Base(){
buffer_ = new char[10];
}
~Base() {
std::cout << "in Base::~Base" << std::endl;
delete []buffer_;
}
private:
char *buffer_;
};
class Derived : public Base {
public:
Derived(){}
~Derived() {
std::cout << "int Derived::~Derived" << std::endl;
}
};
int main() {
Base *base = new Derived;
delete base;
return 0;
}
上面代碼輸出如下:
in Base::~Base
可見,上述代碼并沒有呼叫派生類Derived的解構式,如果派生類中在堆上申請了資源,那么就會產生記憶體泄漏,
為了避免因為繼承導致的記憶體泄漏,我們需要將父類的解構式宣告為virtual,代碼如下(只列了部分修改代碼,其他不變):
~Base() {
std::cout << "in Base::~Base" << std::endl;
delete []buffer_;
}
然后重新執行代碼,輸出結果如下:
int Derived::~Derived
in Base::~Base
借助此文,我們再次總結下存在繼承情況下,建構式和解構式的呼叫順序,
派生類物件在創建時建構式呼叫順序:
- 呼叫父類的建構式
- 呼叫父類成員變數的建構式
- 呼叫派生類本身的建構式
派生類物件在析構時的解構式呼叫順序:
- 執行派生類自身的解構式
- 執行派生類成員變數的解構式
- 執行父類的解構式
為了避免存在繼承關系時候的記憶體泄漏,請遵守一條規則:無論派生類有沒有申請堆上的資源,請將父類的解構式宣告為virtual,
回圈參考
在C++開發中,為了盡可能的避免記憶體泄漏,自C++11起引入了smart pointer,常見的有shared_ptr、weak_ptr以及unique_ptr等(auto_ptr已經被廢棄),其中weak_ptr是為了解決回圈參考而存在,其往往與shared_ptr結合使用,
下面,我們看一段代碼:
class Controller {
public:
Controller() = default;
~Controller() {
std::cout << "in ~Controller" << std::endl;
}
class SubController {
public:
SubController() = default;
~SubController() {
std::cout << "in ~SubController" << std::endl;
}
std::shared_ptr<Controller> controller_;
};
std::shared_ptr<SubController> sub_controller_;
};
int main() {
auto controller = std::make_shared<Controller>();
auto sub_controller = std::make_shared<Controller::SubController>();
controller->sub_controller_ = sub_controller;
sub_controller->controller_ = controller;
return 0;
}
編譯并執行上述代碼,發現并沒有呼叫Controller和SubController的解構式,我們嘗試著列印下參考計數,代碼如下:
int main() {
auto controller = std::make_shared<Controller>();
auto sub_controller = std::make_shared<Controller::SubController>();
controller->sub_controller_ = sub_controller;
sub_controller->controller_ = controller;
std::cout << "controller use_count: " << controller.use_count() << std::endl;
std::cout << "sub_controller use_count: " << sub_controller.use_count() << std::endl;
return 0;
}
編譯并執行之后,輸出如下:
controller use_count: 2
sub_controller use_count: 2
通過上面輸出可以發現,因為參考計數都是2,所以在main函式結束的時候,不會呼叫controller和sub_controller的解構式,所以就出現了記憶體泄漏,
上面產生記憶體泄漏的原因,就是我們常說的回圈參考,

為了解決std::shared_ptr回圈參考導致的記憶體泄漏,我們可以使用std::weak_ptr來單面去除上圖中的回圈,
class Controller {
public:
Controller() = default;
~Controller() {
std::cout << "in ~Controller" << std::endl;
}
class SubController {
public:
SubController() = default;
~SubController() {
std::cout << "in ~SubController" << std::endl;
}
std::weak_ptr<Controller> controller_;
};
std::shared_ptr<SubController> sub_controller_;
};
在上述代碼中,我們將SubController類中controller_的型別從std::shared_ptr變成std::weak_ptr,重新編譯執行,結果如下:
controller use_count: 1
sub_controller use_count: 2
in ~Controller
in ~SubController
從上面結果可以看出,controller和sub_controller均以釋放,所以回圈參考引起的記憶體泄漏問題,也得以解決,

可能有人會問,使用std::shared_ptr可以直接訪問對應的成員函式,如果是std::weak_ptr的話,怎么訪問呢?我們可以使用下面的方式:
std::shared_ptr controller = controller_.lock();
即在子類SubController中,如果要使用controller呼叫其對應的函式,就可以使用上面的方式,
關注公眾號【高性能架構探索】,第一時間獲取干貨;回復【pdf】,免費獲取計算機經典書籍
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/413901.html
標籤:其他
