提问人:bmcisme 提问时间:4/30/2020 最后编辑:bmcisme 更新时间:4/30/2020 访问量:417
C++ 对数组复制构造函数和赋值运算符
C++ array of pairs copy constructor and assignment operator
问:
我在 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= 仍然不起作用。我修复了上面的复制构造函数,所以现在它显示了一个有效的复制构造函数,供其他任何想要将其用作有效工作基础的人使用。还修复了赋值运算符,并提供了正确的版本。
答:
m_data
被定义为 中的指针,初始化为 但在实现中,它被使用而没有被分配给某个实际的存储。UnorderedMap.h
nullptr
评论
我注意到在你的复制构造函数中,你,但几行后你开始向它赋值;这是一个很大的禁忌。在你之后,你必须使用或类似的东西分配一个新数组。delete [] m_data
delete []
m_data = new MyPair<K,V>[data_size];
我还注意到您只是在类声明中初始化了您的类,使用
private:
MyPair<K,V>* m_data = nullptr;
data_size = 0;
等等。这通常不是C++中初始化的工作方式;你必须把它放在实际的构造函数中,看起来不像你在代码中实现了一个。
评论
delete [] m_data
MyPair
我假设它有正确的复制语义。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
评论
Dictionary
m_data