C程序中常见的内存操作错误
对C/C++程序员来说,管理和使用虚拟存储器可能是个困难的, 容易出错的任务。与存储器有关的错误属于那些令人惊恐的错误, 因为它们在时间和空间上, 经常是在距错误源一段距离之后才表现出来。 将错误的数据写到错误的位置, 你的程序可能在最终失败之前运行了好几个小时,且使程序中止的位置距离错误的位置已经很远啦。而避免这种噩梦的最好方法就是防范于未然。
幸好《深入理解计算机系统》中有一段讲: C程序中常见的内存操作有关的10种典型编程错误,十分经典, 因此抄写在此, 以便以后随时查看,复习。
把优秀变成习惯, 虽不能至,心向往之。
1. 间接引用无效指针
进程虚拟地址空间的某些地址范围可能没有映射到任何有意义的数据,如果我们试图间接引用一个指向这些地址的指针,则操作系统会以Segment Fault终止进程。而且,虚拟存储器的某些区域是只读的(如.text或.rodata),试图写这些区域会以保护异常中止当前进程。
如从stdin读取一个int变量时,scanf("%d", &val)是正确用法,若误写为scanf("%d",
val)时,val的值会被解释为一个地址,并试图向该地址写数据。在最好的情况下,进程立即异常中止。在最坏的情况下,val的值恰好对应于虚拟存储器
的某个合法的具有读/写权限的内存区域,于是该内存单元会被改写,而这通常会在相当长的一段时间后造成灾难性的、令人困惑的后果。我们学习C/C++中的指针时, 指针未初始化错误也属于这类错误。
2. 读未初始化的存储器(Reading Uninitialized Memory)
C语言的malloc并不负责初始化申请到的内存区域(在C/C++中未初始化的全局变量会被初始化为0),因此,常见的错误是假设堆存储器被初始化为0,例如:
这个程序是计算一个 n*n的矩阵(**A) 乘以 一个 n*1(*x) 的矩阵, 并返回计算结果(*y)。
// Return y = Ax
int *matvec(int **A, int *x, int n)
{
int i, j;
int *y = (int *)malloc(n * sizeof(int)); for ( i = ; i < n; i++)
for (j = ; j < n; j++)
y[i] += A[i][j] * x[j];
return y;
}
上述代码中,错误地假设了y被初始化为0。正确的实现方式是显式地依次将y[i]置为0或者使用calloc分配内存。
3. 栈缓冲区溢出(Allowing Stack Buffer Overflows)
这个是我们熟悉的缓冲区溢出错误(buffer overflow bug)
void bufoverflow()
{
char buf[];
//Here is the stack buffer overflow bug
gets(buf);
return;
}
如果输入超过64个字符, 上面的代码将导致栈缓冲区溢出。 可以使用 fgets 函数代替 gets函数, fget函数有第二个参数, 以限制输入串的大小。
4. 误以为指针和它们指向的对象是相同大小的。(Assuming that Pointers and the Objects They Point to Are the Same Size)
例如: 申请一个二维 n*m 的int数组空间。
// Create an nxm array
int **makeArray1(int n, int m)
{
int i;
int **A = (int **)malloc(n * sizeof(int)); // Wrang way
// right way
//int **A = (int **)malloc(n * sizeof(int *)); for (i = ; i < n; i++)
A[i] = (int *)malloc(m * sizeof(int));
return A;
}
上述代码目的是创建一个由n个指针构成的数组,每个指针均指向一个包含m个int的数组,但是第五行误将sizeof(int
*)写成sizeof(int)。这段代码只有在int和int *的size相同的机器上运行良好。如果在像Core
i7这样的机器上运行这段代码,由于指针变量的size大于sizeof(int),则会引发代码中的for循环写越界。因为这些字中的一个很可能是已分
配块的边界标记脚部,所以我们可能不会立即发现这个错误,直到进程运行很久释放这个内存块时,此时,分配器中的合并代码会戏剧性地失败,而没有任何明显的
原因。这是"在远处起作用"(action at distance)的一个隐秘示例,这类"在远处起作用"是与存储器有关的编程错误的典型情况。
5. 造成错位错误(Making Off-by-One Errors)
错位(Off-by-one)错误是另一种常见的覆盖错误来源:
// Create an nxm array
int **makeArray2(int n, int m)
{
int i;
int **A = (int **)malloc(n * sizeof(int *)); for (i = ; i <= n; i++)
A[i] = (int *)malloc(m * sizeof(int));
return A;
}
很明显,for循环次数不合预期,导致写越界。幸运的话,进程会立即崩溃;不幸的话,运行很长时间才抛出各种诡异问题。
6. 引用指针,而不是它所指向的对象(Referencing a Pointer Instead of the Object It Points to)
如果不注意C操作符的优先级和结合性,就会错误地操作指针,而不是指针所指向的对象。
比如下面的函数,其目的是删除一个有*size项的二叉堆里的第一项,然后对剩下的*size-1项重建堆:
int *binheapDelete(int **binheap, int *size)
{
int *packet = binheap[]; binheap[] = binheap[*size - ];
*size--; // This should be (*size)--
heapify(binheap, *size, );
return (packet);
}
上述代码中,由于--和*优先级相同,从右向左结合,所以*size--其实减少的是指针自己的值,而非其指向的整数的值。因此,谨记:当你对优先级和结合性有疑问时,就应该使用括号。
7. 误解指针运算(Misunderstanding Pointer Arithmetic)
在C/C++中,指针的算术操作是以它们指向的对象的大小为单位来进行的。例如下面函数的功能是扫描一个int的数组,并返回一个指针,指向val的首次出现:
int *search(int *p, int val)
{
while (*p && *p != val)
p += sizeof(int); // Should be p++
return p;
}
8. 引用不存在的变量(Referenceing Nonexistent Variables)
C/C++新手不理解栈的规则时,可能会引用不再合法的本地变量,例如:
int *stackref()
{
int val; return &val;
}
函数返回的指针(假设为p)指向栈中的局部变量,但该变量在函数返回后随着stackref栈帧的销毁已经不再有效。也即:尽管函数返回的指针p仍然指向
一个合法的存储器地址,但它已经不再指向一个合法的变量了。当程序后续调用其它函数时,存储器将重用刚才销毁栈帧处的存储器区域。再后来,如果程序分配某
个值给*p,那么它可能实际上正在修改另一个函数栈帧中的数据,从而潜在地带来灾难性的、令人困惑的后果。
9. 引用空闲堆块中的数据(Referencing Data in Free Heap Blocks)
典型的错误为:引用已经被释放了的堆块中的数据,例如:
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 = ; i < m; i++)
y[i] = x[i]++; // Oops! x[i] is a word in a free block return y;
}
10. 引起内存泄露(Introducing Memory leaks)
内存泄露是缓慢、隐性的杀手,当程序员忘记释放已分配块时会发生这种问题,例如:
void leak(int n)
{
int *x = (int *)malloc(n * sizeof(int)); return; // x is garbage at this point
}
如果leak在程序整个生命周期内只调用数次,则问题还不是很严重(但还是会浪费存储器空间),因为随着进程结束,操作系统会回收这些内存空间。但如果
leak()被经常调用,那就会发生严重的内存泄露,最坏的情况下,会占用整个虚拟地址空间。对于像守护进程和服务器这样的程序来说,内存泄露是严重的
bug,必须加以重视。
【参考资料】
深入理解计算机系统. Bryant & O`Hallaron.
C程序中常见的内存操作错误的更多相关文章
- 【转】《深入理解计算机系统》C程序中常见的内存操作有关的典型编程错误
原文地址:http://blog.csdn.net/slvher/article/details/9150597 对C/C++程序员来说,内存管理是个不小的挑战,绝对值得慎之又慎,否则让由上万行代码构 ...
- 错误内存【读书笔记】C程序中常见的内存操作有关的典型编程错误
题记:写这篇博客要主是加深自己对错误内存的认识和总结实现算法时的一些验经和训教,如果有错误请指出,万分感谢. 对C/C++程序员来讲,内存管理是个不小的挑战,绝对值得慎之又慎,否则让由上万行代码构成的 ...
- 《深入理解计算机系统》C程序中常见的内存操作有关的典型编程错误
对C/C++程序员来说,内存管理是个不小的挑战,绝对值得慎之又慎,否则让由上万行代码构成的模块跑起来后才出现内存崩溃,是很让人痛苦的.因为崩溃的位置在时间和空间上,通常是在距真正的错误源一段距离之后才 ...
- 对开发中常见的内存泄露,GDI泄露进行检测
对开发中常见的内存泄露,GDI泄露进行检测 一.GDI泄露检测方法: 在软件测试阶段,可以通过procexp.exe 工具,或是通过任务管理器中选择GDI对象来查看软件GDI的对象是使用情况. 注意点 ...
- Android中常见的内存泄漏
为什么会产生内存泄漏? 当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏. ...
- 谈谈.NET中常见的内存泄露问题——GC、委托事件和弱引用
其实吧,内存泄露一直是个令人头疼的问题,在带有GC的语言中这个情况得到了很大的好转,但是仍然可能会有问题.一.什么是内存泄露(memory leak)?内存泄露不是指内存坏了,也不是指内存没插稳漏出来 ...
- .NET中常见的内存泄露问题——GC、委托事件和弱引用
一.什么是内存泄露(memory leak)? 内存泄露不是指内存坏了,也不是指内存没插稳漏出来了,简单来说,内存泄露就是在你期待的时间内你程序所占用的内存没有按照你想象中的那样被释放. 因此什么是你 ...
- JavaScript 中常见的内存泄露陷阱(摘)
内存泄露是每个开发者最终都不得不面对的问题.即便使用自动内存管理的语言,你还是会碰到一些内存泄漏的情况.内存泄露会导致一系列问题,比如:运行缓慢,崩溃,高延迟,甚至一些与其他应用相关的问题. 什么是内 ...
- Js中常见的内存泄漏场景
常见的内存泄漏场景 内存泄漏Memory Leak是指程序中已动态分配的堆内存由于疏忽或错误等原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果.内存泄漏并非指内 ...
随机推荐
- Android四大核心组件之Service
实验内容 启动Service 绑定Service 与Service进行通信 实验要求 启动Service 绑定Service 与Service进行通信 实验步骤 Service概述 Service通常 ...
- sql关联表查询结果并插入
这里涉及三个表,AA,BB,CC,将AA的数据更新到CC表中,将AA中LABEL_ID分别截取字段与BB表中的label_id对应查询到LABEL_NAME作为CC表的一个字段插入,这里分成四段查询 ...
- 【转】SQL Server中的事务与锁
SQL Server中的事务与锁 了解事务和锁 事务:保持逻辑数据一致性与可恢复性,必不可少的利器. 锁:多用户访问同一数据库资源时,对访问的先后次序权限管理的一种机制,没有他事务或许将会一塌糊涂 ...
- My97DatePicker日期插件
My97DatePicker是一款优秀的日期插件,它可以很方便地实现各种日期选择效果,博得广大IT开发人员的青睐. 下面来讲讲它的常用功能及设置方法. 方法/步骤 先来最简单的配置方法: (1)下 ...
- 【皇甫】☀ TreeSet
-Set: 元素是无序(存入和取出的顺序不一致),元素不可以重复 |-- HashSet: 底层数据结构是哈希表 HashSet是如何保证元素的唯一性的呢? 是通过元素的两个方法,hashCode和e ...
- 关于sigwait
刚开始看sigwait函数,只是知道它是用来解除阻塞的信号,可是使我疑惑的是那么解除了以后为什么线程收到终止信号SIGINT的时候还是没能终止呢? 于是网上找了一些资料,总的理解如下所示: sig ...
- SetSysColors 修改系统颜色
首先我们来看一下SetSysColors函数的原型: BOOL WINAPI SetSysColors( __in int cElements, //要改变的对象的数量 __in const INT* ...
- 。。。IO流的学习之一。。。
文件写入流FileWriter的使用: import static org.junit.Assert.*; import java.io.File; import java.io.FileWriter ...
- android案例一 电话拨号器
效果图: 电话拨号器的核心原理: 意图 MainActivity代码: private EditTest et_number; //加载一个布局 setContentView(R.l ...
- iOS第三方库管理工具
作者:彷徨iOS 原文地址1:http://iostree.sinaapp.com/?p=78 原文地址2:http://blog.csdn.net/wzzvictory/article/detail ...