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

Unexpected destructor call when returning object from a pointer

本文关键字:意外 析构 函数调用 指针 返回 对象      更新时间:2023-10-16

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

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

队列实现中的重要部分:

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;

优先级对的结构:

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

此外,下面是插入操作(最低效率插入,不执行堆/队列操作):

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中。让我们从内部一点一点地看一下,然后努力走出去。

queueArray是一个PriorityPair<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取消引用 this 以给出一个T,return 语句返回该T的副本。这不会影响您看到的行为,但如果将析构函数调用添加到测试对象,则会受到影响。通过将返回类型从T更改为T&T const&来返回对T的引用可能会更有效,这将放弃复制。

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

  • 您的两个构造函数都应该使用初始值设定项列表并具有空体(是的,new表达式可以进入初始值设定项列表,我认为实际上与我交谈过的每个人都是第一次问这个问题或假设不正确,包括我)。这将更有效率,更惯用。
  • 你不需要为PriorityPair实现析构函数(除了学习语言的细微差别);这就是所谓的普通旧数据(POD)类型。如果你想让PriorityPair的销毁deleteT,你需要它,但你希望T完全分开管理。
  • 正如人们在评论中指出的那样,如果编译器或标准库需要它们,则不允许自己使用这些标识符名称。这可能很好,它可能会在编译时给您带来问题,或者它可能看起来工作正常,但会将所有用户的浏览器历史记录和电子邮件发送给他们的父母和/或雇主。最后一种不太可能,但C++标准并不禁止;这就是未定义行为的含义。其他允许的行为是制造黑洞来摧毁地球,并从你的鼻子里射出恶魔,但这些在实践中更不可能。
  • 我认为就像你有PriorityQueue正确的析构函数逻辑一样,很容易把这种事情搞砸。祝贺!