对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 = 0; i < n; i++)for (j = 0; 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[64];  //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数组空间。

 1 // Create an nxm array
 2 int **makeArray1(int n, int m)
 3 {
 4     int i;
 5     int **A = (int **)malloc(n * sizeof(int)); // Wrang way
 6     // right way
 7     //int **A = (int **)malloc(n * sizeof(int *));
 8
 9     for (i = 0; i < n; i++)
10     A[i] = (int *)malloc(m * sizeof(int));
11     return A;
12 }

上述代码目的是创建一个由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)错误是另一种常见的覆盖错误来源:

 1 // Create an nxm array
 2 int **makeArray2(int n, int m)
 3 {
 4     int i;
 5     int **A = (int **)malloc(n * sizeof(int *));
 6
 7     for (i = 0; i <= n; i++)
 8         A[i] = (int *)malloc(m * sizeof(int));
 9     return A;
10 }

很明显,for循环次数不合预期,导致写越界。幸运的话,进程会立即崩溃;不幸的话,运行很长时间才抛出各种诡异问题。

6. 引用指针,而不是它所指向的对象(Referencing a Pointer Instead of the Object It Points to)

如果不注意C操作符的优先级和结合性,就会错误地操作指针,而不是指针所指向的对象。
比如下面的函数,其目的是删除一个有*size项的二叉堆里的第一项,然后对剩下的*size-1项重建堆:

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

上述代码中,由于--和*优先级相同,从右向左结合,所以*size--其实减少的是指针自己的值,而非其指向的整数的值。因此,谨记:当你对优先级和结合性有疑问时,就应该使用括号。

7. 误解指针运算(Misunderstanding Pointer Arithmetic)

在C/C++中,指针的算术操作是以它们指向的对象的大小为单位来进行的。例如下面函数的功能是扫描一个int的数组,并返回一个指针,指向val的首次出现:

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

8. 引用不存在的变量(Referenceing Nonexistent Variables)

C/C++新手不理解栈的规则时,可能会引用不再合法的本地变量,例如:

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

函数返回的指针(假设为p)指向栈中的局部变量,但该变量在函数返回后随着stackref栈帧的销毁已经不再有效。也即:尽管函数返回的指针p仍然指向 一个合法的存储器地址,但它已经不再指向一个合法的变量了。当程序后续调用其它函数时,存储器将重用刚才销毁栈帧处的存储器区域。再后来,如果程序分配某 个值给*p,那么它可能实际上正在修改另一个函数栈帧中的数据,从而潜在地带来灾难性的、令人困惑的后果。

9. 引用空闲堆块中的数据(Referencing Data in Free Heap Blocks)


典型的错误为:引用已经被释放了的堆块中的数据,例如:

 1 int *heapref(int n, int m)
 2 {
 3     int i;
 4     int *x, *y;
 5
 6     x = (int *)malloc(n * sizeof(int));
 7
 8     /* ... */    /* Other calls to malloc and free go here */
 9
10     free(x);
11
12     y = (int *)malloc(m * sizeof(int));
13     for (i = 0; i < m; i++)
14         y[i] = x[i]++;        // Oops! x[i] is a word in a free block
15
16     return y;
17 }

10. 引起内存泄露(Introducing Memory leaks)

内存泄露是缓慢、隐性的杀手,当程序员忘记释放已分配块时会发生这种问题,例如:

1 void leak(int n)
2 {
3     int *x = (int *)malloc(n * sizeof(int));
4
5     return;     // x is garbage at this point
6 }

如果leak在程序整个生命周期内只调用数次,则问题还不是很严重(但还是会浪费存储器空间),因为随着进程结束,操作系统会回收这些内存空间。但如果 leak()被经常调用,那就会发生严重的内存泄露,最坏的情况下,会占用整个虚拟地址空间。对于像守护进程和服务器这样的程序来说,内存泄露是严重的 bug,必须加以重视。

【参考资料】

