C++ 对象作为返回值:复制还是引用?

Posted

技术标签:

【中文标题】C++ 对象作为返回值:复制还是引用?【英文标题】:C++ object as return value: copy or reference? 【发布时间】:2012-04-19 14:25:24 【问题描述】:

我想测试当函数的返回值为对象时 C++ 的行为。我做了这个小例子来观察分配了多少字节,并确定编译器是复制对象(比如当对象作为参数传递时)还是返回某种引用。

但是,我无法运行这个非常简单的程序,我也不知道为什么。错误说:“调试断言失败!表达式:BLOCK_TYPE_IS_INVALID”在某些 dbgdel.cpp 文件中。 Project 是一个 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;
   

【问题讨论】:

可能重复 [Debug Assertion Failed ... _BLOCK_TYPE_IS_VALID (pHead ](***.com/questions/1102123/…) 问题是当你返回值时,它会复制对象中的指针。然后两个副本都被销毁,但它们都指向同一个内存块。当第二个被销毁时,它会尝试重新删除同一块内存,这是不允许的。 我找到了一个不错的page,它解释了如何在 C++ 中返回对象 【参考方案1】:

你违反了Rule of Three。

具体来说,当您返回一个对象时,会生成一个副本,然后将其销毁。因此,您有一系列事件,例如

Ctest1::Ctest1(void);
Ctest1::Ctest1(const Ctest1&);
Ctest1::~Ctest1();
Ctest1::~Ctest1();

即创建了两个对象:你的原始对象构造,然后是隐式复制构造函数。然后删除这两个对象。

由于这两个对象都包含 same 指针,因此您最终会在同一个值上调用 delete 两次。 轰隆隆


额外的功劳:当我调查“我想知道副本是如何制作的”之类的问题时,我将 print 语句放在有趣的类方法中,如下所示:
#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;

【讨论】:

虽然你是对的,除非 OP 已经知道这意味着什么,否则这个答案将毫无意义。你能描述得更详细一点吗? 你在制作答案的过程中抓住了我。现在已经完成了。 哇,谢谢大家,从来没有听说过三法则,我知道用指针复制对象可能会导致这种情况,但我不知道表达式 Return 可以调用某些东西的析构函数。 为什么return会立即复制并销毁它? :] 我不能像这样使用返回的对象? :[ 如果您没有看到额外的 copy-ctor 调用,-fno-elide-constructors 标志可能很有用。【参考方案2】:

得到(最终)您最初打算询问的内容,简短的回答是这很少会成为问题。该标准包含一个条款,明确免除编译器必须在返回值上实际使用复制构造函数,即使复制构造函数有副作用,因此差异是外部可见的。

根据您是返回变量还是仅返回值,这称为命名返回值优化 (NRVO) 或仅返回值优化 (RVO)。大多数合理的现代编译器都实现了这两者(有些,例如 g++ 甚至在您关闭优化时也会这样做)。

为了避免复制返回值,编译器所做的是将复制的地址作为隐藏参数传递给函数。函数然后在那个地方构造它的返回值,所以在函数返回之后,这个值已经在那里,没有被复制。

这很常见,而且效果很好,以至于 Dave Abrahams(当时是 C++ 标准委员会成员)在几年前写了 an article 表明,对于现代编译器,人们避免额外复制的尝试实际上通常会产生代码比只写简单、明显的代码要慢。

【讨论】:

【参考方案3】:

正如 Rob 所说,您还没有创建 C++ 使用的所有三个构造函数/赋值运算符。他提到的三法则的意思是,如果你声明一个析构函数、复制构造函数或赋值运算符 (operator=()),你需要全部使用这三个。

如果您不创建这些函数,那么编译器将为您创建它们自己的版本。但是,编译器复制构造函数和赋值运算符只对原始对象中的元素进行浅拷贝。这意味着作为返回值创建的复制对象,然后复制到main() 中的对象中具有指向与您创建的第一个对象相同地址的指针。因此,当原始对象被销毁为复制对象腾出空间时,堆上的 classSpace 数组被释放,导致复制对象的指针失效。

【讨论】:

【参考方案4】:

如果您想查看对象的副本何时生成,只需执行以下操作:

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;

【讨论】:

哇,伟大的思想都一样。但我没有移动 ctor 或移动赋值运算符。

以上是关于C++ 对象作为返回值:复制还是引用?的主要内容,如果未能解决你的问题,请参考以下文章

c++中为啥要函数返回引用?

C++ 把引用作为函数返回值

C++返回值为Const &的看法 大虾们给解释下

当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?

当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?

c++的复制构造函数