前言
由於 C++ 語言沒有自動內存回收機制,程序員每次 new 出來的內存都要手動 delete。程序員忘記 delete,流程太複雜,最終導致沒有 delete,異常導致程序過早退出,沒有執行 delete 的情況並不罕見。
用智能指針便可以有效緩解這類問題,本文主要講解std::auto_ptr智能指針。
對於編譯器來說,智能指針實際上是一個棧對象,並非指針類型,在棧對象生命期即將結束時,智能指針通過析構函數釋放有它管理的堆內存。所有智能指針都重載了“operator->”操作符,直接返回對象的引用,用以操作對象。訪問智能指針原來的方法則使用“.”操作符。
訪問智能指針包含的裸指針則可以用 get() 函數。由於智能指針是一個對象,所以if (my_smart_object)永遠爲真,要判斷智能指針的裸指針是否爲空,需要這樣判斷:if (my_smart_object.get())。
智能指針包含了 reset() 方法,如果不傳遞參數(或者傳遞 NULL),則智能指針會釋放當前管理的內存。如果傳遞一個對象,則智能指針會釋放當前對象,來管理新傳入的對象。
測試1
#include <iostream>
#include <memory>
class Simple {
public:
Simple(int param = 0) {
number = param;
std::cout << "Simple: " << number << std::endl;
}
~Simple() {
std::cout << "~Simple: " << number << std::endl;
}
void PrintSomething() {
std::cout << "PrintSomething: " << info_extend.c_str() << std::endl;
}
std::string info_extend;
int number;
};
void TestAutoPtr() {
std::auto_ptr<Simple> my_memory(new Simple(1)); // 創建對象,輸出:Simple:1
if (my_memory.get()) { // 判斷智能指針是否爲空
my_memory->PrintSomething(); // 使用 operator-> 調用智能指針對象中的函數
my_memory.get()->info_extend = "Addition"; // 使用 get() 返回裸指針,然後給內部對象賦值
my_memory->PrintSomething(); // 再次打印,表明上述賦值成功
(*my_memory).info_extend += " other"; // 使用 operator* 返回智能指針內部對象,然後用“.”調用智能指針對象中的函數
my_memory->PrintSomething(); // 再次打印,表明上述賦值成功
}
}
int main ()
{
TestAutoPtr();
return 0;
}
編譯執行,程序執行正常
測試2
std::auto_ptr 屬於 STL,當然在 namespace std 中,包含頭文件 #include 便可以使用。std::auto_ptr 能夠方便的管理單個堆內存對象。
#include <iostream>
#include <memory>
class Simple {
public:
Simple(int param = 0) {
number = param;
std::cout << "Simple: " << number << std::endl;
}
~Simple() {
std::cout << "~Simple: " << number << std::endl;
}
void PrintSomething() {
std::cout << "PrintSomething: " << info_extend.c_str() << std::endl;
}
std::string info_extend;
int number;
};
/*
void TestAutoPtr() {
std::auto_ptr<Simple> my_memory(new Simple(1)); // 創建對象,輸出:Simple:1
if (my_memory.get()) { // 判斷智能指針是否爲空
my_memory->PrintSomething(); // 使用 operator-> 調用智能指針對象中的函數
my_memory.get()->info_extend = "Addition"; // 使用 get() 返回裸指針,然後給內部對象賦值
my_memory->PrintSomething(); // 再次打印,表明上述賦值成功
(*my_memory).info_extend += " other"; // 使用 operator* 返回智能指針內部對象,然後用“.”調用智能指針對象中的函數
my_memory->PrintSomething(); // 再次打印,表明上述賦值成功
}
}
*/
void TestAutoPtr2() {
std::auto_ptr<Simple> my_memory(new Simple(1));
if (my_memory.get()) {
std::auto_ptr<Simple> my_memory2; // 創建一個新的 my_memory2 對象
my_memory2 = my_memory; // 複製舊的 my_memory 給 my_memory2
my_memory2->PrintSomething(); // 輸出信息,複製成功
my_memory->PrintSomething(); // 崩潰
}
}
int main ()
{
TestAutoPtr2();
return 0;
}
編譯,用GDB來調試查看。
std::auto_ptr my_memory(new Simple(1));執行時:
std::auto_ptr執行時解析:
/**
* 構造函數,將auto_ptr綁定到指針__p。
* __p是一個指向new出來的對象的指針,默認爲0(NULL)是說auto_ptr的構造函數可以不傳參構造,
* 這時成員_M_ptr=0,如果接着解引用auto_ptr對象,將Segmentation fault。當然,通常應用auto_ptr的構造
* 函數會傳參的。auto_ptr提供了get函數來判斷_M_ptr是否爲空、reset函數重置_M_ptr指針。
* 在繼承情況下,_M_ptr可以是__p的基類型。
* 構造函數聲明爲explicit表示禁止參數的自動類型轉換(因爲它們總是邪惡的)。
*
*/
explicit auto_ptr(element_type* __p = 0) throw () :
_M_ptr(__p) {
}
my_memory.get();執行時:
get()函數調用解析:
/**
* 返回auto_ptr管理的指針,這通常用於判斷指針是否爲空的情況,所以,如果要判斷
* auto_ptr管理的指針是否爲空,不要使用if(auto_ptr_obj){}而是使用get函數(實際上,
* 因爲auto_ptr並沒用定義指向element_type的dumb指針的隱式類型轉換操作符,所以根本
* 編譯不過if(auto_ptr_obj))。
* 但是,auto_ptr並沒有禁止你進一步操作你得到的指針,甚至delete它使
* auto_ptr對象內置的指針懸空。
*/
element_type* get() const throw () {
return _M_ptr;
}
my_memory2 = my_memory; 執行時:
可以看到執行 my_memory2 = my_memory這句話後,my_memory的值傳給my_memory2後,my_memory被清零,讓出了內存管理權,my_memory2 完全奪取了 my_memory 的內存管理所有權。
後續調用my_memory->PrintSomething(); 時進行空指針的引用,程序報錯了:Segmentation fault (core dumped)
最終如上代碼導致崩潰,如上代碼時絕對符合 C++ 編程思想的,居然崩潰了,跟進std::auto_ptr 的源碼後,我們看到,罪魁禍首是“my_memory2 = my_memory”,這行代碼,my_memory2 完全奪取了 my_memory 的內存管理所有權,導致 my_memory 懸空,最後使用時導致崩潰。
所以,使用 std::auto_ptr 時,絕對不能使用“operator=”操作符。作爲一個庫,不允許用戶使用,確沒有明確拒絕[1],多少會覺得有點出乎預料。
測試3
#include <iostream>
#include <memory>
class Simple {
public:
Simple(int param = 0) {
number = param;
std::cout << "Simple: " << number << std::endl;
}
~Simple() {
std::cout << "~Simple: " << number << std::endl;
}
void PrintSomething() {
std::cout << "PrintSomething: " << info_extend.c_str() << std::endl;
}
std::string info_extend;
int number;
};
void TestAutoPtr3() {
std::auto_ptr<Simple> my_memory(new Simple(1));
if (my_memory.get()) {
my_memory.release();
}
}
int main ()
{
TestAutoPtr3();
return 0;
}
編譯調試
可以看到my_memory.release();這一句中執行時,先將指針置位0,然後delete 這個執行,實際創建的對象被沒有釋放掉,即對象沒有被析構,沒有輸出“~Simple: 1”。這就造成了內存的浪費。
雖然這次執行正常退出了,但多次執行,可能會把內存搞崩潰了。
如果程序沒有通過delete、free語句現實釋放內存的話,不論是崩潰(崩潰前爲執行釋放語句)還是正常退出(即沒有在程序中寫下釋放語句,但程序‘正常’退出)。那麼這沒有回收的內存將在你每一次關機時又系統回收。
這是一個致命的bug:加入程序持續運行多次,則可能在後面的某一此中因內存溢出而崩潰,而可能根本找不到原因(因爲前幾次都是正確的)。
當我們不想讓 my_memory 繼續生存下去,我們調用 release() 函數釋放內存,結果卻導致內存泄露(在內存受限系統中,如果my_memory佔用太多內存,我們會考慮在使用完成後,立刻歸還,而不是等到 my_memory 結束生命期後才歸還)。
測試4
#include <iostream>
#include <memory>
class Simple {
public:
Simple(int param = 0) {
number = param;
std::cout << "Simple: " << number << std::endl;
}
~Simple() {
std::cout << "~Simple: " << number << std::endl;
}
void PrintSomething() {
std::cout << "PrintSomething: " << info_extend.c_str() << std::endl;
}
std::string info_extend;
int number;
};
void TestAutoPtr3() {
std::auto_ptr<Simple> my_memory(new Simple(1));
if (my_memory.get()) {
Simple* temp_memory = my_memory.release();
delete temp_memory;
}
}
void TestAutoPtr4() {
std::auto_ptr<Simple> my_memory(new Simple(1));
if (my_memory.get()) {
my_memory.reset(); // 釋放 my_memory 內部管理的內存
}
}
int main ()
{
TestAutoPtr3();
TestAutoPtr4();
return 0;
}
delete temp_memory;執行時,調用了析構函數來釋放對象的內存空間。
my_memory.reset(); 執行時,調用了析構函數來釋放對象的內存空間。
總結
原來 std::auto_ptr 的 release() 函數只是讓出內存所有權,這顯然也不符合 C++ 編程思想。
總結:std::auto_ptr 可用來管理單個對象的對內存,但是,請注意如下幾點:
(1) 儘量不要使用“operator=”。如果使用了,請不要再使用先前對象。
(2) 記住 release() 函數不會釋放對象,僅僅歸還所有權。
(3) std::auto_ptr 最好不要當成參數傳遞(讀者可以自行寫代碼確定爲什麼不能)。
(4) 由於 std::auto_ptr 的“operator=”問題,有其管理的對象不能放入 std::vector等容器中。
(5) ……
使用一個 std::auto_ptr 的限制還真多,還不能用來管理堆內存數組,這應該是你目前在想的事情吧,我也覺得限制挺多的,哪天一個不小心,就導致問題了。