在赋值运算符中使用复制构造函数

Using copy constructor in assignment operator

本文关键字:复制 构造函数 赋值运算符      更新时间:2023-10-16

在赋值运算符中使用复制构造函数是否违反样式准则?即:

const Obj & Obj::operator=(const Obj & source)
{
    if (this == &source)
    {
        return *this;
    }
    // deep copy using copy-constructor
    Obj * copy = new Obj(source);
    // deallocate memory
    this->~Obj();
    // modify object
    *this = *copy;
    return *copy;
}

假设复制构造函数对对象执行深度复制。


编辑:

正如评论者所指出的,我的代码是极其错误的。

至于整体概念问题:正如WhozCraig所建议的那样,复制/交换习惯用法似乎是可行的:什么是复制和交换习惯用法?

以下是示例中的复制/交换习惯用法:

#include <algorithm>
class Obj
{
    int *p;
    void swap(Obj& left, Obj& right);
public:
    Obj(int x = 0) : p(new int(x)) {}
    Obj(const Obj& s);
    Obj& operator = (const Obj& s);
    ~Obj() { delete p; }
};
Obj::Obj(const Obj& source) : p(new int(*source.p))
{}
void Obj::swap(Obj& left, Obj& right)
{
    std::swap(left.p, right.p);
}
Obj & Obj::operator=(const Obj & source)
{
    Obj temp(source);
    swap(*this, temp);
    return *this;
}
int main()
{
    Obj o1(5);
    Obj o2(o1);
    Obj o3(10);
    o1 = o3;
}

为了了解它的工作原理,我有目的地创建了一个成员,它是动态分配内存的指针(如果没有用户定义的复制构造函数和赋值运算符,这将是有问题的)。

如果您关注赋值运算符,它会调用Obj复制构造函数来构造临时对象。然后调用特定于Objswap来交换各个成员。现在,在调用swap之后,魔法就在temp对象中。

当调用temp的析构函数时,它将对this曾经具有的指针值调用delete,但已被temp指针替换掉。因此,当temp超出作用域时,它会清理"旧"指针分配的内存。

此外,请注意,在分配期间,如果new在创建临时对象期间抛出异常,则在this的任何成员发生更改之前,分配将抛出异常。这样可以防止对象的成员由于无意中更改而损坏。

现在,前面给出的答案是使用常用的"共享代码"方法来复制赋值。以下是该方法的完整示例,并解释了它为什么会出现问题:

class Obj
{
    int *p;
    void CopyMe(const Obj& source);
public:
    Obj(int x = 0) : p(new int(x)) {}
    Obj(const Obj& s);
    Obj& operator = (const Obj& s);
    ~Obj() { delete p; }
};
void Obj::CopyMe(const Obj& source)
{
    delete p;
    p = new int(*source.p);
}
Obj::Obj(const Obj& source) : p(0)
{
   CopyMe(source);
}
Obj & Obj::operator=(const Obj & source)
{
    if ( this != &source )
        CopyMe(source);
    return *this;
} 

所以你会说"这怎么了?"好吧,问题是CopyMe的第一件事就是调用delete p;。然后你会问的下一个问题是"那又怎样?这不是我们应该做的吗,删除旧内存?"

这样做的问题是,对new的后续调用有可能失败。因此,我们所做的是在我们知道新数据将可用之前销毁我们的数据。如果new现在抛出一个异常,则表示我们的对象搞砸了。

是的,您可以通过创建一个临时指针、分配并在最后将临时指针分配给p来轻松修复它。但很多时候,这一点可能会被忘记,像上面这样的代码永远留在代码库中,即使它有潜在的损坏错误。

为了说明,以下是CopyMe:的修复程序

void Obj::CopyMe(const Obj& source)  
{
    int *pTemp = new int(*source.p);
    delete p;
    p = pTemp;
}

但是,您将再次看到大量使用"共享代码"方法的代码存在此潜在错误,而对异常问题只字不提。

您试图做的事情不起作用。您似乎认为赋值运算符返回的对象变成了新的"this",但事实并非如此。你没有修改对象,只是销毁了它。

Obj a;
Obj b;
a = b; // a has been destroyed
       // and you've leaked a new Obj.
// At the end of the scope, it will try to destroy `a` again, which
// is undefined behavior.

使用放置new可以实现赋值运算符,但这是一个脆弱的解决方案,通常不推荐使用:

const Obj & Obj::operator=(const Obj & source)
{
    if (this == &source)
    {
        return *this;
    }
    this->~Obj();
    new (this) Obj(source);
    return *this;
}

如果在构造过程中可能出现异常,并且可能导致派生类出现问题,则会导致问题。

在复制构造函数和assignnet运算符之间共享代码是完全有意义的,因为它们经常执行相同的操作(将作为参数属性传递的对象复制到此)。

就我个人而言,我经常通过巧妙地对赋值运算符进行编码,然后从复制构造函数调用它:

Obj::Obj(const Obj & source)
{
    Obj::operator=( source );
}
const Obj& Obj::operator=(const Obj& source)
{
    if (this != &source)
    {
        // copy source attribtes to this.
    }    
    return *this;
}

如果您正确地编写operator=,它就会起作用如前所述,建议使用一个交换函数:复制C++中的构造函数和=运算符重载:是否可以使用通用函数?

无论如何,您在两个函数之间共享代码的想法是好的,但您实现它的方式并不好。工作是肯定的。。它有很多问题,并没有达到你想要的目的。它递归地调用运算符=。此外,您永远不应该像以前那样显式调用析构函数(this->~Obj();)。