C++ 对数组复制构造函数和赋值运算符

C++ array of pairs copy constructor and assignment operator

提问人:bmcisme 提问时间:4/30/2020 最后编辑:bmcisme 更新时间:4/30/2020 访问量:417

问:

我在 C++ 程序中为我的复制构造函数和赋值运算符而苦苦挣扎。我在单独测试其中任何一个时遇到分段错误(核心转储)。我正在构建一个哈希表,该哈希表是通过一个数组构建的,每个索引内都有一对。索引是根据哈希函数选择的,对的第一部分是键,对的第二部分是值。显然,这门课还有更多内容,但不会影响复制和赋值运算符,所以我把它们留在那里。我没有内存泄漏,并且我测试了 op= 和 copy 构造函数,其中已经有大量值。

在 UnorderedMap.h 中

template <typename K, typename V>
class MyUnorderedMap: public Dictionary<K, V>
{
    private:
        MyPair<K, V> *m_data = nullptr; // hash table, array of pairs

        int data_size = 0; // current number of elements inside the array
        int reserved_size = 0; // max elements inside the array

    public:
        // Start data_size and reserved_size at 0, m_data to nullptr
        MyUnorderedMap();

        ~MyUnorderedMap();

        MyUnorderedMap(const MyUnorderedMap<K, V> &source);

        MyUnorderedMap<K, V> & operator=(const MyUnorderedMap<K, V> &source);
}

在 UnorderedMap.hpp 中

// Copy Constructor
template <typename K, typename V>
MyUnorderedMap<K, V>::MyUnorderedMap(const MyUnorderedMap<K, V> &source)
{
  data_size = source.data_size;
  reserved_size = source.reserved_size;
  m_data = new MyPair<K, V>[reserved_size];
  for(int i = 0; i < reserved_size; i++)
  {
    m_data[i].first = source.m_data[i].first;
    m_data[i].second = source.m_data[i].second;
  }
}


// Assignment Operator
template <typename K, typename V>
MyUnorderedMap<K, V> & MyUnorderedMap<K, V>::operator=(const MyUnorderedMap<K, V> &source)
{
  if(this!=&source)
  {
    delete[] m_data;
    reserved_size = source.reserved_size;
    data_size = source.data_size;
    m_data = new  MyPair<K, V>[reserved_size];
    for(int i=0; i<reserved_size; i++)
    {
      m_data[i].first = source.m_data[i].first;
      m_data[i].second = source.m_data[i].second;
    }
  }
  return *this;
}

在 MyPair.h 中

template <typename K, typename V> 
struct MyPair
{
    K first; 
    V second;

    MyPair(){}
    MyPair(const K &key): first(key) {}
    MyPair(const K &key, const V &value): first(key), second(value) {}
};

有没有人看到它为什么会这样?我对我的复制构造函数比 operator= 更有信心。

编辑 x3:我有一个未显示的插入函数,可以正确插入到哈希表中。所以我解决了复制构造函数,但 op= 仍然不起作用。我修复了上面的复制构造函数,所以现在它显示了一个有效的复制构造函数,供其他任何想要将其用作有效工作基础的人使用。还修复了赋值运算符,并提供了正确的版本。

C++ 数组 hashtable copy-constructor assignment-operator

评论

0赞 Beta 4/30/2020
什么是?Dictionary
0赞 1201ProgramAlarm 4/30/2020
您永远不会在任一函数中为其赋值。m_data
0赞 bmcisme 4/30/2020
@Beta它只是一个带有虚拟函数的模板化类 - 它实际上没有任何代码,只是一个要编写的函数模板

答:

0赞 ad3angel1s 4/30/2020 #1

m_data被定义为 中的指针,初始化为 但在实现中,它被使用而没有被分配给某个实际的存储。UnorderedMap.hnullptr

评论

0赞 bmcisme 4/30/2020
它被正确、动态地分配到一个单独的插入函数中的实际存储 - 我只是不希望这篇文章太长。.对不起,我只是以为那会暗示我的坏!
0赞 aschepler 4/30/2020
@bmcisme 也许在某些函数中设置了它,但看起来复制构造函数根本没有设置它。
0赞 user4581301 4/30/2020
@bmcisme建议:熟悉 RAII。如果你在构造函数中做繁重的工作,特别是在获取资源方面,那么你以后所做的几乎所有事情都会容易得多。
0赞 Matthew P. 4/30/2020 #2

我注意到在你的复制构造函数中,你,但几行后你开始向它赋值;这是一个很大的禁忌。在你之后,你必须使用或类似的东西分配一个新数组。delete [] m_datadelete []m_data = new MyPair<K,V>[data_size];

