C - scanf的内存管理

日期 2018-10-10
Coding/编程
作者 Webster Zhang

前言

今天继续写那个简化版的搜索引擎的时候,遭遇了这样一个情况:输入一段英文的文本,单词之间以空格分割。而我需要把指定范围内的单词收集起来作进一步的处理。本来感觉很简单,但是毕竟还是太年轻,尝试了四十分钟以后不是内存泄漏就是Segfault,后来跑去吃了一顿饭以后突然想到了办法。

动态内存分配!

Give_me_pointers

动态内存分配是指,在程序运行的过程中,向系统从堆中申请足够大小而且未被使用的内存空间。由于内存的大小是程序在运行的时候申请的,所以在写代码的时候并不能确定具体需要多少内存。这个特性使得在进行一些基于用户输入来保存不同大小的数据的操作时,能尽量减少内存的浪费。比如在进行scanf/fscanf读取用户输入的字符串的时候,运用这一技术可以使得程序基于用户输入字符串的长度,获得不同大小的内存空间来进行储存,而不需要用户事先提供所输入字符串的长度。但是运用这一技术时如果操作不当,会导致一个有可能会非常严重的问题———内存泄漏。

内存泄漏

Mem_leak

内存泄漏指的是在程序长期运行时,若程序不停地从堆中获得内存而不将已经用完的内存放回堆中(释放),导致有一部分内存脱离程序控制或者堆的内存溢出的情况。之所以说是“可能”会导致问题,是因为程序可能会很快运行完毕退出,所申请的内存会被操作系统的垃圾回收机制释放。而且有一些语言(如Java,C#等)支持垃圾回收器,减少了程序员犯错的机会。然而,这并不代表写一些可以很快运行完毕的程序的时候可以不释放这些内存,因为虽然现代的操作系统都带有垃圾回收机制,但是在某些系统中,系统并不会整理堆中的内存。这就意味着,一旦被程序丢失了某个内存块的控制,那个内存块将在内存断电之前都将保持不可控制/访问的状态。长期运行将有可能会导致操作系统或程序崩溃,造成严重后果。其实内存泄漏这个错误,几乎所有接触过动态内存分配的程序员都经历过,包括绝大多数参与操作系统编写的程序员,甚至几乎每个系统都有发生过内存泄漏的情况。而内存泄漏的另一个特点就是和一些bug一样,它不一定会在程序运行的时候马上产生,而是会在运行到某些特定的代码区域的时候才会发生。所以一个程序开始运行以后,过了几个月以后突然由于内存泄漏而导致系统崩溃,这种事情是有可能发生的。

内存泄漏的检测

Valgrind

在Linux中,内存泄漏可以使用Valgrind这个小工具来分析内存的使用情况。至于Valgrind怎么使用,官方手册了解一下?

scanf的内存管理

在读取字符串的过程中,其实是有不少方法可以进行动态内存分配的。大致有几种方案:

  1. 询问用户需要输入多长的字符串,然后使用malloc/calloc一次性向系统申请足够的空间
  2. 使用getc/fgetc一个一个字节获取用户的输入,然后使用realloc/calloc一个一个字节地申请内存空间
  3. 使用scanf/fscanf一个一个“单词”获取用户输入,然后使用realloc/calloc一个“单词”一个“单词”地申请内存空间
  4. 使用gets/fgets一行一行地获取用户输入,然后使用realloc/calloc一行一行地申请内存空间
  5. 一次性获取用户的所有输入,然后使用malloc/calloc一次性向系统申请足够的内存空间。

各方案的缺点

第一个明显不可取,用户体验极差
第二个理论上可以简单地实现,且这种方法会被使用在内存的少得特别可怜的情况下。但是可以很明显地看到,执行内存分配的次数等于用户输入的字节数。如果用户需要输入一万个字符,程序将会需要执行一万次申请内存的操作。在数据量大的时候会导致明显的资源浪费。
第三个看起来比较简单,但是一个“单词”是多长?如果可以在不询问用户的长度知道这个问题的答案,干嘛还需要动态分配内存呢,直接在写代码的时候申请一个指定大小的字符数组就好了啊。
第四个和第三个差不多,但是需要知道一行是多长。
第五个看上去实现起来没啥难度,一直读读到EOF就好了啊,但是实际上似乎没办法通过只读一次文件的情况下实现这个操作。

缓冲区的使用

最后我使用的方案基本上是第二、第三和第四个方案的变体。第二个方案特点在于我们知道一次性需要读取多少个字符,第三和第四个方案实际上隐含了让程序一次性读取多个字符的思路。尽管最近内存颗粒非常贵,但是由于现代程序的运行环境并不是以前那种内存要按字节来节约使用的情况,所以完全可以设置一个比较大的缓冲区。比如在写程序的时候先设置一个空间为10000的字符数组作为缓冲。在读取时使用scanf/fscanf一次性读取一个单词,以空格分隔。读取到的单词放在缓冲区中,这样就可以使用strlen来获取到这个单词的长度了。之后我们再根据这个长度去申请对应大小的内存块就好了。若10000个字符还不足以存放一个单词,就先申请10000字节的内存空间,然后把缓冲区的东西放到这个内存空间里面去,再用这个缓冲区去再读取10000个字符,然后用realloc把剩下的部分的内存空间也申请完,把剩下的单词也放进去,以此类推。

缓冲区的优点

  1. 不用事先知道需要多大的空间,用户体验较好。
  2. 不用一个个地申请内存空间,节约了时间成本。
  3. 事先清楚最多需要读取多少的字节数,缓冲区可以反复使用。

缓冲区的缺点

  1. 在一个单词的字符量不大时,缓冲区会导致浪费。(但是现在内存足够,就是可以为所欲为)
  2. 写代码时难度稍大。(起码比一个一个字符来要难)
  3. 在一个单词的字符量大于缓冲区的容量时,程序员若不加以处理和预防,将会导致缓冲区溢出(点名批评C和C++)

代码

不存在的,下一个。

参考

Wiki - 内存管理
Wiki - 垃圾回收
极客学院 - C 内存管理
Valgrind官方手册
IBM - 缓冲区溢出