对C程序员来说,管理和使用虚拟内存是个困难的、容易出错的任务。与内存有关的错误常常令人费解,因为它们在时间和空间上,经常在距错误源一段距离之后才表现出来。
与内存相关的常见错误包括:
- 间接引用坏指针;
- 读取未初始化的内存;
- 栈缓冲区溢出;
- 假设指针和它们指向对象的大小相同;
- 错位错误;
- 引用指针而不是它所指向的对象;
- 误解指针运算;
- 引用不存在的变量;
- 引用空闲堆块中的数据;
- 内存泄露。
1、间接引用坏指针
间接引用坏指针的一个常见示例是scanf错误。假设要使用scanf从stdin读一个整数到一个变量。正确的方法是传递给scanf一个变量的地址:
但若传递val的内容,而不是它的地址,在这种情况下,scanf将把val的内容解释为一个地址,并试图将一个字写到这个位置。在最好的情况下,程序立即以异常终止;但在最糟糕的情况下,val的内容对应于虚拟内存的某个合法的读/写区域,于是就覆盖了这块内存,这通常在相当长的一段时间后造成灾难性的、令人困惑的后果。
知识点:scanf输入的是变量的地址。
2、读取未初始化的内存
BSS(Block Started by Symbol)通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。其特点是可读写的,在程序执行之前BSS段会自动清0。所以,未初始的全局变量在程序执行之前已经成0了。
虽然BSS内存位置(如未初始化的全局变量)总是被加载器初始化为零,但是对于堆内存却不是这样的。一个 常见的错误就是假设堆内存被初始化为零。
在该示例中,不正确地假设向量y被初始化为零,造成计算结果错误。正确的方式是显式地将y[i]设置为零,或者使用calloc。
知识点:calloc在动态分配完内存后,自动初始化该内存空间为零,而malloc不初始化,里边数据是随机的垃圾数据。
3、栈缓冲区溢出
如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就会有缓冲区溢出错误(buffer overflow bug)。如下面的函数就有缓冲区溢出错误,因为gets函数复制一个任意长度的串到缓冲区。为纠正这个错误,需使用fgets函数,这个函数限制了输入串的大小。
知识点:fgets函数读取指定大小的数据,避免gets函数从stdin接收字符串而不检查它所复制的缓存的容积导致缓存溢出的问题。
4、假设指针和它们指向对象的大小相同
一种常见的错误是假设指向对象的指针和它们所指向的对象是相同大小的。
这里的目的是创建一个由n个指针组成的数组,每个指针都指向一个包含m个int的数组。然而在第5行中将sizeof(int *)写成了sizeof(int),代码实际创建的是一个int的数组。
这段代码只有在int和int的指针大小相同的机器上运行良好。但是,在像Core i7这样的机器上运行这段代码,其中指针大于int,那么第7、8行的循环将写到超出A数组结尾的地方。
知识点:指针和它们指向对象的大小不相同
5、错位错误
错位错误是另一种常见的造成覆盖错误的来源。
这里在第5行创建了一个n个元素的指针数组,但是随后在第7、8行试图初始化这个数据的n+1个元素,在这个过程中覆盖了A数组后面的某个内存位置。
知识点:注意在C语言中,对于含有n个元素的数组,数组下标从0开始,最大到n-1。
6、引用指针而不是它所指向的对象
如果不注意C操作符的优先级和结合性,就会错误地操作指针,而不是指针所指向的对象。比如下面的函数,其目的是删除一个有*size项的二叉堆里的第一项,然后对剩下的*size-1项重新建堆。
在第6行,目的是减少size指针所指向的整数的值。然而一元运算符--和*的优先级相同,从右向左结合,所以第6行中的代码实际减少的是指针自己的值,而不是它所指向的整数的值。如果幸运的话,程序会立即失败;但更有可能发生的是,当程序在执行过程后很久才产生一个不正确的结果,而我们只有一头雾水。
知识点:当对优先级和结合性有疑问的时候,就使用括号。比如第6行,使用表达式(*size)--,更能清晰地表明意图。
7、误解指针运算
常见的错误是忘记指针的算术操作是以它们指向的对象的大小为单位来进行的,而这种大小单位不一定是字节。如下面函数的目的是扫描一个int的数组,并返回一个指针,指向val的首次出现。
但是,因为每次循环时,第4行都把指针加了4(int的字节数),函数就不正确地扫描数组中每4个整数。
8、引用不存在的变量
没有经验的C程序员不理解的栈的规则,有时会引用不合法的本地变量,如下图所示。
这个函数返回一个指针(比如p),指向栈里的一个局部变量,然后弹出它的栈帧。尽管p仍然指向一个合法的内存地址,但是它已经不再指向一个合法的变量了,从而带来灾难性的、令人困惑的后果。
知识点:局部变量只在本函数内有效。
9、引用空闲堆块中的数据
同上相似的一个错误是引用已经释放了的堆块中的数据。如下图,这个函数在第6行分配了一个整数数组x,在第10行释放了块x,然后在第14行中又引用了它。
当程序在第14行引用x[i]时,数组x可能是某个其他已分配块的一部分了,因此其内容被重写了。
10、内存泄露
内存泄露是缓慢、隐形的杀手,当程序员 不小心忘记释放已分配的块,而在堆里创建了垃圾时,就会发生这种问题。如下面的函数分配了一个堆块,然后不释放它就返回。
如果不及时释放堆里的垃圾,慢慢地堆里就充满了垃圾,最糟糕的情况下,会占用整个虚拟地址空间。
知识点:C标准库提供的malloc、free函数,以及C++中的new、delete操作符需要成对使用。
发表评论 取消回复