【问题标题】:Understand C++ pointer lifetime / zombie pointers了解 C++ 指针生命周期/僵尸指针
【发布时间】:2020-02-13 07:03:39
【问题描述】:

看了 CppCons Will Your Code Survive the Attack of the Zombie Pointers? 之后,我对指针的生命周期有点困惑,需要澄清一下。

首先是一些基本的了解。如果有任何错误,请纠正我:

int* p = new int(1);
int* q = p;
// 1) p and q are valid and one can do *p, *q
delete q;
// 2) both are invalid and cannot be dereferenced. Also the value of both is unspecified
q = new int(42); // assume it returns the same memory
// 3) Both pointers are valid again and can be dereferenced

我对 2) 感到困惑。显然它们不能被取消引用,但为什么不能使用它们的值(例如,将一个与另一个进行比较,甚至是不相关且有效的指针?)这在25:38 附近有所说明。我在 cppreference 上找不到任何关于此的信息,这是我从那里获得 3) 的。

注意:返回相同内存的假设不能一概而论,因为它可能会发生也可能不会发生。对于此示例,应该认为它“随机”返回与视频示例中的情况相同的内存,并且(也许?)以下代码中断所需的内存。

LIFO 列表中的多线程示例代码可以在单个线程中进行模拟,如下所示:

Node* top = ... //NodeA allocated before;
Node* newnodeC = new Node(v); 
newnodeC->next = top;
delete top; top = nullptr;
// newnodeC->next is a zombie pointer
Node* newnodeD = new Node(u); // assume same memory as NodeA is returned
top = newnodeD;
if(top == newnodeC->next) // true
  top = newnodeC;
// Now top->next is (still) a zombie pointer

这应该是有效的,除非 Node 包含非静态 const 成员或根据

下的规则的引用

如果在另一个对象占用的地址处创建了一个新对象,那么原始对象的所有指针、引用和名称都将自动引用新对象,并且一旦新对象的生命周期开始,可用于操作新对象,但前提是满足以下条件[它们是]

那么为什么这是一个僵尸指针并且据说是 UB?

上面的(单线程)压缩代码是否可以通过newnodeC->next = std::launder(newnodeC->next) 来修复(如果有 const 成员)

如果不满足上面列出的条件,仍然可以通过应用指针优化屏障std::launder获得指向新对象的有效指针

我希望这可以修复“僵尸指针”,并且编译器不会发出赋值指令,而只是将其视为优化障碍(例如,当函数被内联并且再次访问 const 成员时)

总之:我以前没有听说过“僵尸指针”。我是否正确,除非重新分配指针或使用重新创建的相同对象类型重新分配内存(和没有 const/reference 成员)?这不能由 C++17 std::launder 修复吗? (排除多线程问题)

另外:在第一个代码中的3) if(p==q) 甚至通常有效吗?因为根据我对视频(第二部分)的理解,阅读p 是无效的。

编辑:作为我很确定 UB 发生的解释:再次假设纯偶然地与 new 返回相同的内存:

// Global
struct Node{
  const int value;
};
Node* globalPtr = nullptr;
// In some func
Node* ptr = new Node{42};
globalPtr = ptr;
const int value = ptr->value;
foo(value);
// Possibly on another thread (if multi-threaded assume proper synchronisation so that this scenario happens)
delete globalPtr;
globalPtr = new Node{1337}; // Assume same memory returned
// First thread again (and maybe on serial code too)
if(ptr == globalPtr)
  foo(ptr->value);
else
  foo(globalPtr->value);

根据视频,delete globalPtr 之后的ptr 也是“僵尸指针”,不能使用(又名“将是 UB”)。一个充分优化的编译器可以利用这一点并假设指针从未被释放(尤其是当删除/新建发生在另一个函数/线程/...上时)并将foo(ptr->value)优化为foo(42)

还要注意提到的Defect Report 260

如果指针值变得不确定,因为指向的对象已达到其生命周期的末尾,则有效类型为指针且指向同一对象的所有对象都将获得不确定值。因此,在 X 点的 p 和在 Z 点的 p、q 和 r 都可以改变它们的值。

