【发布时间】: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之后,p和q不能被认为指向内存中的同一位置。有关更多解释,请参阅我的答案。
标签: c++ pointers dynamic-memory-allocation lifetime object-lifetime