C 语言常见的内存错误类型
提起 C 语言,大家可能会第一个想到指针,接着可能会想起与内存有关的错误。我们就利用这篇博客来总结一下 C 语言中常见的与内存有关的错误!
常见的内存错误- 解引用坏指针
- 读取未初始化的内存
- 覆盖内存
- 解引用一个不存在的变量
- 多次释放一个内存 block
- 引用被释放的内存
- 内存 block 释放失败
那么接下来,我们用具体的案例来说明一下上面提到的与内存有关的错误。
Dereferencing Bad Pointers
int val;
scanf("%d",val);
这个代码的本意是使用 scanf 从 stdin 读取一个整数到 val 这个变量。但是 scanf 需要的参数是一个格式字符串和变量的地址
scanf("%d",&val);
这个内存相关的错误是把 val 的值作为一个内存地址来使用了,程序会试图将一个整数写到这个内存位置上, 这样做会导致不可预知的后果,如果运气好的话,程序会由于 segmentation fault 导致崩溃。如果运气不好的话,val 的值对应到了虚拟内存中的一些有效的读写区域,导致 scanf 将数据覆盖到该内存区域,然后发生无法预料的错误,这种类型的错误难以被发现或者调试。
Reading Uninitialized Memory
/* return y = Ax */
int *matvec(int **A, int *x) {
int *y = (int *)malloc( N * sizeof(int) );
int i, j;
for (i=0; i<N; i++) {
for (j=0; j<N; j++) {
y[i] += A[i][j] * x[j];
}
}
return y; }
在 C 语言中,为初始化的全局变量总是会被加载器初始化为零,但是对于堆内存却不会主动初始化。
在程序中,y[i] 这个变量的初始值并不一定等于零,但是从程序的写法来看,程序员默认 y[i] 这个变量的初始值就是为零。对于这个类型错误的解决办法就是正确的初始化变量,将 y[i] 设置为零。
读取未初始化的内存错误,简单来说本来初始化的内存就都存在这个内存区域里面,但是程序到其他内存区域读写数据了。
Overwriting Memory
1、错误估计了对象的大小
int **p;
p = (int **)malloc( N * sizeof(int) );
for (i=0; i<N; i++) {
p[i] = (int *)malloc( M * sizeof(int) );
}
这个程序是创建一个由 N 个指针组成的数组,每个指针指向一个包含 M 个 int 类型的元素的数组。
但是
p = (int **)malloc( N * sizeof(int) );
这句代码是假设了指针和指针它们指向的对象是大小相同的,将sizeof(int) 和 sizeof(int*) 混同,这段代码在 int 字节大小和指向 int 的指针的字节大小相同的机器上会正确运行,但是如果不相等就会出问题,正确的代码应该是
p = (int **)malloc( N * sizeof( int * ) );
2、错位错误
int **p;
p = (int **)malloc( N * sizeof(int *) );
for (i=0; i<=N; i++) {
p[i] = (int *)malloc( M * sizeof(int) );
}
这段代码是创建了一个 N 个元素的指针数组,但是在初始化指针数组的时候初始化了 N+1 个,把 p 数组后面的内存位置覆盖掉了。
3、未做数组越界检查
char s[8];
int i;
gets(s); /* reads “123456789” from stdin */
这个程序不检查输入的字符串的大小就写入栈中的目标缓冲区,那么就会造成缓冲区溢出错误,为了避免这个错误不要使用 gets 函数而是使用 fgets 函数,这个函数有对字符串的大小进行限制。
。
4、误解指针运算
int *search(int *p, int val) {
while (p && *p != val)
p += sizeof(int);
return p;
}
5、引用指针而不是它所指的对象
int *getPacket(int **packets, int *size) {
int *packet;
packet = packets[0];
packets[0] = packets[*size - 1];
*size--; // what is happening here?
reorderPackets(packets, *size);
return(packet);
}
这段代码是由于没有留意 C 操作符的优先级和结合性导致错误的操作了指针,而不是指针所指的对象。
*size--
本意是减少 size 指针指向的值,但是实际运行情况是减少的是指针自己的值。运气够好的话,程序马上出错。要是运气差的话,程序运行了一段时间后才出错,这个时候估计都不知道上哪里寻找错误了。
程序中的 p 指针运算 p += sizeof(int) ,每次都把指针加了4,这样导致访问了数组中的每 4 个整数。指针的算术操作是以它们指向的对象的大小为单位来操作的,但是这个对象的大小单位不一定是 int 。
6、引用不存在的变量
int *foo () {
int val;
return &val;
}
这段程序主要是利用栈的理解,这个函数返回一个指针 &val,指向栈里的一个局部变量,然后弹出它的栈帧,这个时候虽然 &val 仍然指向一个合法的内存地址,但是它已经不再是指向 val 变量了。当 &val 指向的内存地址被重新利用之后,程序会带来令人困惑的运行结果。
Freeing Blocks Multiple Times
x = (int *)malloc( N * sizeof(int) );
<manipulate x>
free(x); ...
y = (int *)malloc( M * sizeof(int) );
free(x);
<manipulate y>
这段程序把指针 x 指向的内存地址释放了 2 次,第一次释放了正确的内存地址,第二次释放的时候有可能会将已经写在该内存地址的数据释放掉,从而造成无法预料的后果。
Referencing Freed Blocks
x = (int *)malloc( N * sizeof(int) );
<manipulate x>
free(x); ...
y = (int *)malloc( M * sizeof(int) );
for (i=0; i<M; i++)
y[i] = x[i]++;
这个程序的错误是引用了已经被释放了的堆块中的数据,这个类型的错误只会在程序执行的后面才会显示出破坏效果。
Failing to Free Blocks (Memory Leaks)
foo() {
int *x = (int *)malloc(N*sizeof(int));
...
return;
}
这段程序会引起内存泄漏,内存泄漏是缓慢的隐形杀手,在堆里分配了块,使用完之后忘记释放,那么就创建了垃圾。如果垃圾变多了,运气够差的话,会占用整个虚拟地址空间。像是一些比较重要的进程如守护进程,这个守护进程是不会终止的,所以内存泄漏对这类不会终止的进程来说,危害巨大。
参考
这笔记来自于学习华盛顿大学的 《软硬件接口》 课程的课程记录,
网友评论