標籤:

在函數內new一個對象,如果作為引用返回,是不是就可以不用delete了?

背景大概是這樣的:

一個外部函數,它的目的是產生並返回一個新的對象(不管它的形式是對象,指針還是引用)。

獲得了這個對象後,可以在外部操作它,對此我有幾個疑問。

舉例來說,以重載的乘法為例,類型是自定義的IntPair。

class IntPair {
public:
int x;
int y;
IntPair(int _x, int _y) :x(_x), y(_y){ }
~IntPair() {
std::cout &<&< "x:" &<&< x &<&< " y:" &<&< y &<&< " destory" &<&< std::endl; } };

下面的幾種情況,區別在於:

1.函數的返回值(引用還是指針,且第三種情況引用或者對象的結果是一樣的);

2.生成新對象的方式(new,還是聲明),對應的外部獲取該對象的方式;

3.是不是需要delete

第一,假使返回的是指針,在外部需要釋放這個指針:

IntPair* operator*(const IntPair lhs, const IntPair rhs) {
int x = lhs.x * rhs.x;
int y = lhs.y * rhs.y;
IntPair* r = new IntPair(x, y);
std::cout &<&< "* " &<&< r-&>x &<&< " " &<&< r-&>y &<&< std::endl; return r; } int main() { IntPair* r = IntPair(1, 2) * IntPair(2, 3); std::cout &<&< " " &<&< r-&>x &<&< " " &<&< r-&>y &<&< std::endl; delete r; return(0); }

運行一次,也就只有三個對象,這樣全都消除了吧。

第二,那假如我返回一個引用呢?在外面delete這個r的地址(delete r)就報錯了。

IntPair operator*(const IntPair lhs, const IntPair rhs) {
int x = lhs.x * rhs.x;
int y = lhs.y * rhs.y;
IntPair* r = new IntPair(x, y);
std::cout &<&< "* " &<&< r-&>x &<&< " " &<&< r-&>y &<&< std::endl; return *r; } int main() { IntPair r = IntPair(1, 2) * IntPair(2, 3); std::cout &<&< " " &<&< r.x &<&< " " &<&< r.y &<&< std::endl; return(0); }

如果不delete,結果是三個對象也都被釋放了。

因為剛學c++的時候,有聽說new一個對象的話,必然要delete一次。在這種情況下,不delete也是OK的嗎,為什麼就自動釋放了呢?好神奇。

第三,我在那個做乘法的函數里,如果不是new出來的對象,而是那種直接聲明出來的對象,這個時候,不管返回值是引用IntPair還是對象IntPair,這個局部的對象必然會消亡吧!

外面就需要一個copy了?

IntPair operator*(const IntPair lhs, const IntPair rhs) {
int x = lhs.x * rhs.x;
int y = lhs.y * rhs.y;
IntPair r = IntPair(x, y);
std::cout &<&< "* " &<&< r.x &<&< " " &<&< r.y &<&< std::endl; return r; } int main() { IntPair r = IntPair(1, 2) * IntPair(2, 3); std::cout &<&< " " &<&< r.x &<&< " " &<&< r.y &<&< std::endl; return(0); }

ps,這裡因為一個外部的copy,所以多創建了一個對象對吧?

當然我的希望是不要這個copy的調用,所以想問一般情況下,要在一個函數內創建一個新對象,然後返回,到底使用哪種方式是最好的。

p.p.s,說起來,Java直接返回了一個引用(是吧?),對象的內容就不會隨著函數的結束而釋放,進而也就不需要copy了,那它和前兩種方式哪種比較像?

還是回到C++好了,主要是第二種情況,太奇怪了,new的對象不delete也可以嗎?

大概就是這兩個問題額!

哦,編譯器是VS2013,另外,多謝各位了!


一個建議,你在構造函數里加一條cout &<&< "construct"的輸出

這樣就能看清構造和析構是不是能對應了


沒邀請,厚臉皮強答。

先回答你提到的問題。只是我得先聲明一下,就此例子來看,你這幾個問題都有些「有問題,我們要解決問題;沒問題,我們也要創造出問題然後解決它。」給搞的。我真正想搶答的是後面幾點。那些可以幫到C++初學者。

問題一:返回引用不用刪除?會自動刪除?

原代碼:

IntPair operator*(const IntPair lhs, const IntPair rhs) {
int x = lhs.x * rhs.x;
int y = lhs.y * rhs.y;
IntPair* r = new IntPair(x, y);
return *r;
}

int main() {
009 IntPair r = IntPair(1, 2) * IntPair(2, 3);
return(0);
}

沒有自動刪除。你看到的三個析構調用,其中有一個是代碼中標為 009 行定義的r的析構。你返回引用,可是你沒有用引用來"接"這個對象。請對比:

IntPair r = IntPair(1, 2) * IntPair(2, 3);
IntPair rr = IntPair(1, 2) * IntPair(2, 3);

r新構建了一個對象,然後複製了函數返回的引用的值 。rr才是引用。這道理和函數返回指針,你卻不用指針接,非要這樣:

IntPair r = * (IntPair(1, 2) * IntPair(2, 3));

此時,r是個新對象。(重載的 * )函數返回指針同樣沒有被釋放。而你顯然也不能嘗試釋放 r 。

這個問題你後來發現了? 所以乾脆先插一句話:想觀察對象的生命周期,請匹配地在它構造函數和析構函數里列印信息。比如本例,如果你在構造里也列印信息,那你就很容易發現構4析3了。

問題二:函數內創建的堆對象,返回什麼好?

答:如果確實需要函數創建(需要釋放)堆對象,那就返回 std::unique_ptr&吧。不用你IntPair對象了,就以一個int* 為例:

#include &
#include &

std::unique_ptr& create(int value)
{
int* p = new int(value);
return std::unique_ptr&(p);
}

int main(int argc, char **argv)
{
std::unique_ptr& p = create(99);
std::cout &<&< *p &<&< std::endl; //後面也不用手工delete }

C++ 14或17?會有make_unique()函數,所以到時上面create()內的new關鍵字可以不出現了,這會讓追求new 和 delete 完美匹配的處女座舒服點,如:

return std::make_unique&(value);

如果在多線程環境下,並且明確知道所返回的堆對象會被多線程並發訪問,那就用std::shared_ptr&代替std::unique_ptr&。這時make_shared()已經是現成標準。

當然,會有時候,我們寫create()時,我們可以肯定知道的是,這傢伙返回後,需要釋放 ;卻不能確切知道返回的對象會不會被多線程使用,此時還是回歸簡單,就返回更基礎,更簡單,更環保,更傻瓜的 unique_ptr。讓調用者(也是知情者)去負責考慮要不要轉換成shared_ptr。

=$=$=$=$=

我想談問題是:

在函數內返回堆對象?要守規矩。

生孩子是一種非常稀奇的事嗎?當然不是,全世界隨時,到處都有人生娃。但生孩子是一個人一生中的常見活動或行為嗎?顯然也不是。

C++代碼中出現在函數體中直接new一個對象並返回的機率,和生娃差沒多久。

就說你舉的例子。我們知道,在C++中執行 3 * 2,肯定不會返回一個需要你釋放的指針。所以兩個IntPair對象執行相乘rw操作,,卻設計出返回一個需要釋放的指針,這不是介面不友好,讓調用者糾結、猜測、迷惘甚至失去生活的信心,簡直反人類的設計。

所以,就應該大大方方地返回沒有釋放需求的棧對象。C++相比像Java等其它語言,是沒有GC,可是C++有明確自動回收棧對象啊。回收及時,回收時機完全可控。回收還速度超過堆內存……如果不用它,非要用堆對象,然後再扯出Java:

說起來,Java直接返回了一個引用(是吧?),對象的內容就不會隨著函數的結束而釋放,進而也就不需要copy了,那它和前兩種方式哪種比較像?

有些不 厚道。 :)

//直接返回棧對象才人道:
IntPair operator*(const IntPair lhs, const IntPair rhs)
{
return IntPair(lhs.x * rhs.x, lhs.y * rhs.y);
}

馬上有人說,那要複製一次臨時效率多低啊?其實不會。編譯器(估計在10年前)就會幫優化這樣的c返回了。如果對象內部了確實又複雜又大,可以考慮定製 IntPair 的 「move construct」行為。

什麼情況下,確實需要從函數返回新的堆對象? 簡單說:你想都不用想地,直覺地為某個函數命名為 CreateXXX 或 NewXXX()時。


所以想問一般情況下,要在一個函數內創建一個新對象,然後返回,到底使用哪種方式是最好的。

這種情況題主可以直接讓operator*返回一個IntPair對象,不需要返回指針或引用。

IntPair operator*(const IntPair lhs, const IntPair rhs)
{
int x = lhs.x * rhs.x;
int y = lhs.y * rhs.y;
std::cout &<&< "* " &<&< x &<&< " " &<&< y &<&< std::endl; return IntPair(x, y); }

然後直接:

IntPair r = IntPair(1, 2) * IntPair(2, 3);

等號右側計算的結果是個右值,VS會使用默認的移動構造運算符(即operator=(IntPair))將這個operator*返回的臨時對象的所有權交給r,不會有額外的複製開銷。


首先,第三種是錯誤的。你創建一個局部變數然後返回它的引用,你明明知道這是返回的已經退棧的局部變數了,程序今天沒崩潰只是未做檢查的僥倖而已。

