间接引用坏指针
使用以下代码向 val 变量写入数据
1 | scanf("%d",&val); |
如果写错成下面这样:
1 | scanf("%d",val); |
我们传递的是 val 而不是 val 的地址,scanf 将 val 解释为一个地址,最好的情况下程序立即终止,最糟糕情况下,val 的内容对应于虚拟内存的某个合法的读/写区域,于是覆盖了这段内存,这通常会在相当长的一段时间后造成灾难性的,令人困惑的后果。
读未初始化的内存
1 | int matvec(){ |
i 是一个随机值。
允许栈缓冲区溢出
如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就有缓冲区溢出错误(buffer overflow bug)。如下列代码所示:
1 | void bufoverflow(){ |
更安全的版本:
1 | void bufoverflow(){ |
假设指针和它们指向的对象是相同大小的
如下列代码所示:
1 | int **makeArray(int n, int m){ |
以上代码的目的是创建一个由 n 个指针组成的数组,每一个指针都指向一个包含 m 个 int 的数组。但是代码int **A = (int **)malloc(n*sizeof(int*));
被写成了 int **A = (int **)malloc(n*sizeof(int));
。实际创建的是包含 n 个 int 类型的数组。
这段代码只有在 int 和 int* 大小相同的机器行运转良好,如果在像 Core i7 这样的机器上运行代码,其中int* 大于 int,后续 A[i] = (int*)malloc(m*sizeof(int));
将写到超出 A 数组结尾的地方。这样的错误只有可能在代码运行很久之后发生,且错误很令人困惑。
造成错位错误
1 | void fill(){ |
以上的主要错误就是数组越界访问了,num 的数组下标范围[0,9],总共 10 个元素。for 循环填充了11个元素,覆盖了 num[9] 之后的内存位置。
引用指针,而不是它所指向的对象
如果忽略了 C 操作符的优先级,就有可能错误地操作指针,而不是指针所指向的对象。例如下面代码,用于用于删除一个 有 *size 项的二叉堆里的第一项,然后对剩下的 *size -1 项重新建堆。
1 | int *binheapDelete(int **binheap, int *size){ |
第 4 行目的减少二叉堆元素的数目,因为一元运算符 ‘*’ 和 ‘–’ 优先级相同,从右向左结合。所以第 6 行实际减少的是指针自己的值,而不是原本指针指向的整数的值。最幸运的话程序立即崩溃,不幸的是程序运行很久之后才出现一个不正确的结果,会令开发人员很困惑。第 6 行正确写法:(*size)--
误解指针运算
如下列代码所示,扫描一个 int 数组,并返回一个指针,指向 val 的首次出现:
1 | int* search(int *p, int val){ |
每次循环时,第 3 行都把指针加了 4,函数就不正确地扫描了数组中的每 4 个整数。
引用不存在的变量
局部变量存在在栈里,函数返回时局部变量就回收了,不能再引用它的地址。如下列代码所示:
1 | int *stackref(){ |
如果函数返回之后的某一个时间,修改了 val 的值,很可能修改了另一个函数栈帧的条目,引发令人困惑的问题。
引用空闲堆块中的数据
堆内存被释放时,就不能再次引用该堆块的地址。如果试图修改它,将破坏其它变量的值。如下列代码所示:
1 | int *heapref(){ |
第 5 行修改了已释放的堆内存,有可能会在程序运行很久之后才会发现这个问题,同样会令人很困惑。
引起内存泄漏
内存泄漏发生于忘记释放堆内存,这是一个缓慢的,隐性的杀手。如下列代码所示:
1 | void leak(int n){ |
如果经常调用 leak,那么渐渐地,堆里就会充满垃圾,最糟糕情况下,会占用整个虚拟地址空间。对于像守护进程和服务器这样需要长久运行的服务,内存泄漏是特别严重的。
小节
虚拟内存是对内存的抽象。虚拟内存的实现需要硬件和软件共同完成,操作系统提供页表,硬件根据页表将一个虚拟地址翻译为物理地址。
虚拟内存提供以下功能:
- 在主存中自动缓冲最近使用的存放在磁盘上的虚拟地址空间的内存。
- 虚拟内存简化了内存管理,进而又简化了链接,在进程间共享数据,进程的内存分配及程序加载。
- 虚拟内存通过在每条页表条目中加入保护位,从而简化了内存保护。
现代系统通过将虚拟内存和磁盘文件关联起来,来初始化虚拟内存片,这个过程称为内存映射。内存映射为共享数据,创建新的进程以及加载程序提供了一种高效的地址。