我认为这是明确的解释:delete globalPtr 之后,ptr 的值也是不确定的。但这怎么能与

如果在另一个对象占用的地址处创建了一个新对象,那么原始对象的所有指针 [...] 将自动指向新对象,并且 [...] 可用于操作新对象

【问题讨论】:

  • "假设它返回相同的内存" - 您可以添加:"3) if(p==q) 两个指针都再次有效并且可以取消引用,对吧?"
  • 视频示例中的假设是,返回相同的指针值也就是相同的内存,因此if 成功。对于仅查看它们是否可以使用最近释放的内存块来处理请求的分配器来说,很容易发生这种情况。在if(p==q):重点是:我可以这样做吗? p指向的对象被删除了,所以p是一个“僵尸指针”。我真的推荐(至少)视频的第一部分。很有趣:)
  • (3) 是错误的。它使q 可以解除引用,但不能使p
  • 为什么? “在 [那个] 地址 [...] 处创建了一个新对象,因此原始对象的所有指针 [...] 将自动引用新对象”,请参见上面的链接和引用。
  • @Flamefire 因为new返回的是新创建的对象的地址,不保证和之前删除的一样。所以在你的第二个new 之后,pq 不能被认为指向内存中的同一位置。有关更多解释,请参阅我的答案。

标签: c++ pointers dynamic-memory-allocation lifetime object-lifetime


【解决方案1】:

删除的指针值具有无效的指针值,而不是未指定的值。

从 C++17 开始,无效指针值的行为在 [basic.stc]/4 中定义:

通过无效指针值的间接传递以及将无效指针值传递给释放函数具有未定义的行为。对无效指针值的任何其他使用都具有实现定义的行为。

因此,尝试比较两个无效指针具有实现定义的行为。有一个脚注说明此行为可能包含运行时错误。

在你的第一个 sn -p p 有一个无效的指针值后删除;您对q 所做的任何事情都与此无关。无效的指针值不能神奇地再次变为有效。没有办法(在标准 C++ 范围内)确定新分配是否与先前分配位于“相同位置”。

std::launder 无济于事;再次使用无效的指针值并因此触发实现定义的行为。

也许您可以查阅实现的文档并查看它定义此行为的内容。

在您的问题中,您提到了 C DR 260,但这无关紧要。 C 是与 C++ 不同的语言。在 C++ 中,删除的指针具有无效的指针值,而不是不确定的值

