世界焦点!C程序中常见的与内存相关的错误

2023-06-14 17:16:48 来源:STM32嵌入式开发

C语言入门程序员来说,管理和使用虚拟存储器可能是个困难的,容易出错的任务。与存储器有关的错误属于那些最令人惊恐的错误,因为它们经常在时间和空间上,都在距错误源一段距离之后,才表现出来。将错误的数据编写到错误的位置,你的程序可能在最终失败之前运行了好几个小时,且使程序中止的位置距离错误的位置已经很远了。


(相关资料图)

1、间接引用坏指针

在进程的虚拟地址空间中有较大的漏洞,没有映射到任何有意义的数据。如果我们试图间接引用一个指向这些洞的指针,那么操作系统就会以段异常终止我们的程序。而且,虚拟存储器的某些区域是只读的。试图写这些区域将造成以保护异常终止这个程序。

间接引用坏指针的一个常见示例是经典的scanf错误。假设我们想要使用scanf从stdin读一个整数到变量。做这件事情正确的方法是传递给scanf一个格式串和变量的地址:

然而,对于c语言程序员初学者而言,很容易传递val的内容,而不是它的地址:

在这种情况下,scanf将把val的内容解释为一个地址,并试图将一个字写到这个位置。在最好的情况下,程序立即以异常终止。在最糟糕的情况下,val的内容对应于虚拟存储器的某个合法的读/写区域,于是我们就覆盖了存储器,这通常会在相当以后造成灾难性的、令人困惑的后果。

2、读未初始化的存储器

虽然.bss存储器位置(诸如未初始化的全局C变量)总是被加载器初始化为零,但是对于堆存储器却并不是这样的。一个常见的错误就是假设堆存储器被初始化为零:

在这个示例中,程序员不正确地假设向量y被初始化为零。正确的实现方式是在for循环时将y[i]设置为零,或使用calloc。

3、允许栈缓冲区溢出

如果一个程序不检查输入串的大小就写入栈中的目标换成区,那么这个程序就会有缓冲区溢出错误。例如,下面的函数就有缓冲区错误,因为gets函数拷贝一个任意长度的串到缓冲区。为了纠正这个错误,我们必须使用fgets函数,这个函数限制了输入串的大小:

4、假设指针和它们指向的对象是相同大小的

一种常见的错误是假设指向对象的指针和它们所指向的对象是相同大小的:

这里的目的是创建一个由n个指针组成的数组,每个指针都指向一个包含m个int的数组。然而,因为程序员将int **A = (int **)malloc(n * sizeof(int));中将sizeof(int)写成了sizeof(int),代码实际创建的是一个int的数组。这段代码只有在int和指向int的指针大小相同的机器上运行良好。

但是,如果我们在像Alpha这样的机器上运行这段代码,其中指针大于int,那么在for(i = 0; i < n; i++) A[i] = (int *)malloc(m * sizeof(int));将写到超过A数组末端的地方。因为这些字中的一个很可能是分配块的边界标记脚部,所以我们可能不会发现这个错误,而没有任何明显的原因。

5、造成错位错位

错位错误是另一种很常见的覆盖错误发生的原因:

这是前面程序的另一个版本。这里我们创建了一个n个元素的指针数组,但是随后试图初始化这个数组的n+1个元素,在这个过程中覆盖了A数组后面的某个存储器。

6、引用指针,而不是它所指向的对象

如果我们不太注意C操作符的优先级和结合性,我们就会错误地操作指针,而不是期望操作指针所指向的对象。比如,考虑下面的函数,其目的是删除一个有*size项的二叉堆里的第一项,然后对剩下的*size-1项重新建堆。

*size—目的是减少size指针指向的整数的值。然而,因为一元—和*运算符优先级相同,从右向左结合,所以代码实际减少的是指针自己的值,而不是它所指向的整数的值。如果幸运的话,程序会立即失败,但是更有可能发生的是,当程序在它执行过程的很后面产生一个不正确的结果时,我们只能在那里抓脑袋了。这里的原则是如果你对优先级和结合性有疑问,就使用括号。使用表达式(*size)--。

7、误解指针运算

另一种常见的错误是忘记了指针的算术操作是以它们指向的对象的大小为单位来进行的,而这种大小单位并不一定是字节。例如,下面函数的目的是扫描一个int的数组,并返回一个指针,指向val的首次出现:

8、引用不存在的变量

没有太多经验的C程序员不理解栈的规则,有时会引用不再合法的本地变量,如下列所示:

这个函数返回一个指针,指向栈里的一个局部变量,然后弹出它的栈帧。尽管p仍然指向一个合法的存储器地址,但是它已经不再指向一个合法的变量了。当以后在程序中调用其他函数时,存储器将重用它们的帧栈。后来,如果程序分配某个值给*p,那么它可能实际正在修改另一个函数的帧栈中的一个条目,从而带来潜在地灾难性的、令人困惑的后果。

9、引用空闲堆块中的数据

一个相似的错误是引用已被释放了的堆块中的数据。如下面的示例,示例中分配了一个整数数组x,之后释放了块x,最后又引用了它。

10、引起存储器泄漏

存储器泄漏是缓慢、隐形的杀手,当程序员不小心忘记释放已分配块,而在堆里创建了垃圾时,会发生这种问题。例如,下面的函数分配了一个堆块x,然后不释放它就返回。

如果leak经常被调用,堆里就会充满了垃圾,最糟糕的情况下,会占有整个虚拟地址空间。对于像守护进程和服务器这样的程序来说,存储器泄漏是特别严重的,根据定义这些程序是不会终止的。

审核编辑:汤梓红

标签:

上一篇:差动放大电路温漂分析
下一篇:最后一页