C++对象作为返回值:复制或引用

C++ object as return value: copy or reference?

本文关键字:复制 引用 返回值 对象 C++      更新时间:2023-10-16

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

你违反了三条规则。

具体来说,当您返回一个对象时,会生成一个副本,然后销毁。所以,你有一系列类似的事件

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

(最终)了解您最初想要询问的内容,简短的回答是这很少是问题。该标准包含一个子句,该子句专门免除编译器在返回值上实际使用复制构造函数的必要性,即使复制构造函数有副作用,因此差异在外部是可见的。

根据返回变量还是仅返回值,这被称为命名返回值优化(NRVO)或仅返回值优化。大多数相当现代的编译器都实现这两种功能(有些编译器,如g++,甚至在关闭优化时也能实现)。

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

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

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

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

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

struct Foo {
    Foo() { std::cout << "default ctorn"; }
    Foo(Foo const &) { std::cout << "copy ctorn"; }
    Foo(Foo &&) { std::cout << "move ctorn"; }
    Foo &operator=(Foo const &) { std::cout << "copy assignn"; return *this; }
    Foo &operator=(Foo &&) { std::cout << "move assignn"; return *this; }
    ~Foo() { std::cout << "dtorn"; }
};
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;
}