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