无知的 tonyseek

Yet Another Seeker

C 语言程序设计笔记

呵呵,看标题像是大一同学的学习笔记,事实上我是一个大四将毕业的人了。我在大一入学之后认认真真学习的第一个程序语言是 C 语言,当然,是在在课堂上。可是快四年来,我用的最多的却是各路动态语言(人生短暂我用 Python)。后来毕业设计需要选题,我跟了一个很有黑客精神的导师 [0] ,所以也就希望能挑战一下自己,选择了用我并不熟悉的 C 语言来做系统编程&网络编程。

那个项目 [1] 总算还花了我不少时间和精力,虽然答辩时遇到了奇葩的人并发生了不愉快的事情,但我终究是学到了不少东西。这篇博客用来记录我掉的那些坑,并让读者看到后可以对我说:“你还是回大一去重学吧”。

没承诺的事情它就不会去做

用 C 语言读取一个文件,当然可以用 POSIX 系统调用 read 函数。不过如果想兼容其他非 POSIX 平台,比如 Windows,比如某些特定的嵌入式环境,最好的选择还是使用 ANSI C 的 stdio.h 提供的 fread 函数。

我起初是用了这样一段代码去读取一个文本文件:

test_data.c
 char * alloc_read_test_data(FILE *stream, size_t max_len)
 {
     char *buffer;

     /* allocate buffer */
     buffer = malloc(sizeof(char) * max_len);
     memset(buffer, 0, sizeof(char) * max_len);

     /* read test data */
     if (fread(buffer, sizeof(char), max_len, stream) > 0)
         return buffer;

     return NULL;
 }

这段代码在 Linux 下完全工作正常,但是在 OSX 下总是会出现“乱入”:

bash
 $ cat test_data.txt
 What is the answer to life?

 $ ./test_read_data test_data.txt
 What is the answer to life?
 he answer to life?

 $ # WTF ↑↑

我不相信这是脏内存导致的,因为为了保险起见我已经用 memset 将新分配的内存块填 0 了。而且非常奇异的是,“乱入”的字符总是正常读取字符的节选。为此我搜索了很多资料都未寻到解答,还以为是 OSX 下 libc 的 Bug。最后翻查文档 [2]fread 的描述:

Reads an array of count elements, each one with a size of size bytes, from the stream and stores them in the block of memory specified by ptr.

The position indicator of the stream is advanced by the total amount of bytes read. The total amount of bytes read if successful is (size*count) .

貌似文档只承诺了 fread 会做这么几点:

  • 将 n 组指定尺寸的元素从流( FILE * )读入,然后存储到第一个参数指针所指向的内存区域
  • 将流的位置指针向后移动,读了多少移动多少
  • 读入的总字节数为提供的 count 参数

然后,关于返回值文档的描述是:

The total number of elements successfully read is returned. If this number differs from the count parameter, either a reading error occurred or the end-of-file was reached while reading. ...

好了,再多承诺一点:

  • 返回值如果小于 count 参数,则说明发生了读取错误,或者已经到达了文件结尾

貌似在这些承诺中,有两点没有被提及:

  1. 将读取到的内容存入指定内存区域后,是否要在结尾补上字符串终结符 '\0'
  2. 如果因为 EOF 或者其他原因,读到的字节数 M 小于指定字节数 COUNT, fread 是否仅仅对大小为 M 的内存区域做出修改呢?

首先可以确定第一点是否定的, fread 并非仅仅为文本流读取设计,它不会自己去补这个终结符;第二点则是我遇到的奇怪问题所在:文档承诺对传入指针参数指向的内存区域写入,并且预期的写入尺寸由 count 参数指定,而读取到的实际尺寸小于预期尺寸一律视为一种异常情况,包括 EOF 导致的截断。异常情况下文档的约定**并没有承诺**仅仅写入 M 大小的区域,而不污染 (M, COUNT] 的区域。尽管 Linux 下 glibc 做了这件没承诺的事情,但 libc 并没有这个责任。

所以,如果不考虑 EOF 以外的读取异常,用 fread 读取一个最终读到尺寸可能小于预期尺寸的文件,应该手动补上终结符 '\0' 以解决余下的区域可能的污染问题。

test_data.c
 char * alloc_read_test_data(FILE *stream, size_t max_len)
 {
     char *buffer;
     size_t nread;

     /* allocate buffer */
     buffer = malloc(sizeof(char) * max_len);
     memset(buffer, 0, sizeof(char) * max_len);

     /* read test data */
     nread = fread(buffer, sizeof(char), max_len, stream)
     if (!nread)
         return NULL;

     buffer[nread] = '\0';  /* for safe */
     return buffer;
 }

困了想睡觉…… 要不明天接着写吧?

[0]尹剑飞老师,在深大的同学可以考虑选或者旁听他的课
[1]Yet Another Application Server Based on Lua and SCGI
[2]fread - C++ Reference

Comments