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<K,V>[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++ 数组对复制构造函数和赋值运算符的主要内容,如果未能解决你的问题,请参考以下文章