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

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

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

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。

这编译和 运行s,但它只存储我尝试添加到它的第一个键和第一个值。两者都有一个成员函数,可以将键值对添加到数组中。我用 Visual Studio 2015 调试器对此进行了测试,发现它很好地存储了第一个键值对,然后它尝试将下一个键值对存储在下一个索引中,但数据无处可去。并且调试器只显示每个数组中的一个槽。当我尝试计算我认为我存储在第二个索引中的数据时,我得到一个非常小的数字(试图存储浮点数据类型),而不是我试图存储的数据。

我知道使用向量来完成这个可能是值得的。然而,这是我在学校用 C++ class 完成的作业的扩展,我这样做的目的是尝试完成它,并了解这样做可能会导致问题的原因,因为这是根据我目前所掌握的知识,这对我来说是显而易见的方法。

编辑:用于添加键值对的代码:

// 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;
}

编辑:调用 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 步之后,class 刚刚以最明显的方式被复制过来。反过来,这意味着在一个简短的例子中,左右各有一个对象,它们都有指向内存中相同位置的指针。当第 3 步触发时,右边的对象实际上会继续执行并删除内存。所以数据留下了指向随机垃圾内存的指针。写入此随机内存是未定义的行为,因此您的程序可能会做(不一定是确定性的)奇怪的事情。

(老实说)您的显式资源管理 class 的编写方式存在很多问题,太多无法在此处涵盖。我认为在 Accelerated C+ 中,一本非常优秀的书,它将引导您解决这些问题,并且有一整章涵盖了如何正确编写这样一个 class.

的每个细节。