为什么我的类模板的动态分配数组属性只能存储一个项

Why is the dynamically allocated array attribute of my class template only able to store one item?

本文关键字:存储 一个 属性 我的 数组 动态分配 为什么      更新时间:2023-10-16

我正在尝试扩展我创建的类模板的功能。以前,它允许您使用任何类型的键值对,但前提是您在编译时知道数组的大小。它看起来是这样的:

template <typename K, typename V, int N>
class KVList {
    size_t arraySize;
    size_t numberOfElements;
    K keys[N];
    V values[N];
public:
    KVList() : arraySize(N), numberOfElements(0) { }
    // More member functions
}

我希望能够将其用于运行时决定的动态元素数量,所以我将代码更改为:

template <typename K, typename V>
class KVList {
    size_t arraySize;
    size_t numberOfElements;
    K* keys;
    V* values;
public:
    KVList(size_t size) : numberOfElements(0) {
        arraySize = size;
        keys = new K[size];
        values = new V[size];
    }
    ~KVList() {
        delete[] keys;
        keys = nullptr;
        delete[] values;
        values = nullptr;
    }
    // More member functions
}

新构造函数有一个参数,该参数的大小将用于KVList。它仍然在0处启动numberOfElements,因为这两种用法都会将KVList启动为空,但它确实将arraySize设置为size参数的值。然后,它为键和值的数组动态分配内存。添加的析构函数会释放这些数组的内存,然后将它们设置为nullptr。

它编译并运行,但它只存储我试图添加到它的第一个键和第一个值。两者中都有一个成员函数,可以向数组添加一个键值对。我用Visual Studio 2015调试器测试了这一点,注意到它将第一个键值对存储得很好,然后它试图将下一个键值对保存在下一个索引中,但数据不在任何位置。调试器在每个数组中只显示一个插槽。当我试图对我认为存储在第二个索引中的数据进行排序时,我得到了一个非常小的数字(试图存储的是浮点数据类型),而不是我试图存储的数据。

我知道使用向量来实现这一点可能是值得的。然而,这是我在学校C++课上完成的一项作业的扩展,我这样做的目标是努力完成它,并了解以这种方式做可能会导致问题的原因,因为就我目前所知,这对我来说是显而易见的。

EDIT:用于添加键值对的代码:

// Adds a new element to the list if room exists and returns a reference to the current object, does nothing if no room exists
KVList& add(const K& key, const V& value) {
    if (numberOfElements < arraySize) {
        keys[numberOfElements] = key;
        values[numberOfElements] = value;
        numberOfElements++;
    }
    return *this;
}

EDIT:调用add()的代码:

// Temp strings for parts of a grade record
string studentNumber, grade;
// Get each part of the grade record
getline(fin, studentNumber, subGradeDelim); // subGradeDelim is a char whose value is ' '
getline(fin, grade, gradeDelim); // gradeDelim is a char whose value is 'n'
// Attempt to parse and store the data from the temp strings
try {
    data.add(stoi(studentNumber), stof(grade)); // data is a KVList<size_t, float> attribute
}
catch (...) {
    // Temporary safeguard, will implement throwing later
    data.add(0u, -1);
}

用于测试显示信息的代码:

void Grades::displayGrades(ostream& os) const {
    // Just doing first two as test
    os << data.value(0) << std::endl;
    os << data.value(1);
}

用于测试的主cpp文件中的代码:

Grades grades("w6.dat");
grades.displayGrades(cout);

w6.dat:的内容

1022342 67.4
1024567 73.5
2031456 79.3
6032144 53.5
1053250 92.1
3026721 86.5
7420134 62.3
9762314 58.7
6521045 34.6

输出:

67.4
-1.9984e+18

问题(或至少其中一个)是您的pastebin中的这一行:

 data = KVList<size_t, float>(records);

这条看似无辜的线路做了很多事情。由于数据已经存在,默认情况下会构建您输入Grades构造函数主体的实例,因此这将做三件事:

  1. 它将使用构造函数在右侧构造一个KVList
  2. 它将调用复制赋值运算符,并将我们在步骤1中构造的内容赋值给数据
  3. 右侧的对象被破坏

你可能在想:什么复印作业操作员,我从来没有写过。编译器会自动为您生成它。实际上,在C++11中,使用显式析构函数自动生成拷贝赋值运算符(正如您所做的那样)是不推荐的;但它仍然存在。

问题是编译器生成的复制赋值运算符不适合您。所有成员变量都是琐碎的类型:整数和指针。所以他们只是抄了一遍。这意味着在第2步之后,类已经以最明显的方式被复制了。这反过来意味着,对于一个简短的例子,左边和右边都有一个对象,它们都有指向内存中同一位置的指针。当步骤3启动时,右侧对象实际上会继续并删除内存。因此,数据只剩下指向随机垃圾内存的指针。写入这个随机内存是未定义的行为,所以你的程序可能会做(不一定是确定性的)奇怪的事情。

(老实说)显式资源管理类的编写方式有很多问题,这里无法涵盖太多问题。我认为在Accelerated C++这本非常优秀的书中,它将带你了解这些问题,其中有整整一章涵盖了如何正确编写这样一个类的每一个细节。