[英]C++ object as return value: copy or reference?
我想測試當 function 的返回值是 object 時 C++ 的行為。我做了這個小例子來觀察分配了多少字節並確定編譯器是否復制了 object(就像當 object 作為參數傳遞時)或者返回某種引用。
但是,我無法運行這個非常簡單的程序,我也不知道為什么。 錯誤提示:某些 dbgdel.cpp 文件中的“調試斷言失敗:表達式。BLOCK_TYPE_IS_INVALID”。 項目是一個 win32 控制台應用程序。 但我很確定這段代碼有問題。
class Ctest1
{
public:
Ctest1(void);
~Ctest1(void);
char* classSpace;
};
Ctest1::Ctest1(void)
{
classSpace = new char[100];
}
Ctest1::~Ctest1(void)
{
delete [] classSpace;
}
Ctest1 Function(Ctest1* cPtr){
return *cPtr;
}
int _tmain(int argc, _TCHAR* argv[])
{
Ctest1* cPtr;
cPtr=new Ctest1();
for(int i=1;i<10;i++)
*cPtr = Function(cPtr);
delete cPtr;
return 0;
}
你違反了三原則。
具體來說,當您返回 object 時,會制作一個副本,然后銷毀。 所以,你有一系列的事件,比如
Ctest1::Ctest1(void);
Ctest1::Ctest1(const Ctest1&);
Ctest1::~Ctest1();
Ctest1::~Ctest1();
即創建了兩個對象:原始的 object 構造,然后是隱式復制構造函數。 然后這兩個對象都被刪除。
由於這兩個對象都包含相同的指針,因此您最終會針對相同的值調用兩次delete
。 繁榮
#include <iostream> int serial_source = 0; class Ctest1 { #define X(s) (std::cout << s << ": " << serial << "\n") const int serial; public: Ctest1(void): serial(serial_source++) { X("Ctest1::Ctest1(void)"); } ~Ctest1(void) { X("Ctest1::~Ctest1()"); } Ctest1(const Ctest1& other): serial(serial_source++) { X("Ctest1::Ctest1(const Ctest1&)"); std::cout << " Copied from " << other.serial << "\n"; } void operator=(const Ctest1& other) { X("operator="); std::cout << " Assigning from " << other.serial << "\n"; } #undef X }; Ctest1 Function(Ctest1* cPtr){ return *cPtr; } int main() { Ctest1* cPtr; cPtr=new Ctest1(); for(int i=1;i<10;i++) *cPtr = Function(cPtr); delete cPtr; return 0; }
得到(最終)您最初打算詢問的內容,簡短的回答是這很少是問題。 該標准包含一個條款,專門免除編譯器必須在返回值上實際使用復制構造函數,即使復制構造函數有副作用,所以差異是外部可見的。
根據您返回的是變量還是值,這稱為命名返回值優化 (NRVO) 或僅返回值優化 (RVO)。 最合理的現代編譯器同時實現了這兩者(一些,例如 g++ 甚至在您關閉優化時也會這樣做)。
為了避免復制返回值,編譯器所做的是將復制的地址go作為隱藏參數傳遞給function。然后function在那個地方構造它的返回值,所以function返回后,該值已經存在而不被復制。
這很常見,而且效果很好,以至於 Dave Abrahams(當時是 C++ 標准委員會成員)幾年前寫了一篇文章表明,使用現代編譯器,人們試圖避免額外復制的嘗試實際上產生的代碼比如果你只需編寫簡單、明顯的代碼。
正如 Rob 所說,您還沒有創建 C++ 使用的所有三個構造函數/賦值運算符。 他提到的三法則的意思是,如果你聲明一個析構函數、復制構造函數或賦值運算符( operator=()
),你需要使用所有三個。
如果您不創建這些函數,那么編譯器將為您創建它自己的版本。 但是,編譯器復制構造函數和賦值運算符僅對原始 object 中的元素進行淺表復制。這意味着復制的 object 作為返回值創建,然后復制到main()
中的 object 具有指向相同對象的指針地址為您創建的第一個 object。 因此,當原始 object 被銷毀以為復制的 object 騰出空間時,堆上的 classSpace 數組將被釋放,導致復制對象的指針失效。
如果您想查看 object 的副本何時制作,只需執行以下操作:
struct Foo {
Foo() { std::cout << "default ctor\n"; }
Foo(Foo const &) { std::cout << "copy ctor\n"; }
Foo(Foo &&) { std::cout << "move ctor\n"; }
Foo &operator=(Foo const &) { std::cout << "copy assign\n"; return *this; }
Foo &operator=(Foo &&) { std::cout << "move assign\n"; return *this; }
~Foo() { std::cout << "dtor\n"; }
};
Foo Function(Foo* f){
return *f;
}
int main(int argc,const char *argv[])
{
Foo* f=new Foo;
for(int i=1;i<10;i++)
*f = Function(f);
delete f;
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.