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

时间:2012-04-05 20:27:22

标签: c++ memory object return

我想测试当函数的返回值是一个对象时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;
   }

4 个答案:

答案 0 :(得分:14)

您违反了Rule of Three

具体来说,当您返回一个对象时,会复制 ,然后销毁它。所以,你有一系列事件,比如

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

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

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

<小时/> 额外信用:当我调查诸如“我想知道如何制作副本”之类的问题时,我将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;
}

答案 1 :(得分:3)

正如Rob所说,你还没有创建C ++使用的所有三个构造函数/赋值运算符。他提到的三条规则的含义是,如果你宣布一个析构函数,复制构造函数或赋值运算符(operator=()),你需要使用这三个。

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

答案 2 :(得分:3)

获得(最终)你最初打算询问的内容,简短的回答是它很少出现问题。该标准包含一个子句,专门免除编译器必须在返回值上实际使用复制构造函数,即使复制构造函数具有副作用,因此差异在外部可见。

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

为了避免复制返回值,编译器所做的是将副本作为隐藏参数的地址传递给函数。然后该函数在该位置构造其返回值,因此在函数返回后,该值已经存在而没有被复制。

这很常见,而且运作良好,几年前Dave Abrahams(C ++标准委员会成员)写了一篇article,表明在现代编译器中,人们试图避免“额外复制”实际产生代码比你编写简单明了的代码要慢。

答案 3 :(得分:2)

如果您想查看对象副本的制作时间,请执行以下操作:

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;
}