【问题标题】:Best way to overload the C++ assignment operator重载 C++ 赋值运算符的最佳方法
【发布时间】:2013-02-27 15:39:55
【问题描述】:

我有一个 A 类,它在其构造函数中为整数(由类成员说 _pPtrMem 指向)动态分配内存,并在析构函数中释放相同的内存。为了避免浅拷贝,我重载了赋值运算符和拷贝构造函数。赋值运算符重载的广泛使用方式如下:

A& operator = (const A & iToAssign)
{
  if (this == & iToAssign)  // Check for self assignment
    return *this;
  int * pTemp = new int(*(iToAssign._pPtrMem)); // Allocate new memory with same value
  if (pTemp)
  {
    delete _pPtrMem;  // Delete the old memory
    _pPtrMem = pTemp; // Assign the newly allocated memory
  }
  return *this;   // Return the reference to object for chaining(a = b = c)
}

实现相同的另一种方法是

A& operator = (const A & iToAssign)
{
  *_pPtrMem= *(iToAssign._pPtrMem); // Just copy the values
  return *this;   
}

既然第二个版本比较简单和快速(没有释放,分配内存)为什么没有被广泛使用?有什么我无法弄清楚的问题吗?

此外,我们从赋值运算符中返回一个相同类型的对象用于链接(a = b = c)...所以不是返回 *this 而是返回 iToAssign 对象,因为这两个对象现在应该是相等的?

【问题讨论】:

  • 如果你只有一个整数,为什么还要使用指针呢?如果您有动态分配的数组,请考虑改用std::vector
  • 注意:在版本 1 中,delete 为时过早。应该在当前对象处于稳定状态后进行(否则不提供强异常保证)。在这里它不稳定,因为您在删除后分配给当前对象。你应该std::swap(_pPtrMem, pTemp);delete pTemp;。最好使用复制和交换习语。
  • @LokiAstari 但是分配指针不能抛出异常,没有什么可以出错的。
  • @TorstenRobitzki:但是删除可以抛出(如果被删除的对象有析构函数)。因此,为了保持一致,请在完成更改对象后始终放置删除(这样,如果您稍后修改包含的类型,则无需更改分配中的代码)。但是您对于像 int 这样的 POD 类型是正确的,这就是为什么这只是一个评论。
  • @LokiAstari delete不会为时过早(但不需要进行自分配测试)。如果delete 可以抛出,则no 可以安全地实现它;这就是为什么析构函数永远不应该抛出。 (有一些特殊的例外,但其析构函数可能抛出的类永远不应用作另一个类的一部分或放入容器中。)

标签: c++ operator-overloading assignment-operator


【解决方案1】:

通常,实现复制赋值运算符的最佳方式是为您的类提供一个swap() 函数(或使用标准函数,如果它可以满足您的需要),然后通过复制构造函数实现复制赋值运算符:

A& A::operator= (A iToAssign)  // note pass by value here - will invoke copy constructor
{
  iToAssign.swap(*this);
  return *this;
}
// void swap(A& other) throws() // C++03
void A::swap(A& other) noexcept
{
    std::swap(_pPtrMem, other._pPtrMem);
}

这可以确保您的复制赋值运算符和复制构造函数永远不会发生分歧(也就是说,不会发生您更改了一个而忘记更改另一个)。

【讨论】:

  • "或者使用标准的,如果它做你想做的事" -- 在我看来这会导致无限递归,因为std::swap 依赖于operator= .
  • @LokiAstari 不一样。如果您为您的班级提供自己的swap() 重载,则using std::swap 惯用语允许通过ADL 找到此重载,并且只有在不存在这样的情况时才会使用“using-ed”std::swap
  • @Angew:是的,想通了。但是您不能在赋值运算符中使用 std::swap (根据 Benjamins 的评论)。因此,您必须定义自己的(或在作业中手动进行交换)。因此,再次将 using 语句放入其中是没有意义的。
  • @AlexChamberlain:但是在 RHS 是 R 值的情况下,您会同时抑制移动语义和复制省略。
  • @AlexChamberlain:另外,this fairly famous article.
【解决方案2】:

不,您的实施没有问题。但是动态分配一个整数至少是非常特别的。

这种实现并没有被广泛使用,因为没有人在空闲存储区分配一个整数。您通常将动态分配的内存用于在编译时可变长度未知的数组。在这种情况下,大多数时候只使用 std::vector 是个好主意。

不,这不好,返回一个不同的对象。身份不等于平等:

T a, b, d;
T& c = a = b;
c = d; // should change a, not b 

你会期望第三行改变 b 吗?

或者更好的事件示例:

T a;
T& b = a = T();

这将导致一个悬空引用,引用一个临时的和被破坏的对象。

【讨论】:

  • 感谢您对我的第二个查询的清晰解释...很好的例子!
【解决方案3】:

第一个版本用于_pPtrMem 是指向一些基本类型的指针,例如动态分配的数组。如果指针指向具有正确实现的赋值运算符的单个对象,则第二个版本将做得同样好。但在那种情况下,我认为您根本不需要使用指针。

【讨论】:

    【解决方案4】:

    在第二种情况下,如果 _pPtrMem 最初未分配,则该行

    *_pPtrMem= *(iToAssign._pPtrMem); // Just copy the values
    

    导致分配到无效的内存位置(可能是分段错误)。这只有在 _pPtrMem 在此调用之前已分配内存时才有效。

    【讨论】:

    • “我有一个类 A,它在其构造函数中动态分配 [...]pPtrMem [..]”.
    【解决方案5】:

    在这种情况下,第二种实现要好得多。 但是在对象中使用动态的通常原因是因为 大小可能会有所不同。 (另一个原因是因为你想要 浅拷贝的引用语义。)在这种情况下, 最简单的解决方案使用你的第一个作业(没有 测试自我分配)。根据对象,可能 如果新值,考虑重用已经存在的内存 会合适;它在某种程度上增加了复杂性(因为你必须 测试它是否适合,并且仍然执行 分配/复制/删除,如果没有),但它可以改进 在某些情况下的表现。

    【讨论】:

      猜你喜欢
      • 2013-03-30
      • 2016-08-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-12-27
      • 2012-04-22
      • 2015-06-01
      • 2011-01-27
      相关资源
      最近更新 更多