深入理解计算机系统. Bryant & O`Hallaron.

转载于:https://www.cnblogs.com/acm1314/p/5632821.html

C程序中常见的内存操作错误相关推荐

  1. 错误内存【读书笔记】C程序中常见的内存操作有关的典型编程错误

    题记:写这篇博客要主是加深自己对错误内存的认识和总结实现算法时的一些验经和训教,如果有错误请指出,万分感谢. 对C/C++程序员来讲,内存管理是个不小的挑战,绝对值得慎之又慎,否则让由上万行代码构成的 ...

  2. Android中常见的内存泄露

    内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏.内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会 ...

  3. Android开发中常见的内存泄露案例以及解决方法总结

    Android开发中常见的内存泄露案例以及解决方法总结 参考文章: (1)Android开发中常见的内存泄露案例以及解决方法总结 (2)https://www.cnblogs.com/shen-hua ...

  4. .NET中常见的内存泄露问题——GC、委托事件和弱引用

    其实吧,内存泄露一直是个令人头疼的问题,在带有GC的语言中这个情况得到了很大的好转,但是仍然可能会有问题. 一.什么是内存泄露(memory leak)? 内存泄露不是指内存坏了,也不是指内存没插稳漏 ...

  5. 编程过程中常见的内存开辟和释放问题

    本博客是个人自己写的第一篇技术贴,鉴于本人经常参考业内人士的博客比如 JuLy 等等,从中学到不少东西,闲暇之余,写点东西,希望可以对需要的人有所帮助. 有关内存的开辟和释放函数,请参考本人转载的第一 ...

  6. JS中常见的内存泄漏及识别方式

    JavaScript常见的内存泄漏及识别方式 1.什么是内存 2.什么是内存泄漏 3.内存泄漏导致的后果 4.常见的内存泄漏 (1)全局变量引起的内存泄漏 (2)闭包引起的内存泄漏 (3)被遗忘的定时 ...

  7. java内部类内存泄漏,Android中常见的内存泄漏和解决方案

    什么是内存泄漏? 简单点说,就是指一个对象不再使用,本应该被回收,但由于某些原因导致对象无法回收,仍然占用着内存,这就是内存泄漏. 为什么会产生内存泄漏,内存泄漏会导致什么问题? 相比C++需要手动去 ...

  8. android中常见的内存泄漏和解决的方法

    android中的内存溢出预计大多数人在写代码的时候都出现过,事实上突然认为工作一年和工作三年的差别是什么呢.事实上干的工作或许都一样,产品汪看到的结果也都一样,那差别就是速度和质量了. 写在前面的一 ...

  9. C语言中常见的内存相关的Bugs

    这里写目录标题 内存相关的Bugs 一.常见的错误参考 1.经典的scanf bug 2.指针未正确初始化 3.指针被释放时没有置空 4.不要用return语句返回指向"栈内存"的 ...

最新文章

  1. 孙正义:未来30年投资趋势【附PPT】
  2. Python+Django+Eclipse 在Windows下快速开发自己的网站
  3. ABAP SAPGUI 里使用 F4 value help 选择时间
  4. Java 8 日期和时间解读
  5. win10 java无法运行_Win10中配置jdk之后javac无法运行
  6. 《大话设计模式》笔记(1)——创建型模式
  7. 【推荐实践】推荐技术在旅游电商中的应用及挑战.pdf(附下载链接)
  8. 计算机中那些事儿(八):再历装系统之终身学习
  9. Redis集群的搭建与主从复制,redis-cluster
  10. 中英文对照 —— 计算机编程
  11. CF1067D Computer Game
  12. 基于PWM调宽的呼吸灯算法
  13. 学习笔记1——制作数据集
  14. c语言 dct变换,汇编实现的DCT变换算法
  15. 如何用微信公众号二维码事件做扫码登陆
  16. 图形化管理工具Portaniner安 以及 Docker镜像详解(三)
  17. 黄金连分数(python)
  18. Hololens2开发笔记-重刷系统(正常发布版本和内部预览版本)
  19. Python:Turtle图形绘制
  20. 翻译软件哪个准确度高

热门文章

  1. SQL Compare
  2. usaco Packing Rectangles
  3. [IE编程] 如何获得IE版本号
  4. 论营销的重要性:以一个磁铁为例
  5. 2018.3,GC可控了
  6. AnimatorController即动画控制器创建的BUG
  7. idea编辑springboot,如何打成war包
  8. 洛谷P1388 算式
  9. 20145234黄斐《java程序设计》第六周
  10. Android基础_1 四大基本组件介绍与生命周期