我知道段错误是什么,但是在不知道它们通常是什么样的情况下很难在代码中发现。毫无疑问,尽管有太多详尽的 list 无法列出,但是在C和C++中最常见的段错误原因是什么?
最佳答案
什么是段故障?
简而言之,当代码尝试访问它的无权访问的内存时,会引起段错误。每个程序都有一块可使用的内存(RAM),并且出于安全原因,只允许访问该块中的内存。
有关段错误的更详尽的技术说明,请参见What is a segmentation fault?。
这是段错误错误的最常见原因。同样,这些应用于诊断现有的段故障。若要了解如何避免它们,请学习您语言未定义的行为。
此列表也不能替代您自己的调试工作。 (请参阅答案底部的那部分。)您可以寻找这些内容,但是调试工具是唯一解决问题的可靠方法。
访问NULL或未初始化的指针
如果您使用的指针为NULL(ptr=0
)或完全未初始化(尚未设置为任何指针),则尝试使用该指针进行访问或修改具有未定义的行为。
int* ptr = 0;
*ptr += 5;
由于分配失败(例如
malloc
或new
)将返回空指针,因此在使用它之前,应始终检查指针是否为NULL。还要注意,即使读取未初始化的指针(通常不包括变量)的值(不进行取消引用)也是未定义的行为。
有时,对未定义指针的这种访问可能非常微妙,例如试图在C print语句中将此类指针解释为字符串。
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
和<=
而不是<
的情况(读取的字节数过多):char arr[10];
for (int i = 0; i<=10; i++)
{
std::cout << arr[i] << std::endl;
}
甚至是一个不幸的错字,它会编译良好(见here),并且仅分配1个使用
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。一些内存调试器
另外,建议使用静态分析工具来检测未定义的行为-但是,它们再次只是为了帮助您发现未定义的行为,并且不能保证找到所有出现的未定义的行为的工具。
但是,如果您真的很倒霉,那么使用调试器(或更罕见的是,仅使用调试信息重新编译)可能会充分影响程序的代码和内存,从而不再发生段错误,这种现象称为heisenbug。
在这种情况下,您可能要做的是获取核心转储,并使用调试器获得回溯。