C语言中几种常见的与内存有关的错误

Posted lgxo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言中几种常见的与内存有关的错误相关的知识,希望对你有一定的参考价值。


前言

对C程序员来说,管理和使用虚拟内存可能是个困难的、容易出错的任务。与内存有关的错误属于那些最令人惊恐的错误,因为它们在时间上或空间上,经常在距离错误源一段距离之后才表现出来。
在这里讨论了几种常见的与内存有关的错误。


常见错误

1.间接引用坏指针

我们知道在进程的虚拟地址空间中有较大的洞,没有映射到任何有意义的数据。如果我们试图简介引用一个指向这些洞的指针,那么操作系统会以段异常终止这个程序。
其次,虚拟内存的某些区域是只读的。试图写这些区域将会以保护异常终止这个程序。
例如经典的scanf错误:

	scanf("%d", val); /* 应该是:scanf("%d", &val); */

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

2.读未初始化的内存

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

/* Return y = Ax */
int *matvec(int **A, int *x, int n){
	int i, j;
	
	int *y = (int *)malloc(n * sizeof(int));
	
	for(i = 0; i < n; i++){
		for(j = 0; j < n; j++){
			y[i] += A[i][j] * x[j];
		}
	}
}

该函数中程序员假设向量y被初始化为0,然而事实并不是这样的。正确的实现方式是显示地将 y[i] 设置为0,或是使用calloc函数

3.允许栈缓冲区溢出

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

void bufoverflow(){
	char buf[64];
	
	gets(buf);
	
	return;
}

gets函数复制一个任意长度的串到缓冲区,该函数很可能发生缓冲区溢出错误。纠正这个错误,我们可以使用fgets函数,这个函数限制了输入串的大小。

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

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

/* Create an nxm array */
int **makeArray1(int n, int m){
	int i;
	
	int **A = (int **)malloc(n * sizeof(int));
	
	for(i = 0; i < n; i++){
		A[i] = (int *)malloc(m * sizeof(int));
	}
	
	return A;
}

第5行的目的是创建一个由n个指针组成的数组,然而程序员将sizeof(int *)写成了sizeof(int),代码实际上创建的是一个int数组,如果指针大于int,那么之后的循环将写到超出A数组结尾的地方。
因为这些字中的一个很可能是已分配的边界标记脚部,所以在后边释放这个块之前,我们可能不会发现这个错误。此时,分配器中的合并代码会戏剧性地失败,而没有任何明显的原因。这类”在远处起作用“是与内存有关的编程错误的典型情况。

5.造成错位错误

错位(off-by-one)错误是另一种很常见的造成覆盖错误的来源。
例如:

/* Create an nxm array */
int **makeArray1(int n, int m){
	int i;
	
	int **A = (int **)malloc(n * sizeof(int *));
	
	for(i = 0; i <= n; i++){
		A[i] = (int *)malloc(m * sizeof(int));
	}
	
	return A;
}

注意第7行的判断条件i <= n,发生了数组越界,于是该函数会初始化这个数组的 n+1 个元素,会覆盖数组A后边的某个内存的位置。

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

如果不太注意C操作符的优先性和结合性,我们就会错误地操做指针,而不是指针所指向的对象。
例如:

int *binheapDelete(int **binheap, int *size){
	int *packet = binheap[0];
	
	binheap[0] = binheap[*size - 1];
	*size--;
	heapify(binheap, *size, 0);
	
	return packet;
}

该函数的目的是删除一个有 *size 的项的二叉堆里的第一项,然后对剩下的 *size-1 项重新建堆。
在第5行,想要减少 size 指针指向的整数值。然而,因为一元运算符 --*的优先级相同,从右向左结合,所以此处实际上减少的是指针自己的值。如果幸运的话,程序会立即失败;但更可能发生的是,程序执行很长一段时间后才产生一个不正确的结果。
建议对不能肯定的优先级和结合性,使用括号()

7.误解指针运算

另一种常见的错误是忘记了指针的算术操作是以它们指向的对象的大小为单位来进行的,而这种大小单位不一定是一个字节。
例如:

int *search(int *p, int val){
	while(*p && *p !== val){
		p *= sizeof(int);
	}
	
	return p;
}

该函数的目的是扫描一个int数组并返回一个指向val的首次出现的指针。然而,因为每次循环时,第3行都把指针加了4(一个整型的字节数),函数就会不正确地扫面数组中的每四个整数。

8.引用不存在的变量

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

int *stackref(){
	int val;
	
	return &val;
}

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

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

一个相似的错误就是引用已经被释放了的堆块中的数据。
例如:

int *heapref(int n, int m){
	int i;
	int *x, *y;
	
	x = (int *)malloc(n * sizeof(int));
	
	//Other calls to malloc and free go here
	
	free(x);
	
	y = (int *)malloc(m * sizeof(int));
	for(i = 0; i < m; i++){
		y[i] = x[i]++;
	}
	
	return y;
}

该函数在第5行分配了一个堆块x,然后在第9行释放了该堆块,却又在第13行引用了它。取决于malloc和free的调用模式,当程序再次在第13行引用x[i]时,数组x可能已经是其他堆块的一部分了,因此其内容很可能已经被重写了。
和其他许多与内存有关的错误一样,这个错误只会在程序执行的后面,当我们注意到y中值被破坏了才会显现出来。

10.引起内存泄露

内存泄露是缓慢、隐性的杀手,当程序员不小心忘记释放已分配块,而在堆里创建了垃圾时,会发生这种问题。
例如:

void leak(int n){
	int *x = (int *)malloc(n * sizeof(int));
	
	return;
}

该函数分配了一个堆块x,但是没有释放它就返回了。如果程序中经常调用该leak函数,那么渐渐地堆里就会充满了垃圾。在最糟糕的情况下,会占用整个虚拟地址空间。


结语

管理和使用虚拟内存时一件困难和容易出错的任务,常见的错误包括:简介引用坏指针,读取未初始化的内存,允许栈缓冲区溢出,假设指针和它们指向的对象大小相同,引用指针而不是它所指的对象,误解指针运算,引用不存在的变量、引起内存泄露等。
所以在使用的时候一定要小心小心再小心。

以上是关于C语言中几种常见的与内存有关的错误的主要内容,如果未能解决你的问题,请参考以下文章

C程序中与内存有关的常见错误

socket链接中几种常见的错误

socket链接中几种常见的错误

Oracle数据库中几种常见的SCN

MCU中几种常见的打印输出的方法

Java中几种常见的NPE问题