【问题标题】:Unexpected destructor call when returning object from a pointer从指针返回对象时意外的析构函数调用
【发布时间】:2017-08-06 21:26:07
【问题描述】:

我正在尝试将我在 C# 到 C++ 中制作的优先级队列实现重新创建为一个可以跳入 C++ 的项目,但是很多细微差别都让我感到困惑。队列被设计为一个模板,可以在任何给定的类 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; 
    }

另外,这里是插入操作(最低有效的插入,没有堆/队列操作):

    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):

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

【问题讨论】:

  • 以下划线大写开头的名称保留用于实现;你的程序有未定义的行为。
  • 这个_Object;,以及所有以下划线和大写字符开头的标识符,或者包含两个连续下划线的标识符,在C++中被保留用于实现。您不得自己创建此类名称。像_Object 这样的名称很可能会被实现使用。
  • 请提供minimal reproducible example。 queue.InsertElement 是什么?另外,我没有看到任何 deletes 正在完成,它们是否被删除以创建更短的示例?
  • @Caboose:不遵守规则的奇妙之处在于,你不一定会被告知你的错误,直到也许你已经出货了 100 万件并且诉讼开始进入。从这个意义上说,编程 C++ 比在学校参加数学考试要难得多:-(
  • 为什么要加入*((PriorityPair&lt;T&gt;)(*queueArray[0]))._Object?您的queueArray 已经包含PriorityPair&lt;T&gt; 类型的元素。由于我不知道它发生的确切原因,我不会发布答案,但演员本身会创建一个临时的。此外,由于缺少 IsEmpty、maxSize 和 currentHeapSize,您的示例仍未完成,但这些添加很简单。

标签: c++ pointers return destructor


【解决方案1】:

除了以下划线开头的名称,正如人们在 cmets 中指出的那样,您的问题似乎在 return *((PriorityPair&lt;T&gt;)(*queueArray[0]))._Object 行中。让我们从内到外逐一分析。

queueArray 是一个PriorityPair&lt;T&gt;**,正如PriorityQueue 中所声明的那样。这可以读作“指向PriorityPair&lt;T&gt; 的指针的指针”,但在你的情况下,它看起来像是“指向PriorityPair&lt;T&gt; 的指针的原始数组”,这也是一个有效的读法。到目前为止一切顺利。

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

*queueArray[0] 只是一个PriorityPair&lt;T&gt;&amp;,或“对PriorityPair&lt;T&gt; 的引用”。同样,这里的引用只是意味着您正在处理queueArray[0] 指向的实际事物,而不是副本。

(PriorityPair&lt;T&gt;)(*queueArray[0]) 是一个PriorityPair&lt;T&gt;,将你已经拥有的一个转换为一个新的结果。 这会创建一个临时的PriorityPair&lt;T&gt;,您稍后会看到它被销毁。没有编程原因进行此转换(您的 IntelliSense 问题是一个不同的问题,我对 VS 了解不多,无法评论它们);它已经是正确的类型。如果您将this 添加到输出中,您可以验证它是另一个被销毁的对象,因为this 是指向当前对象的指针,并且临时对象需要存在于内存中的其他位置。

((PriorityPair&lt;T&gt;)(*queueArray[0]))._Object 是一个T*,或“指向T”的指针。实际上,它指向了为优先级队列顶部存储的T,这很好。

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

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

  • 你的两个构造函数都应该使用 initializer lists 并且有空的主体(是的,new 表达式可以进入初始化列表,我想我所谈过的每个人都是第一次问这个问题或者假设不正确,包括我)。这会更有效率,也更惯用。
  • 您不需要为PriorityPair 实现析构函数(学习语言的细微差别除外);这就是所谓的普通旧数据 (POD) 类型。如果您希望 PriorityPair 将 delete 和 Ts 销毁,您将需要它,但您希望 Ts 完全单独管理。
  • 正如人们在 cmets 中指出的那样,you aren’t allowed to use those identifier names yourself 以防编译器或标准库需要它们。这可能没问题,它可能在编译时给您带来问题,或者它可能看起来工作正常但发送所有用户的浏览器历史和电子邮件给他们的父母和/或雇主。最后一个不太可能,但 C++ 标准并没有禁止它;这就是未定义行为的含义。其他允许的行为包括制造黑洞来摧毁地球和shooting demons out of your nose,但在实践中这些可能性更小。
  • 我认为你有 PriorityQueue 的析构逻辑是正确的,而且很容易把这种事情搞砸。恭喜!

【讨论】:

  • 另外值得注意的是,当前的实现仅在nullptr 可转换为您的T 时才有效。如果您希望选择为空队列返回某些内容,则仅返回 T*(可能是 nullptr)会更有意义。
  • 谢谢,这正是我希望看到的响应类型。我的问题的很大一部分似乎只是 Intellisense,但结果我了解了这样的投射的含义。取而代之的是返回 T& 也很有帮助,并且更接近我的意图。你能评论一下为什么使用初始化列表更好吗?我天真的猜测是它们的行为与我当前的实现没有什么不同。
  • 好故障。在当前状态下无法真正回答问题,但值得投票。
  • @Useless 我其实对此很好奇。当返回 null 时,C# 中的行为定义明确且易于理解/处理,但这里就不那么简单了。在处理其他更紧迫的问题之前,我有点忽略了这一点。
  • 初始化器列表是最初构造成员和基类的地方。在正文中,您正在分配成员,但它们已经构建;旧值被覆盖/删除/在operator= 中完成的任何其他操作。对于 POD 类型,默认初始化什么都不做,它们几乎是等价的,但并非所有类型都如此。如果您有一个类型,其中默认初始化会做一些昂贵的事情,并且当您在主体中分配时,工作将被丢弃,这会产生很大的不同。
猜你喜欢
  • 1970-01-01
  • 2015-05-02
  • 2023-03-31
  • 1970-01-01
  • 2021-07-25
  • 2015-12-14
  • 1970-01-01
  • 2021-02-03
  • 2021-10-10
相关资源
最近更新 更多