2010-04-08 61 views
3

說,例如我有以下代碼(純示例):析構函數如何知道何時激活自己?它可以依賴嗎?

class a { 
    int * p; 
public: 
    a() { 
     p = new int; 
    } 
    ~a() { 
     delete p; 
    } 
}; 

a * returnnew() { 
    a retval; 
    return(&retval); 
} 

int main() { 
    a * foo = returnnew(); 
    return 0; 
} 

在returnnew(),將被RETVAL的函數的返回後(當RETVAL超出範圍)破壞?或者它會禁用自動銷燬後,我返回的地址,我可以說刪除foo;在main()的結尾?或者,以類似的方式(僞代碼):

void foo(void* arg) { 
    bar = (a*)arg; 
    //do stuff 
    exit_thread(); 
} 

int main() { 
    while(true) { 
     a asdf; 
     create_thread(foo, (void*)&asdf); 
    } 
    return 0; 
} 

析構函數會去哪裏?我必須在哪裏說刪除?或者這是不確定的行爲?唯一可能的解決方案是使用STL引用計數的指針嗎?這將如何實施?

謝謝 - 我已經使用了C++一段時間,但從未完全處於這種情況,並且不想創建內存泄漏。

回答

10

對於堆棧創建的對象,析構函數在對象超出作用域時自動調用。

對於在堆上創建的對象,僅在明確調用delete時纔會釋放內存。

是否從函數返回堆棧創建對象的地址無關緊要。當項目超出範圍時,析構函數仍將被調用。

因此,對於你的代碼示例:代碼轉回至調用returnnew()代碼之前

a * returnnew() 
{ 
    a retval; 
    return(&retval); 
} 

a的析構函數被調用。您返回該對象的地址,但該地址指向內存中不再屬於您的地方。

我在哪裏可以說刪除?

您只使用delete當你使用new
您只使用delete[]如果使用new[]

,或者這是不確定的行爲?

你如何處理不屬於你的內存地址將是未定義的行爲。這是不正確的代碼,但。

唯一可行的解​​決方案是使用STL引用計數的指針嗎?

您可以按值返回對象,也可以在堆上創建一個新對象。您還可以通過參數將對象傳遞給函數,並要求函數對其進行更改。

這將如何實施?

//Shows how to fill an object's value by reference 
void fillA(a& mya) 
{ 
    mya.var = 3; 
} 

//Shows how to return a value on the heap 
a* returnNewA() 
{ 
    //Caller is responsible for deleting the returned pointer. 
    return new a(); 
} 

//Shows how to return by value, memory should not be freed, copy will be returned 
a returnA() 
{ 
    return a(); 
} 
+1

+1好的解釋 – 2010-04-08 01:36:51

+0

還要注意的是,「按值返回」的功能會被任何像樣的編譯器優化,使其不產生任何不必要的副本,使得它在這種情況下,最好的選擇(和一般) 。 http://en.wikipedia.org/wiki/Return_value_optimization – 2010-04-08 01:55:30

0

returnnew()會因爲你是返回一個指針到一個局部變量返回時破壞變量。如果你想從那裏返回RETVAL,dinamically分配它,例如:

a * returnnew() { 
    a * retval = new a; 
    return retval; 
} 

或者只是:

a * returnnew() { 
    return new a; 
} 

動態分配的內存沒有範圍,因此將不會被釋放,直到你這麼說,通過刪除/刪除[] /免費,或直到程序退出(和其他幾個與問題無關的情況)。在任何人評論之前,我的「直到你這麼說」還包括共享/智能/等指針行爲。

至於你的第二個代碼&問題,如果你在線程之外分配變量,但是隻從內部使用它,你可以讓線程在不再需要時釋放(刪除)它。但是,如果您計劃從多個點訪問變量,並且無法猜測何時可以安全地銷燬它,那麼當然,請使用智能或共享指針。

0

+1給Brian的答案,只是想添加一個關於線程方面的評論。

您創建線程的代碼將銷燬您傳遞給線程函數的asdf對象,而不管子是什麼,因爲asdf位於父堆棧上。要麼在堆上創建asdf,要麼按值傳遞。否則,父母將銷燬asdf,並讓你的子線程指向父棧上的錯誤地址。在任何情況下都不好,析構函數或沒有析構函數。如果你首先創建了線程,而asdf是ITS堆棧中的堆棧對象,而不是父堆棧,那麼你只能安全地將asdf傳遞給線程中的函數。

