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

Posted

技术标签:

【中文标题】C++ 数组对复制构造函数和赋值运算符【英文标题】:C++ array of pairs copy constructor and assignment operator 【发布时间】:2020-08-14 05:08:48 【问题描述】:

我在我的 C++ 程序中使用我的复制构造函数和赋值运算符。在单独测试它们中的任何一个时,我都会遇到分段错误(核心转储)。我正在构建一个哈希表,该哈希表是通过一个数组构造的,每个索引中有一对。索引是基于散列函数选择的,对的第一部分是键,对的第二部分是值。课程显然还有更多内容,但不会影响复制和赋值运算符,因此我将它们保留在那里。我没有内存泄漏,我测试了 op= 和复制构造函数,其中已经有很多值。

在 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= 更有信心。

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

【问题讨论】:

什么是Dictionary 你永远不会在任何一个函数中为 m_data 赋值。 @Beta 它只是一个带有虚函数的模板类 - 它实际上没有任何代码,只是一个要编写的函数模板 【参考方案1】:

m_data 被定义为UnorderedMap.h 中的一个指针,初始化为nullptr,但在实现中它被使用而不被分配给一些实际的存储。

【讨论】:

它在单独的插入函数中正确、动态地分配了实际存储空间 - 我只是不希望这篇文章太长.. 抱歉,我只是假设这意味着我的错误! @bmcisme 也许在某些函数中设置了它,但看起来复制构造函数根本没有设置它。 @bmcisme 推荐:Familiarize yourself with RAII。如果您在构造函数中完成繁重的工作,尤其是在获取资源方面,那么您以后所做的一切都会容易得多。【参考方案2】:

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

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

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

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

【讨论】:

这种初始化风格适用于现代 C++,即 C++11 及以上版本。 对于您的第一部分,我删除了 operator= 中的 delete [] m_data .. 还是应该删除 m_data 然后像上面那样重新分配一个新的 m_data 数组? @ad3angel1s 啊,我猜他们在我的 CS 课程简介中没有教我们这个,哈哈 @bmcisme 你肯定想删除旧数组,但你也肯定想确保重新分配一个新的 m_data 数组,因为两个不同的MyPair 对象可能有不同的数组长度。跨度> @mppombo5 不幸的是,学校可能是学习 C++ 的危险场所。一方面,太多的人仍在使用基于 Pascal 的教学大纲为 C 设计的教学大纲进行教学。许多不那么残酷的案例没有涵盖 C++11,因为......好吧,他们只是没有。有趣的是,即使课程要教授 1970 年代的风格,你仍然需要购买最新版本的教科书。这本书可能比讲座更新鲜、更相关。【参考方案3】:

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

如果您采用 cmets 中的说明,您的代码应如下所示:

一、拷贝构造函数:

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 块之后)与旧内容一起死亡。不存在抛出异常和创建无效对象的问题,复制构造函数和赋值运算符之间不存在冗余代码等问题。

这种编写赋值运算符的技术称为copy / swap idiom。它工作的唯一方法是如果你有一个 correct 复制构造函数和一个 correct 析构函数。这就是我们首先编写这两个函数的原因,以便利用 swap-ping 赋值运算符中的成员。

【讨论】:

以上是关于C++ 数组对复制构造函数和赋值运算符的主要内容,如果未能解决你的问题,请参考以下文章

c++中拷贝构造函数和赋值运算符重载本质上一样么

是否为数组/向量插入调用了赋值运算符或复制构造函数?

在 C++ 中编写复制构造函数和赋值运算符的清单

❥关于C++之类的复制构造函数&赋值运算符

赋值运算符和复制构造函数有啥区别?

C++中赋值运算操作符和=重载有啥区别?