警告!
以下是分段错误的潜在原因。 几乎不可能列出所有原因。此列表的目的是帮助诊断现有的段错误。
分段错误和未定义行为之间的关系怎么强调都不过分!以下所有可能导致分段错误的情况在技术上都是未定义的行为。这意味着他们可以做任何事情,而不仅仅是分段错误——正如有人曾经在 USENET 上所说,“@987654321 @"。当您有未定义的行为时,不要指望会发生段错误。您应该了解 C 和/或 C++ 中存在哪些未定义行为,并避免编写包含这些行为的代码!
关于未定义行为的更多信息:
什么是段错误?
简而言之,当代码尝试访问它无权访问的内存时,就会导致分段错误。每个程序都有一块内存 (RAM) 可以使用,出于安全原因,它只允许访问该块中的内存。
有关什么是分段错误的更全面的技术解释,请参阅What is a segmentation fault?。
以下是导致分段错误错误的最常见原因。同样,这些应该用于诊断现有的段错误。要了解如何避免它们,请了解您的语言的未定义行为。
此列表也不能替代您自己的调试工作。 (请参阅答案底部的那部分。)这些是您可以寻找的东西,但您的调试工具是解决问题的唯一可靠方法。
访问 NULL 或未初始化的指针
如果您的指针为 NULL (ptr=0) 或完全未初始化(尚未设置为任何内容),则尝试使用该指针进行访问或修改具有未定义的行为。
int* ptr = 0;
*ptr += 5;
由于分配失败(例如使用malloc 或new)将返回一个空指针,因此在使用它之前,您应该始终检查您的指针不为NULL。
还要注意,即使 读取 未初始化指针(以及一般变量)的值(不取消引用)也是未定义的行为。
有时这种对未定义指针的访问可能非常微妙,例如尝试将此类指针解释为 C 打印语句中的字符串。
char* ptr;
sprintf(id, "%s", ptr);
另见:
访问悬空指针
如果您使用malloc 或new 分配内存,然后通过指针使用free 或delete 分配内存,则该指针现在被视为悬空指针。取消引用它(以及简单地读取它的值 - 假设您没有为其分配一些新值,例如 NULL)是未定义的行为,并且可能导致分段错误。
Something* ptr = new Something(123, 456);
delete ptr;
std::cout << ptr->foo << std::endl;
另见:
堆栈溢出
[不,不是您现在所在的网站,命名是为了什么。] 过于简单化了,“堆栈”就像您在某些食客中粘贴订单纸的尖刺。可以这么说,当您在该峰值上放置太多订单时,可能会出现此问题。在计算机中,任何非动态分配的变量和任何尚未被 CPU 处理的命令都会进入堆栈。
造成这种情况的一个原因可能是深度递归或无限递归,例如当函数调用自身而无法停止时。因为该堆栈已溢出,订单文件开始“脱落”并占用其他不适合它们的空间。因此,我们可以得到分段错误。另一个原因可能是尝试初始化一个非常大的数组:它只是一个订单,但它本身已经足够大了。
int stupidFunction(int n)
{
return stupidFunction(n);
}
堆栈溢出的另一个原因是一次有太多(非动态分配的)变量。
int stupidArray[600851475143];
堆栈溢出的一个案例来自于在一个条件中简单地省略了return 语句,以防止函数中的无限递归。该故事的寓意是,始终确保您的错误检查有效!
另见:
野指针
在内存中创建指向某个随机位置的指针就像用您的代码玩俄罗斯轮盘赌一样 - 您很容易错过并创建指向您无权访问的位置的指针。
int n = 123;
int* ptr = (&n + 0xDEADBEEF); //This is just stupid, people.
作为一般规则,不要创建指向文字内存位置的指针。即使他们一次工作,下一次他们可能不会。在任何给定的执行过程中,您都无法预测程序的内存位置。
另见:
试图读取数组的末尾
数组是一个连续的内存区域,其中每个连续的元素都位于内存中的下一个地址。但是,大多数数组对于它们有多大或最后一个元素是什么并没有天生的感觉。因此,很容易越过数组的末尾而永远不知道它,尤其是在使用指针算法时。
如果您读到数组末尾之后,您可能会进入未初始化或属于其他内容的内存。这在技术上是未定义的行为。段错误只是许多潜在的未定义行为之一。 [坦率地说,如果你在这里遇到段错误,你很幸运。其他的则更难诊断。]
// like most UB, this code is a total crapshoot.
int arr[3] {5, 151, 478};
int i = 0;
while(arr[i] != 16)
{
std::cout << arr[i] << std::endl;
i++;
}
或者常见的使用for 和<= 而不是<(读取1 个字节太多):
char arr[10];
for (int i = 0; i<=10; i++)
{
std::cout << arr[i] << std::endl;
}
甚至是一个不幸的错字,它编译得很好(见here)并且只分配了一个用dim而不是dim元素初始化的元素。
int* my_array = new int(dim);
另外应该注意的是,您甚至不允许创建(更不用说取消引用)指向数组外部的指针(只有当它指向数组内的元素或超过结尾)。否则,您将触发未定义的行为。
另见:
忘记 C 字符串上的 NUL 终止符。
C 字符串本身就是具有一些附加行为的数组。它们必须以 null 结尾,这意味着它们末尾有一个 \0,以便可靠地用作字符串。这在某些情况下会自动完成,而在其他情况下则不会。
如果忘记了这一点,一些处理 C 字符串的函数永远不知道何时停止,并且您可能会遇到与读取数组末尾相同的问题。
char str[3] = {'f', 'o', 'o'};
int i = 0;
while(str[i] != '\0')
{
std::cout << str[i] << std::endl;
i++;
}
对于 C 字符串,\0 是否会产生任何影响确实是偶然的。你应该假设它会避免未定义的行为:所以最好写char str[4] = {'f', 'o', 'o', '\0'};
尝试修改字符串文字
如果将字符串文字分配给 char*,则无法修改它。比如……
char* foo = "Hello, world!"
foo[7] = 'W';
...触发未定义的行为,并且分段错误是一种可能的结果。
另见:
不匹配的分配和释放方法
您必须同时使用malloc 和free,同时使用new 和delete,以及同时使用new[] 和delete[]。如果你把它们混在一起,你可能会出现段错误和其他奇怪的行为。
另见:
工具链中的错误。
编译器的机器代码后端中的错误非常有能力将有效代码转换为段错误的可执行文件。链接器中的错误肯定也可以做到这一点。
特别可怕的是这不是你自己的代码调用的UB。
也就是说,在被证明并非如此之前,您应该始终假设问题出在您身上。
其他原因
分段错误的可能原因与未定义行为的数量一样多,甚至标准文档都无法列出。
几个不太常见的检查原因:
调试中
首先,仔细阅读代码。大多数错误只是由拼写错误或错误引起的。确保检查分段错误的所有潜在原因。如果失败,您可能需要使用专用调试工具来找出根本问题。
调试工具有助于诊断段错误的原因。使用调试标志 (-g) 编译您的程序,然后使用您的调试器运行它以查找可能发生段错误的位置。
最近的编译器支持使用-fsanitize=address 构建,这通常会导致程序运行速度慢约 2 倍,但可以更准确地检测地址错误。但其他错误(如从未初始化的内存读取或泄漏文件描述符等非内存资源)不支持此方法,并且无法同时使用多个调试工具和ASan。
一些内存调试器
- GDB | Mac、Linux
- valgrind(内存检查)| Linux
- 博士。记忆 |窗户
此外,建议使用静态分析工具来检测未定义的行为 - 但同样,它们只是帮助您找到未定义行为的工具,它们并不能保证找到所有出现的未定义行为。
但是,如果您真的很不走运,使用调试器(或者,更罕见的是,仅使用调试信息重新编译)可能会充分影响程序的代码和内存,从而不再发生段错误,这种现象称为 heisenbug。
在这种情况下,您可能想要做的是获取核心转储,并使用调试器获取回溯。