【讨论】:

  • 好的,所以正确的措辞是“无效的指针值”而不是“未指定的值”。但是,由于任何用途都是 UB 或实现定义的,因此这种差异并不重要。 (或者是否有任何地方指定“无效指针值”是哪个值?)There is no way to determine whether a new allocation is at the "same location" 哎哟,这太疯狂了。现在,对对象(指针)的 副本 的操作会对对象/值的另一个副本产生副作用,可能使其无法使用。尝试向初学者/中级程序员解释这一点。这就像`a = b; b -= c; //a 可能无效
  • @Flamefire“无效指针值”是“哪个值”。 UB 和实现定义的行为之间的区别是显着的。 C++ 的奥秘可能很深,尽管在这种情况下可能不是。如果我复制一个文件句柄并关闭它,原来的句柄不再有用是不是很神秘?例如。该原始句柄可能与我们打开不同文件时获得的新句柄匹配。
  • 想象一个系统,其中硬件根据 MMU 的活动分配列表自动检查任何指针寄存器
  • 很容易理解无效的句柄/指针不能被取消引用,但是它的值不能再被使用是困难的。文件句柄类比很棒。我无法提出一个用例,您希望保留可能被其他人关闭的文件句柄,即使它可以重新打开并指向(另一个或相同的)文件。 any pointer register against the MMU's list of active allocations 分配可能再次处于活动状态。它只是包含其他内容。
【解决方案2】:

我不同意这个:

// 3) Both pointers are valid again and can be dereferenced

事实上,你被愚弄了,因为在第二个new,程序通常会在它刚刚可用时重新分配相同的内存块,但不能依赖于此(不能保证相同的内存块会被重用)。

例如,如果您使用将已删除指针设置为nullptr 的良好做法,则以下程序:

int main()
{
    int * p = new int(1);
    int * q = p;

    std::cout << (p == q) << std::endl;

    delete q;
    q = nullptr;
    p = nullptr;

    q = new int(42);

    std::cout << (p == q) << std::endl;

    delete q;

    return 0;
}

会导致:

1
0

【讨论】:

  • 问题中没有澄清,但在 cmets 中明确表示假设new 在两种情况下都返回相同的指针。
  • it is not guaranteed that the same memory block will be reused,对。这是一个假设在这种情况下使用相同的内存块的示例,就像在视频中一样。我将更新问题以明确这一点。
  • @MaxLanghof 啊好吧,我的错,我确实不明白。
【解决方案3】:

从问题中描述的设计开始,一个已经分配的int* p 和:

int* q = p;
q = new int(42); // assume it returns the same memory

我们现在有一个与此相同的场景:

int* p = new int(42);
int* q = p;

因为它满足了我们可以假设pq指向同一个内存位置的前提条件;在这种情况下,这个位置被分配了一些东西然后被删除然后再次分配的事实并不重要。在此之前发生的任何事情都无关紧要,因为我们假设这两个指针处于与刚刚描述的状态相同的状态。

关于步骤#2中的“两者的值未指定”,我会说q的值在这一点上是未指定的,因为它被传递给delete,但p的值是不变。

这里delete的行为在C++14下其实并不是undefined,是implementation defined;来自delete的一些文档:

以这种方式无效的指针的任何使用,甚至将指针值复制到另一个变量,都是未定义的行为。 (C++14 前)

通过以这种方式变为无效的指针的间接传递并将其传递给释放函数(双重删除)是未定义的行为。任何其他用途都是实现定义的。 (C++14 起)

https://en.cppreference.com/w/cpp/memory/new/operator_delete

所以,要回答我认为是你的问题,在这种情况下,不,不是僵尸指针。

那么为什么这是一个僵尸指针并且据说是 UB?

事实并非如此,因此导致您得出该结论的是误解或错误信息。

【讨论】:

  • 我认为您认为他们“依赖”任何功能。问题仅仅是“如果碰巧返回相同的虚拟地址怎么办”。这被表述为“假设它返回相同的记忆”——意思是“为了讨论的目的,请考虑在这种情况下会发生什么”。发帖人明确认识到这并不意味着我们通常可以假设将返回相同的虚拟地址。这就像说“掷硬币并假设它是正面”。我们被要求探讨 正面 的情况,而不是假设硬币总是会出现正面。
  • 对,那(heads case)是这里答案的后半部分,它断言两个这样的指针是如何产生的并不重要,因为它们与非常规范的东西相同。跨度>
  • > 不是,所以导致你得出这个结论的都是误解或错误信息。这源于链接的视频,例如第一部分。分配恰好返回相同的指针,因此之前获得的指针的副本仍将被使用,并且被称为“僵尸指针”,因为它的指针已被删除。我认为对于指针对象的常量成员,这实际上是 UB:编译器可能会“缓存”常量成员的值,但新分配的实例可能在那里有另一个值。我会把它放到问题中
  • 你的第一个 sn-p 没有意义,因为 p 没有定义。您还谈论p2,但在任何地方都没有定义。
  • "Starting with a contrivance..."pq 来自最初(和当前)编写的问题。这个问题的发明是“编译器做到了”。无论他们是根据明确的语言法则(如上面的第二种情况)还是通过问题中断言的偶然事件来获得这种方式都没有关系。它们是指向相同内存位置的相同类型的两个指针。我想然后将它们称为pp2 有点令人困惑,我会改变它(谢谢)。
猜你喜欢
  • 2021-05-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-12
  • 2016-12-07
  • 2012-09-13
相关资源
最近更新 更多