第二種,你內存泄露了。實際上你產生了四個實例,兩個作為乘數,一個是你new的,一個是你在main函數棧里的。new的那個你沒有delete,因此內存泄露

第一種是正確的。

————————————————————————————————————

有朋友質疑說,第三種方法是正確的(認為可以讓一個函數返回局部變數的引用,然後外面用一個拷貝構造來完成數據的return)。我們暫且不討論脫離作用域這種問題,我們只分析函數return的時候,將離開它的作用域,這個object將被析構,如此返回得到的東西,還是你原來希望的嗎?我們來看下面的簡單例子:

#include &
using namespace std;
class Foo {
public:
int value;
~Foo(){
// Destruct the object
this-&>value = 0;
cout &<&< "deleting Foo" &<&< endl; }; }; Foo getFoo(){ Foo f = Foo(); // here user want to set something f.value = 33; cout &<&< f.value &<&< endl; return f; } int main() { Foo f2 = getFoo(); // the return value is not as expected cout &<&< f2.value &<&< endl; return 0; }

上述代碼的執行結果是什麼?

33
deleting Foo
0
deleting Foo

很顯然,在外層函數拷貝構造之前,裡面的object已經析造了。外層函數得到實例是不準確的。

本例僅僅做了個數值變化以示區別,萬一人家的destructor里做了更不得了的事情呢(關閉文件IO,關閉資料庫鏈接等等。。)


只討論operator*。

第一種是對的,但不提倡。

第二種和第一種完全一樣。

第三種是錯的。

正確的做法應該是直接返回值,也就是返回類型是IntPair。搜索RVO就知道了。


你可以認為引用就是指針的語法糖。所以在函數里new出來以引用返回的話,外面也可以再轉成指針然後delete,否則會泄露。如果是局部變數你要返回引用的話,退出函數變數已經銷毀了,你也許能訪問到裡面的一部分數據,也許訪問會崩潰,這都是未定義行為,不過一般這種情況編譯器會warning。如果你反回的是全局變數或者靜態變數,那麼和指針沒什麼區別。


就這道題來說,最好的是返回struct{int x;int y;}。效率最高,沒有內存泄露。因為64位系統這個結構只需要一個寄存器就可以返回,32位寄存器也可以通過2個寄存器就可以返回,哪裡需要分配內存。

另外動態分配內存最好是誰分配,誰釋放,子函數分配內存,在外面釋放,是非常不可取的。

至於你的疑惑,問題在於析構函數被調用,不等於對象被調用delete刪除。

臨時變數退出生命周期也會調用析構函數,但對象是在棧中,不是調用delete釋放。

第二種你看到析構函數調用了,只不過是main裡面的臨時變數r退出生命周期調用的,並不是你new的那個對象。


@叛逆者說錯了,第一種和第二種並不一樣。

第一種沒問題,但這麼搞代碼很難維護;

第二種內存泄露了,你new的那個沒釋放,在main上是新構造了一個;

第三種是錯誤的。

建議搜索關鍵字智能指針,RVO,默認移動構造函數,默認複製構造函數


第二種實際上有4個對象,你泄漏了一個。


不請自來,

第一個是對的,三個都會釋放

IntPair* operator*(const IntPair lhs, const IntPair rhs) {
int x = lhs.x * rhs.x;
int y = lhs.y * rhs.y;
IntPair* r = new IntPair(x, y);
std::cout &<&< "* " &<&< r-&>x &<&< " " &<&< r-&>y &<&< std::endl; return r; } int main() { IntPair* r = IntPair(1, 2) * IntPair(2, 3); // RHS的兩個object已死 std::cout &<&< " " &<&< r-&>x &<&< " " &<&< r-&>y &<&< std::endl; delete r; // 刪除r return(0); }

IntPair* r = IntPair(1, 2) * IntPair(2, 3);

IntPair(1,2) 和 IntPair(2, 3) 在表達式計算完以後就掛了,

r是new出來的所以需要delete

第二個是錯的,題主認為知道打出 x:2 y:6, destory 難道就代表new的object掛了么。事實上,這一行只能代表一個 x = 2, y = 6 的object被析構了,但是並沒有表明就是那個new出來的object被析構了, 在這個例子里,new出來的object的一個副本被析構,然而new出來的object泄露了。我們可以來看一下valgrind的結果:

IntPair operator*(const IntPair lhs, const IntPair rhs) {
int x = lhs.x * rhs.x;
int y = lhs.y * rhs.y;
IntPair* r = new IntPair(x, y);
std::cout &<&< "* " &<&< r-&>x &<&< " " &<&< r-&>y &<&< std::endl; return *r; //傳出new出來的object的引用 } int main() { IntPair r = IntPair(1, 2) * IntPair(2, 3); //把傳出的引用的值Copy給r //new出來的object不但沒destory而且泄露了 std::cout &<&< " " &<&< r.x &<&< " " &<&< r.y &<&< std::endl; return(0); //r析構 }

至於第三個,答主的做法是錯誤並且非常危險的,因為答主返回了一個已經被析構的object的引用。這就是為什麼答主會看到兩個x = 2 , y = 6 destoryed。

IntPair operator*(const IntPair lhs, const IntPair rhs) {
int x = lhs.x * rhs.x;
int y = lhs.y * rhs.y;
IntPair r = IntPair(x, y); // 創建了一個object
std::cout &<&< "* " &<&< r.x &<&< " " &<&< r.y &<&< std::endl; return r; //r的生命周期結束,析構 } int main() { IntPair r = IntPair(1, 2) * IntPair(2, 3); //危險! r copy了未定義的棧內存 std::cout &<&< " " &<&< r.x &<&< " " &<&< r.y &<&< std::endl; return(0); //r 析構 }

GCC編譯的結果在這次直接crash,我們用clang++生成的代碼的valgrind結果

我們而已看到大量的由於引用未定義內存造成的錯誤。

最後夾帶一點私貨,我覺得題主的問題是一個很好的關於C++引用是拙劣的設計的例子。另外,引用導致了代碼可讀性的降低,因為你寫a = 3的時候,你有可能實際修改的是b。

然而指針不會產生這樣的問題,因為所有人都知道*a = 3 修改的是內存中其他的位置。

總之,妄圖給指針穿馬甲來加強安全性的行為,最終還是自己騙自己


因為類中有默認的析構函數,即使你沒有delete,也任然會消除物理內存里存儲的內容,但是你還是需要將指針nullptr


全錯!

第一種返回指針,哪天我寫一個intpair(1,2)*intpair(2,3)=intpair(1,2)是不是就傻逼了?有人會說肯定不會這麼寫,但是別忘了在if語句裡面有的人會不小心把==寫成=。

所以正確的做法就是返回copy。第二第三種寫法就更錯了,前面有很多人指出,不再贅述了。


如下:

第一個:總共3個object: (1,2), (2, 3) 和new構造的(2,6), 一共destruct三次,正確。

第二個:總共4個object:(1,2), (2, 3), new構造的(2,6),以及r

注意:

IntPair r = IntPair(1, 2) * IntPair(2, 3);

這行實際上是使用了IntPair的copy constructor,將new構造的(2,6)這一動態內存上的object中的所有屬性複製到stack靜態內存上一個新建的IntPair object中。顯然r並沒有用到動態內存所以delete r會報錯。與此同時,即使overload函數go out of scope, new所創建的object也不會消失。這種情況就非常僵硬了,因為main里沒有一個指針是指向這個動態object的。

第三個:返回的是overload函數scope內r的地址, 但是這個r會隨著函數go out of scope而被destruct。這種情況便是dangling pointer - 指針原本指向的東西已經不在內存上了。某些compiler是會報錯的。

注意:此時指針指向的不是nullptr, 而是一些不可名狀。因為用於放置它原先指向的object的內存上已經不知道是什麼東西了。


第一種其實也可能有錯,new和delete操作符操作的是當前庫的內存空間,如果你在庫A的函數里new出來一個c指針給庫B用,在庫B里delete就會報錯(就算你把整個c對象定義也放進庫B)。正確的做法是在c對象里加一個release方法,其中delete this,然後通過介面類傳過去,庫B中調用c-&>release,c=0,能確保delete在正確的庫中執行

如果是個數據對象的話,可以在庫里建一個專門用來delete該對象的全局函數導出


你的第三個肯定會導致內存泄露,new必須使用delete釋放


推薦下面的寫法

IntPair operator*=(IntPair z) {x*=z.x, y*=z.y; return *this;}
IntPair operator*(IntPair a, IntPair b) {return a*=b;}


第一種代碼難看,但是是對的

第二種內存泄漏

第三種返回局部對象的引用,錯的


可以在構造函數和析構函數里列印出對象地址,看下每個對象的構造和析構,第二種情況釋放的不是new的對象


我習慣性不喜歡寫delete,所以我習慣寫個調配器,像這種:

template&

class adapter{

public:

adapter(){

pObject = new T();

};

~adapter(){

if(pObject!=nullptr){

delete pObject;

pObject = nullptr;

}

};

T* operator -&>() {

return pObject;

};

private:

T *_pObject;

}

但是雖然不常用,但總感覺怪怪的......


除非你想內存泄漏


推薦閱讀:

為什麼一個空的class的大小是1個位元組?
C++非同步回調如何更優雅?
[C++] 能否設計一個一般的計時函數?
unity項目越大編譯速度越慢 ue4用藍圖秒編譯 背後分別的原因是什麼?
c++的強制類型轉換?

TAG:C |