当我对一个指针进行递增操作并且删除它时,为什么我的程序会崩溃?

30

在我解决一些编程练习时,我意识到我对指针有一个大误解。请问有人能解释一下为什么这段代码会导致C++崩溃吗?

#include <iostream>

int main()
{
    int* someInts = new int[5];

    someInts[0] = 1; 
    someInts[1] = 1;

    std::cout << *someInts;
    someInts++; //This line causes program to crash 

    delete[] someInts;
    return 0;
}

顺便提一下,我知道这里没有必要使用“new”,我只是尽可能地让这个例子变小。


34
你能否解释一下你为什么认为这段代码可以工作,并且你预期它会产生什么结果?也许你认为只删除已分配内存的一部分是可能的?或者你认为调用delete运算符将适用于指向已分配对象内部的任何指针(无论确切位置在哪里)?还是其他原因?请详细说明。 - AnArrayOfFunctions
13
目前为止,每个人都在解释正确的内存语义,但有点晦涩。这很不幸,因为这显然是一个初学者问题,而且OP几乎可以确定地是想要(*someInts)++; - geometrian
9
为什么它不会崩溃? - Lightness Races in Orbit
1
@imallett 我认为你可能是对的。不过我当时确实没有那么理解这个问题!如果提问者能够解释一下程序的尝试做什么,那就好了…… - Ben
5
实际上,他们的代码是一个适当的MCVE,并展示了确切的问题,而带有注释的那行代码恰好是导致崩溃的代码行。(但正如你指出的那样,不是它崩溃的位置。) delete[] 期望的是从相应的数组 new 获取的指针。递增从数组 new 中获得的指针,然后将结果指针传递给 delete[] 是无效的并且会导致UB。正如他们正确指出的那样,someInts++; 是问题所在。 - Justin Time - Reinstate Monica
显示剩余12条评论
4个回答

80

引起程序崩溃的语句实际上是在您标记为引起程序崩溃之后的那个语句!

您必须将与从new[]获得的指针相同的指针传递给delete[]

否则程序的行为是未定义的


之前的评论提到了C标准,但它们已被删除。 - Bathsheba
是的,我的回复是针对一条已删除的评论,该评论基本上说“C可以,为什么C++不行?”如果您希望,我现在可以删除这些评论。 - IanM_Matrix1
1
在C++中,不仅地址必须相同,而且指针类型必须相同(或者是具有虚析构函数的基类)。 - Phil1970
14
对于数组删除(delete[]),必须使用完全相同的指针类型,不允许多态性。 - Ben Voigt
为了参考,大多数编译器在您使用new分配的数据前面存储一些数据,并在delete中使用该数据来正确管理内存。当您增加指针然后删除它时,编译器开始在错误的位置查找其簿记数据。它使用在那里找到的任何数据,这导致了段错误。 - Cort Ammon
“delete[]”语句是程序崩溃的位置,但“someInts++”语句是导致崩溃的语句。 - stannius

33
问题是,使用 someInts++; 会将数组第二个元素的地址传递给 delete[] 语句。你需要传递第一个(原始)元素的地址:
int* someInts = new int[5];
int* originalInts = someInts; // points to the first element
someInts[0] = 1;
someInts[1] = 1;

std::cout << *someInts;
someInts++; // points at the second element now

delete[] originalInts;

19
不涉及特定实现的细节,可以通过考虑 delete[] 的作用来简单说明崩溃背后的直观原因:
销毁由 new[] 表达式创建的数组。您向 delete[] 提供数组指针。它必须释放分配给该数组内容的内存,除了其他事项之外。如何使分配器知道要释放什么?它使用您提供的指针作为键来查找包含已分配块的簿记信息的数据结构。在某个地方,有一个结构存储先前分配的块的指针与相关簿记操作之间的映射。
如果您希望此查找导致某种友好的错误消息(如果传递给 delete[] 的指针不是对应的 new[] 返回的指针),那么这是可能的,但标准中没有任何保证。 因此,如果给 delete[] 的指针之前没有被 new[] 分配,则有可能会出现问题,因为 delete[] 最终查看的是不一致的簿记结构。 线路交叉。 崩溃随之而来。
或者,您可能希望 delete[] 会说“嘿,看起来这个指针指向我之前分配的区域内部。 让我回去找到我分配该区域时返回的指针,并使用它来查找簿记信息”,但是,标准中没有这样的要求:
对于第二个(数组)形式,表达式必须是空指针值或先前通过new-expression的数组形式获得的指针值。 如果表达式是其他任何东西,包括通过非数组形式的new-expression获得的指针,则行为是未定义的。 在这种情况下,您很幸运因为您立即发现了自己做错了什么。

注:这只是一个简单的解释


2
有时候用形象比喻解释并没有错。其他的回答都很简洁,但是看完这个(和@plugwash的)我终于懂得为什么会发生了。 - decvalts
通常,分配的信息不会存储在单独的数据结构中,而是直接存储在分配的内存之前。 - plugwash
当事情出错时,这非常重要。实际查找可能会返回“未找到”错误。简单的减法只会尝试访问用户指针下方的内存中的任何内容。 - plugwash
如果您传递给 delete [] 的指针不是由相应的 new[] 返回的,您可能希望此查找结果显示某种友好的错误消息,但标准中没有任何保证。 - Sinan Ünür

8
你可以在块内增加指针并使用该增加后的指针来访问块中的不同部分,这是可以的。但是你必须将从New获取的指针传递给Delete。不是它的增加版本,也不是通过其他方式分配的指针。
为什么?好吧,推脱的答案是因为标准就是这样规定的。
实际的答案是因为要释放一个内存块,内存管理器需要关于该块的信息。例如,它开始和结束的位置,相邻的块是否空闲(通常,内存管理器会合并相邻的空闲块),以及它属于哪个区域(对于多线程内存管理器中的锁定很重要)。
这些信息通常存储在已分配内存的前面。内存管理器将从你的指针中减去一个固定值,并查找该位置的分配元数据结构。
如果你传递的指针不是指向已分配内存块的开头,则内存管理器试图执行减法并读取其控制块,但它最终读取的不是有效的控制块。
如果你运气好,代码会很快崩溃,如果你运气不好,则可能导致微妙的内存损坏。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接