void foo(void* arg) { 
    bar = (a*)arg; // child has reference to a parent stack address! 
    //do stuff 
    exit_thread(); 
} 

int main() { 
    while(true) { 
     a asdf; // parent stack 
     create_thread(foo, (void*)&asdf); // parent and child diverge here, asdf auto-destroyed 
    } 
    return 0; 
} 
1
a * returnnew() { 
    a retval; 
    return(&retval); 
} 

這裏,retval具有自動存儲持續時間,這意味着當它超出範圍的語言自動破壞它。您返回的地址引用了一個不再存在的對象,並嘗試使用返回值將是一個錯誤。

當你想要一個對象的生命期被你控制時,你必須使用新的操作符來創建它。

a* returnnew() 
{ 
    a* retval = new a(); 
    return retval; 
} 

在這裏,您現在可以完全控制此a的使用期限。它會一直存在,直到你明確delete它或你的程序結束。

您也可以爲您的a類想出有意義的複製語義,並按值返回,在這種情況下,您的調用者將獲得自己的副本,與原始副本不同。然後,您的來電者不在乎原件何時消失。現在

class a 
{ 
    int * p; 
public: 
    a(a const& rhs) 
    { 
     p = new int(rhs.p) 
    } 
    a() 
    { 
     p = new int; 
    } 
    ~a() 
    { 
     delete p; 
    } 
}; 

,你可以構造一個新的a作爲現有a的副本。所以,你的函數然後可以通過值返回a,就像這樣:

a returnnew() 
{ 
    a retval; 
    return retval; 
} 

這裏RETVAL的壽命將當函數返回時結束,它會自動由語言遭到破壞,也沒有資源將被泄露。而你的來電者將擁有自己的副本,並擁有自己的一生。

根據我的經驗,大多數類應該有明智的複製語義,並且不應該害怕按值傳遞和返回它們。這樣做更簡單,避免懸掛指針問題。

C++最大的優勢之一就是自動存儲持續時間對象超出範圍時,語言自動調用析構函數的方式。如果確保程序中的每個資源都屬於這樣一個對象,那麼泄漏資源的時間就會更加困難。

+1

你需要一個賦值操作符,做'* p = *(rhs.p)'。 – fredoverflow 2010-04-08 07:44:38

+0

Woops,這是一個很好的電話。你幾乎總是不想或兩者兼而有之。 – janks 2010-04-08 08:00:05

0

在下面的代碼的情況下:

void foo(void* arg) { 
    bar = (a*)arg; 
    //do stuff 
    exit_thread(); 
} 

int main() { 
    while(true) { 
     a asdf; 
     create_thread(foo, (void*)&asdf); 
    } 
    return 0; 
} 

析構函數被稱爲在while循環的閉括號。這意味着它將被稱爲,每循環迭代(並且將在下一次迭代中再次構建)。

隨着探索的構造函數和析構函數和範圍的細微差別,可以考慮使用下面的類來幫助你在未來的回答這些問題,自己的有用工具:

class trace { 
private: 
    std::string msg_; 
public: 
    explicit trace(const std::string &msg) : msg_(msg) { 
    std::cerr << "Constructing: " << msg_ << std::endl; 
    } 
    ~trace() { 
    std::cerr << "Destructing: " << msg_ << std::endl; 
    } 
}; 

正是如此使用它:

trace glb("global"); 

main() { 
    trace t1("top of main"); 

    for(int i = 0; i < 10; ++i) 
    { 
    trace t2("inside for"); 
    } 

    return 0; 
} 

結果可能會讓你大吃一驚。

1

作爲一般規則,避免C++中內存泄漏的最簡單方法是儘可能避免使用new和delete。

如果你必須有一個指向使用智能指針的指針(例如boost scoped_ptr或shared_ptr)。這樣,您仍然可以將堆放在堆上,但是當智能指針超出範圍時,會調用解構器。否則,試圖確保在任何情況下調用delete都可能會令人頭疼,並導致大量額外的代碼。

相關問題