我还注意到您只是在类声明中初始化了您的类,使用

private:
    MyPair<K,V>* m_data = nullptr;
    data_size = 0;

等等。这通常不是C++中初始化的工作方式;你必须把它放在实际的构造函数中,看起来不像你在代码中实现了一个。

评论

0赞 ad3angel1s 4/30/2020
这种初始化风格适用于现代 C++,即 C++11 及以后。
0赞 bmcisme 4/30/2020
对于您的第一部分,我删除了运算符中的 = ..还是我应该删除m_data然后像上面一样重新分配一个新的m_data数组?delete [] m_data
0赞 Matthew P. 4/30/2020
@ad3angel1s啊,我猜他们没有在我的 CS 课程介绍中教过我们,哈哈
0赞 Matthew P. 4/30/2020
@bmcisme您肯定想要删除旧数组,但您也肯定希望确保重新分配新的m_data数组,因为两个不同的对象可能具有不同的数组长度。MyPair
0赞 user4581301 4/30/2020
不幸的是,@mppombo5学校可能是一个学习C++的危险场所。一方面,有太多人仍然使用基于Pascal教学大纲的C语言教学大纲。许多不那么残酷的案例不包括 C++11,因为......好吧,他们只是没有。有趣的是,即使课程将按照 1970 年代的风格教授,您仍然需要购买最新版本的教科书。这本书可能比讲座更新鲜、更相关。
0赞 PaulMcKenzie 4/30/2020 #3

我假设它有正确的复制语义。Dictionary

如果你从注释中获取了已经陈述的内容,那么你的代码应该是什么样子的:

首先,复制构造函数:

template <typename K, typename V>
MyUnorderedMap<K, V>::MyUnorderedMap(const MyUnorderedMap<K, V> &source) 
        : m_data(new MyPair<K, V>[source.reserved_size]),  // <-- You are missing this
          data_size(source.data_size), 
          reserved_size(source.reserved_size)
{
   for(int i = 0; i < reserved_size; i++)
   {
      m_data[i].first = source.m_data[i].first;
      m_data[i].second = source.m_data[i].second;
   }
}

复制构造函数使用列表。请注意,内存是在成员初始化列表中分配的。这是复制构造函数版本中没有的操作,也是代码的主要问题。member-initialization

但是,还有其他问题,可以通过赋值运算符的实现来解决。

在复制构造函数之后应实现的下一个函数是析构函数,而不是赋值运算符。接下来要实现析构函数是有战略原因的,稍后将对此进行解释。

template <typename K, typename V>
MyUnorderedMap<K, V>::~MyUnorderedMap() 
{
   delete [] m_data;
}

现在您已经有了这两个函数,赋值运算符就变得微不足道了。但是,让我们回顾一下您有缺陷的版本:

template <typename K, typename V>
MyUnorderedMap<K, V> & MyUnorderedMap<K, V>::operator=(const MyUnorderedMap<K, V>  
                                                       &source)
{
     if(this!=&source)
     {
         delete[] m_data;
         data_size = source.data_size;

让我们就到此为止。您可能已经用最后两行代码损坏了您的对象。

原因是您已经更改了对象的成员,但尚未分配内存。如果对下一行的调用引发异常,该怎么办?现在,您有一个处于无效状态的对象。new[]

如果以这种方式编写赋值运算符,则应采用的编写方式是确保首先分配所有内存,然后开始调整对象的成员变量。

但是有一种更简单的方法可以避免所有这些:

#include <algorithm>
//...
template <typename K, typename V>
MyUnorderedMap<K, V> & MyUnorderedMap<K, V>::operator=(const MyUnorderedMap<K, V>  
                                                       &source)
{
     if(this!=&source)
     {
         // create a temporary 
         MyUnorderedMap<K,V> temp(source);

         // swap out the temp's contents with the current object
         std::swap(temp.data, data);
         std::swap(temp.data_size, data_size);
         std::swap(temp.reserved_size, reserved_size);

         // temp will be destroyed when the if() goes out of scope
      }
      return *this;
  }

那么为什么会这样呢?这很简单,代码基本上记录了所做的事情。

您正在创建传入对象的副本。然后,您将当前对象的内容与副本的内容交换。然后副本与旧内容一起死亡(在块之后)。不存在抛出异常和创建无效对象的问题,复制构造函数和赋值运算符之间没有冗余代码等。if

这种编写赋值运算符的技术称为复制/交换惯用语。它工作的唯一方法是,如果您具有正确的复制构造函数和正确的析构函数。这就是为什么我们首先编写这两个函数,以便利用赋值运算符中简单的 -ping 成员。swap