我观看了CppCons 您的代码能否经受住指针攻击的考验?,关于指针的生命周期我有些困惑,希望能得到一些澄清。
首先,这里有一些基本理解,请纠正我如果有任何错误:
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
我对第二个问题感到困惑。显然它们无法被解引用,但为什么它们的值不能被使用(例如,将一个与另一个进行比较,即使它们是不相关和有效的指针?)这在25:38左右说明了。我在cppreference上找不到任何关于此的内容,而我从那里得到了第三个问题。
注意:不能将返回相同内存的假设概括为可能或不可能发生。对于这个例子,应该认为它“随机”返回了与视频示例中相同的内存,并且(也许?)需要下面的代码才能破坏。
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 成员或者引用,根据下面的规则:
如果在原来的对象所占用的地址上创建了一个新对象,则所有指针、引用和原始对象的名称都会自动引用新对象,并且一旦新对象的生命周期开始,它们就可以用于操作新对象,但前提是满足以下条件[它们被满足了]。
那么为什么这是一种僵尸指针并且被认为是未定义行为呢?
如果有 const 成员,那么上述简化代码(单线程)是否可以通过 newnodeC->next = std::launder(newnodeC->next)
来修复问题呢?如下所示:
如果不满足上述列出的条件,则仍然可以通过应用指针优化障碍 std::launder 来获得对新对象的有效指针
我希望这可以修复 "zombie pointer",编译器不会发出指令进行赋值,而只是将其视为优化障碍(例如当函数内联并再次访问 const 成员时)
总之,我以前没有听说过“僵尸指针”。 我正确地认为,除非重新分配指向销毁/删除对象的任何指针(用于读取[指针值]和取消引用[读取指针所指的内容])或者使用相同对象类型重新创建内存(并且没有const /引用成员),否则不能使用它们吗?这不是可以通过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)
。同时注意缺陷报告260:
我认为这是最终的解释:经过当指向的对象到达其生命周期的末尾时,指针值变得不确定时,所有有效类型为指针且指向同一对象的对象都会获得不确定的值。因此,在X点的p,以及在Z点的p,q和r都可以改变它们的值。
delete globalPtr
之后,ptr
的值也是不确定的。但是这如何与...相符呢?如果在原本已被占用的地址上创建了一个新对象,那么原对象的所有指针[...]将自动引用新对象,并且[...]可以用于操作新对象。
if(p==q)
两个指针再次有效,可以进行解引用,对吗?” - Ted Lyngmoif
条件成立。对于简单地查看是否可以使用最近释放的内存块来处理请求的分配器而言,这很容易发生。关于if(p==q)
:问题是:我可以这样做吗?由p
指向的对象已被删除,因此p
是“僵尸指针”。我真的建议(至少)看视频的前半部分。非常有趣 :) - Flamefireq
可引用,但不是p
。 - HolyBlackCatnew
返回新创建对象的地址,这个地址不保证与之前被删除的对象相同。所以在第二次new
之后,p
和q
不能被认为指向内存中的同一位置。关于更多解释,请参见我的答案。 - Fareanor