11

我想测试当函数的返回值为对象时 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

4 回答 4

15

你违反了三法则

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

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

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

由于这两个对象都包含相同的指针,因此您最终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;
}
于 2012-04-05T20:30:49.633 回答
8

得到(最终)你最初想要问的问题,简短的回答是它很少是一个问题。该标准包含一个条款,该条款明确规定编译器不必在返回值上实际使用复制构造函数,即使复制构造函数有副作用,所以差异是外部可见的。

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

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

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

于 2012-04-05T20:52:38.720 回答
5

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

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

于 2012-04-05T20:39:28.597 回答
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;
}
于 2012-04-05T20:51:19.090 回答