使用赋值重载语法复制构造函数?

Copy Constructor with assignment overloading syntax?

我正在编写大五(复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符、析构函数)。我在复制构造函数语法方面遇到了一些问题。

假设我有一个 class foo 具有以下私有成员:

    template<class data> // edit
    class foo{

    private:
        int size, cursor; // Size is my array size, and cursor is the index I am currently pointing at
        data * dataArray; // edit
    }

如果我要为此编写一个任意大小的构造函数 X 它看起来像这样。

template<class data> // edit
foo<data>::foo(int X){
    size = X;
    dataArray = new data[size];
    cursor = 0; // points to the first value
}

现在,如果我想为另一个名为 bar 的对象创建一个复制构造函数,我需要执行以下操作:

template<class data> // edit
foo<data>::foo(foo &bar){
foo = bar; // is this correct? 
}

假设我从下面的代码中重载了 =

 template<class data> // edit
    foo<data>::operator=(foo &someObject){
        if(this != someObject){
            size = someObject.size;
            cursor = someObject.cursor;
            delete[] dataArray;
            dataArray = new data[size];
            for(cursor = 0; cursor<size-1;cursor++)
                 dataArray[cursor] = someObject.dataArray[cursor];
            }

        else
            // does nothing because it is assigned to itself
        return *this;
        }

我的拷贝构造函数正确吗?或者应该 foo = bar 而不是 *this = bar

我对模板化构造函数还是个新手,所以如果我在代码中犯了任何错误,请告诉我,我会更正它。

编辑 1: 感谢 Marcin 在下面提供的答案,我对上面的代码进行了一些编辑,使其在语法上更加正确,并用 //edit 对其进行了评论它们总结在下面的列表中:

  1. 以前是 template<classname data>,这是不正确的,对于函数和 classes 必须分别是 template <typename data>template <class data>
  2. 以前 int*dataArray; 这误用了模板,应该是 data* dataArray;

你的 foo class 没有在内部使用 data 模板参数。我想你想在这里使用它:

int * dataArray; // should be: data * dataArray;

您也不能使用 classname 关键字,但可以使用 typenameclass。您的代码中还有很多其他编译错误。

你的复制构造函数是错误的,它不会编译:

foo = bar; // is this correct? - answer is NO

foo 在此上下文中是一个 class 名称,因此您的假设是正确的。 *this = someObject 这会工作(有额外的修复,至少 dataArray 必须设置为 nullptr),但是你的 class 变量将默认首先由复制构造函数构造,然后被赋值运算符覆盖,所以它安静无效率。如需更多信息,请阅读此处:

Calling assignment operator in copy constructor

Is it bad form to call the default assignment operator from the copy constructor?

实现你想要的最好方法是使用一个已经处理分配、复制和移动的 class,为你处理它的内存管理。 std::vector 正是这样做的,可以直接替换你动态分配的数组和大小。 类 这样做通常被称为 RAII classes。


话虽如此,并且假设这是正确实现各种特殊成员函数的练习,我建议您通过 SO 上的 copy and swap idiom. (See What is the copy and swap idiom? 继续,以获取更多详细信息和评论)。这个想法是根据复制构造函数来定义赋值操作。

从成员、构造函数和析构函数开始。这些定义了 class:

成员的所有权语义
template <class data>
class foo {
 public:
  foo(const size_t n);
  ~foo();

 private:
  size_t size; // array size
  size_t cursor; // current index
  data* dataArray; // dynamically allocated array
};

template <class data>
foo<data>::foo(const size_t n)
 : size(n), cursor(0), dataArray(new data[n])
{}

template <class data>
foo<data>::~foo() {
    delete[] dataArray;
}

这里,内存在构造函数中分配,在析构函数中释放。 接下来写拷贝构造函数。

template <class data>
foo<data>::foo(const foo<data>& other)
 : size(other.size), cursor(other.cursor), dataArray(new data[other.size]) {
     std::copy(other.dataArray, other.dataArray + size, dataArray);
}

(连同声明,foo(const foo& other); 在 class 正文中)。 请注意它如何使用成员初始化列表将成员变量设置为 other 对象中的值。执行新的分配,然后在复制构造函数的主体中将数据从 other 对象复制到该对象中。

接下来是赋值运算符。您现有的实现必须执行大量指针操作,并且不是异常安全的。让我们看看如何更简单、更安全地做到这一点:

template <class data>
foo<data>& foo<data>::operator=(const foo<data>& rhs) {
  foo tmp(rhs); // Invoke copy constructor to create temporary foo

  // Swap our contents with the contents of the temporary foo:
  using std::swap;
  swap(size, tmp.size);
  swap(cursor, tmp.cursor);
  swap(dataArray, tmp.dataArray);

  return *this;
}

(连同在-class、foo& operator=(const foo& rhs);中的声明)。

[-- 旁白:您可以通过按值 接受函数参数 来避免编写第一行(显式复制对象)。这是一回事,在某些情况下可能更有效率:

template <class data>
foo<data>& foo<data>::operator=(foo<data> rhs) // Note pass by value!
{
  // Swap our contents with the contents of the temporary foo:
  using std::swap;
  swap(size, rhs.size);
  swap(cursor, rhs.cursor);
  swap(dataArray, rhs.dataArray);

  return *this;
}

但是,如果您还定义了移动赋值运算符,这样做可能会导致不明确的重载。 --]

这样做的第一件事是创建一个被赋值对象的副本。这利用了复制构造函数,因此对象如何复制的细节只需要在复制构造函数中实现一次。

复制完成后,我们将内部结构与副本的内部结构交换。在函数体的末尾,tmp 副本超出范围,其析构函数清理内存。但这不是在函数开始时分配的内存;这是我们的对象 使用 保存的内存,在我们与临时对象交换状态之前。

这样,分配、复制和解除分配的细节就保留在它们所属的地方,在构造函数和析构函数中。赋值运算符只是 copiesswaps.

除了更简单之外,这还有一个优点:它是异常安全的。在上面的代码中,分配错误可能导致在创建临时对象时抛出异常。但是我们还没有修改 class 的状态,所以即使赋值失败,我们的状态仍然保持一致(和正确)。


遵循相同的逻辑,移动操作变得微不足道。必须定义移动构造函数以简单地获取资源的所有权并将源(移出的对象)留在定义明确的状态中。这意味着将源的 dataArray 成员设置为 nullptr,以便其析构函数中的后续 delete[] 不会导致问题。

移动赋值运算符的实现可以类似于复制赋值,尽管在这种情况下不太关心异常安全性,因为您只是在窃取源对象的已分配内存。在完整的示例代码中,我选择简单地交换状态。

可以看到一个完整的、可编译和可运行的例子here