从指针返回对象时意外的析构函数调用

Unexpected destructor call when returning object from a pointer

我正在尝试将我在 C# 中创建的优先级队列实现重新创建为 C++,作为一个项目以跳入 C++,但是很多细微差别让我感到困惑。队列被设计为一个模板,可以在任何给定的 class T 上工作。队列将明确地使用一个表示称为优先级对的对象的结构:一个指向 T 对象的指针和一个关联的优先级值 (int)。

这样做的目的是让队列中被比较的实际对象(T's)完全独立并且只被指向。我可能并不明确需要该结构来完成此操作,但我就是这样做的。

队列实现中的重要部分:

template <class T>
class PriorityQueue
{
public:
    PriorityQueue(const int maxSizeIn)
    {
        maxSize = maxSizeIn;
        queueArray = new PriorityPair<T>*[maxSize];
        currentHeapSize = 0;
    }
    ~PriorityQueue()
    {
        cout << "Destroy Queue with size: " << currentHeapSize << endl;
        for (int i = 0; i < currentHeapSize; i++)
        {
            delete (PriorityPair<T>*)queueArray[i];
        }
        delete[] queueArray;
}
private:
    PriorityPair<T>** queueArray;

PriorityPair 的结构:

template <class T>
struct PriorityPair
{
    PriorityPair(int valueIn, T* objectIn)
    {
        _PriorityValue = valueIn;
        _Object = objectIn;
    };

    ~PriorityPair()
    {
         cout << "Destroy Pair for object :(" << *_Object << "):  << endl;
    }

    int _PriorityValue;
    T* _Object;
};

在我的测试过程中,我发现调用我的 PeekTop 方法似乎会导致调用 PriorityPair 的析构函数。我最好的猜测是,由于未能理解该语言的一些细微差别,我不小心创建了一个临时的。

查看方法如下:

    T PeekTop()
    {
        if (IsEmpty())
            return nullptr;
        else
            return *((PriorityPair<T>)(*queueArray[0]))._Object; 
    }

另外,这里是插入操作(最低效插入,不做heap/queue操作):

    int InsertElement(PriorityPair<T>* elementIn)
    {
        //do not insert nulls --
        if (elementIn == nullptr)
            return -2;
        //we could user std::vector or manually expand the array, but a hard max is probably sufficient
        if (currentHeapSize == maxSize)
        {
            return -1;
        }
        //insert the pointer to the new pair element in at the index corresponding to the current size, then increment the size
        queueArray[currentHeapSize++] = elementIn;
        return 0;
    }

我主要有以下几点:

PriorityQueue<string> queue = PriorityQueue<string>(10);
string s1 = "string1";
int code = queue.InsertElement(new PriorityPair<string>(5, &s1));
string i = queue.PeekTop();
cout << "-------\n";
cout << i << endl;

这似乎有效,因为它确实正确插入了元素,但我不明白新对是否按我的预期运行。当我 运行 代码时,我的优先级对的析构函数被调用两次。这在调用函数 PeekTop 时特别发生。一次在队列的生命周期内,一次在队列超出范围并被销毁时。

以上代码的输出如下:

Code: 0
Destroy Pair for object :(string1): with priority :(5):
-------
string1
Destroy Queue with size: 1
Destroy Pair for object :(): with priority :(5):

第一个析构函数调用正确地显示了字符串及其值,但在第二个中我们可以看到字符串本身已经超出范围(这很好并且符合预期)。

除了以下划线开头的名称,正如人们在评论中指出的那样,您的问题似乎在行 return *((PriorityPair<T>)(*queueArray[0]))._Object 中。让我们一点一点地看,从内到外。

queueArrayPriorityPair<T>**,如 PriorityQueue 中声明的那样。这可以读作“指向 PriorityPair<T> 的指针的指针”,但在您的情况下,您的意思是它是“指向 PriorityPair<T> 的指针的原始数组”,这也是一个有效的读数。到目前为止一切顺利。

queueArray[0] 是一个 PriorityPair<T>*&,或“对指向 PriorityPair<T> 的指针的引用”。引用在 C++ 中是完全不可见的,这仅意味着您处理的是数组的实际第一个元素,而不是副本。同样,在试图查看队列顶部时,这是一个合理的要求。

*queueArray[0] 只是一个 PriorityPair<T>&,或“对 PriorityPair<T> 的引用”。同样,此处的引用仅意味着您正在处理 queueArray[0] 指向的实际内容,而不是副本。

(PriorityPair<T>)(*queueArray[0]) 是一个 PriorityPair<T>,是将已有的转换为新的结果。 这会创建一个临时 PriorityPair<T>,也就是您稍后看到的被销毁的 。没有编程理由进行此强制转换(您的 IntelliSense 问题是一个不同的问题,我对 VS 的了解还不够,无法对它们发表评论);它已经是正确的类型。如果将 this 添加到输出中,您可以验证它是一个不同的被销毁的对象,因为 this 是指向当前对象的指针,临时对象需要存在于内存中的其他地方。

((PriorityPair<T>)(*queueArray[0]))._Object 是一个 T* 或“指向 T 的指针”。其实它指向的是为优先级队列的顶部存储的T,这很好。

最后,完整的表达式 *((PriorityPair<T>)(*queueArray[0]))._Object 取消引用它以给出 T,并且 return 语句 return 是 的副本 T。这不会影响您看到的行为,但如果您将析构函数调用添加到您测试的对象,它会。通过将 return 类型从 T 更改为 T& 或 return 对 T 的引用可能会更有效T const&.

我注意到的其他问题,与这个问题无关,你可能会发现在学习 C++ 时有用(不是一个全面的列表;我主要不是在寻找这些):

  • 你的两个构造函数都应该使用 initializer lists 并且有空体(是的,new 表达式可以放在初始化列表中,我想我谈过的每个人都问过这个第一次或假设不正确,包括我)。这会更有效,也更惯用。
  • 您不需要为 PriorityPair 实现析构函数(学习语言的细微差别除外);这就是所谓的普通旧数据 (POD) 类型。如果您希望 PriorityPair 销毁 delete,您将需要 T,但您希望 T 完全分开管理。
  • 正如人们在评论中指出的那样,you aren’t allowed to use those identifier names yourself in case the compiler or standard library want them. This might be fine, it might cause problems for you at compile time, or it might appear to work correctly but send all your users’ browser history and emails to their parents and/or employers. That last is unlikely, but the C++ standard doesn’t forbid it; this is what undefined behavior means. Other allowed behaviors are creating a black hole to destroy the Earth and shooting demons out of your nose,但在实践中这些可能性更小。
  • 认为 你的 PriorityQueue 的析构函数逻辑是正确的,很容易把这种事情搞砸。恭喜!