0%

C/C++ 常见的内存错误

间接引用坏指针

使用以下代码向 val 变量写入数据

1
scanf("%d",&val);

如果写错成下面这样:

1
scanf("%d",val);

我们传递的是 val 而不是 val 的地址,scanf 将 val 解释为一个地址,最好的情况下程序立即终止,最糟糕情况下,val 的内容对应于虚拟内存的某个合法的读/写区域,于是覆盖了这段内存,这通常会在相当长的一段时间后造成灾难性的,令人困惑的后果。

读未初始化的内存

1
2
3
4
int matvec(){
int i;
printf("%d\n",i);
}

i 是一个随机值。

允许栈缓冲区溢出

如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就有缓冲区溢出错误(buffer overflow bug)。如下列代码所示:

1
2
3
4
5
void bufoverflow(){
char buf[64];
gets(buf);
return;
}

更安全的版本:

1
2
3
4
5
void bufoverflow(){
char buf[64];
fgets(buf, 64, stdin);
return;
}

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

如下列代码所示:

1
2
3
4
5
6
7
8
9
int **makeArray(int n, int m){
int i;
int **A = (int **)malloc(n*sizeof(int));

for(int i=0; i<n; ++i){
A[i] = (int*)malloc(m*sizeof(int));
}
return;
}

以上代码的目的是创建一个由 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
2
3
4
5
6
void fill(){
int num[10];
for(int i=0; i<=10; ++i){
num[i] = i;
}
}

以上的主要错误就是数组越界访问了,num 的数组下标范围[0,9],总共 10 个元素。for 循环填充了11个元素,覆盖了 num[9] 之后的内存位置。

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

如果忽略了 C 操作符的优先级,就有可能错误地操作指针,而不是指针所指向的对象。例如下面代码,用于用于删除一个 有 *size 项的二叉堆里的第一项,然后对剩下的 *size -1 项重新建堆。

1
2
3
4
5
6
7
int *binheapDelete(int **binheap, int *size){
int *packet = binheap[0];
binheap[0] = binheap[*size - 1];
*size--;
heapify(binheap,*size,0);
return packet;
}

第 4 行目的减少二叉堆元素的数目,因为一元运算符 ‘*’ 和 ‘–’ 优先级相同,从右向左结合。所以第 6 行实际减少的是指针自己的值,而不是原本指针指向的整数的值。最幸运的话程序立即崩溃,不幸的是程序运行很久之后才出现一个不正确的结果,会令开发人员很困惑。第 6 行正确写法:(*size)--

误解指针运算

如下列代码所示,扫描一个 int 数组,并返回一个指针,指向 val 的首次出现:

1
2
3
4
5
int* search(int *p, int val){
while(*p && *p!=val)
p += sizeof(int);
return p;
}

每次循环时,第 3 行都把指针加了 4,函数就不正确地扫描了数组中的每 4 个整数。

引用不存在的变量

局部变量存在在栈里,函数返回时局部变量就回收了,不能再引用它的地址。如下列代码所示:

1
2
3
4
int *stackref(){
int val;
return &val;
}

如果函数返回之后的某一个时间,修改了 val 的值,很可能修改了另一个函数栈帧的条目,引发令人困惑的问题。

引用空闲堆块中的数据

堆内存被释放时,就不能再次引用该堆块的地址。如果试图修改它,将破坏其它变量的值。如下列代码所示:

1
2
3
4
5
6
int *heapref(){
int *p = (int*)malloc(sizeof(int));
*p = 1024;
free(p);
*p = 1000;
}

第 5 行修改了已释放的堆内存,有可能会在程序运行很久之后才会发现这个问题,同样会令人很困惑。

引起内存泄漏

内存泄漏发生于忘记释放堆内存,这是一个缓慢的,隐性的杀手。如下列代码所示:

1
2
3
4
void leak(int n){
int *x = (int*)malloc(n * size(int));
return;
}

如果经常调用 leak,那么渐渐地,堆里就会充满垃圾,最糟糕情况下,会占用整个虚拟地址空间。对于像守护进程和服务器这样需要长久运行的服务,内存泄漏是特别严重的。

小节

虚拟内存是对内存的抽象。虚拟内存的实现需要硬件和软件共同完成,操作系统提供页表,硬件根据页表将一个虚拟地址翻译为物理地址。

虚拟内存提供以下功能:

  • 在主存中自动缓冲最近使用的存放在磁盘上的虚拟地址空间的内存。
  • 虚拟内存简化了内存管理,进而又简化了链接,在进程间共享数据,进程的内存分配及程序加载。
  • 虚拟内存通过在每条页表条目中加入保护位,从而简化了内存保护。

现代系统通过将虚拟内存和磁盘文件关联起来,来初始化虚拟内存片,这个过程称为内存映射。内存映射为共享数据,创建新的进程以及加载程序提供了一种高效的地址。