念一叨

EOL、EOF……它们到底是啥?

如果你曾经学过“比较低级”的高级编程语言,例如 C,一定会被它里面的字符串结尾文件结束符搞蒙过。 在 C 的设定里,字符串是一列 char 类型的数据,由 '\0' 标记着结尾; 读取文件时,当所有文件内容读取完毕后,会返回 EOF的特殊值,其值等于 -1; 又有时,人们会提到用来标记一行字符结尾的 EOL。 这些东西到底是啥?它们之间有什么关系?它们都在 ASCII 里有着特殊的码位吗? 本文就来解答这些疑惑。

“C 风味”的字符串

在高级语言里,「字符串」一词的实际含义通常是「不可变的字符数组」。 这是什么意思? 这就是说,字符串是一列字符类型的数据,有确定的长度;并且你总可以按下标访问随机访问任意位置的字符。 但是它与一般数组不一样,是个值类型;你对原字符串做出写入或更改操作,得到的是一整个新的字符串(所以说「不可变」)。

但是在 C 里,「字符串」的意思似乎和我们直觉里的那回事一点不沾边。 这里,字符串是指「一列在内存中连续且以 '\0' 结尾的 char 类型数据的首位地址」。 (这个 '\0' 在数值上正好就是比特位全零的字节,很方便。) 我们来看看这是在说啥。

比如说内存里有这样的一串字节:

第一行是内存里的数据值,第二行是相对内存地址(前后还有其他空间)。 现在你有这一串里第首个字节的内存地址,用来表示这个“字符串”。 当需要用到这个“字符串”的时候,程序会从首字节开始向后逐个遍历,直到找到第一个值为 '\0' 的字节。 (在 ASCII 里,一个字符的宽度就是一个字节,所以这里「字节」与字符等效。) 找到之后,程序就认为这个“字符串”结束了,'\0' 前一个字符就是“字符串”的最后一个字符。 把首个字节到 '\0' 之间的数据连起来,就得到这个“字符串”的内容 HELLO 了。 (当然,你在写 char[] str = "HELLO"; 时编译器会自动帮你把末尾的 '\0' 补上)

以上便是 C 语言中储存“字符串”的方法,但显然这与我们直觉中的字符串相去甚远。 与其说这是一种数据结构,不如说是一种约定。 C 语言中处理字符串的函数总是预期会看到一个 '\0' 的终止标记; 开发者在使用“字符串”时,只需要给出起始字节的内存地址即可。 这种数据结构并没有固定的大小,全看内存里的实际数据是怎么编排的; 而那个终止标记也实质上包含在结构里面,是不可去掉的成分。

由于某种原因这个终止标记不存在了,就会导致访问越界甚至安全隐患(参考 OpenSSH 著名的「心血漏洞」)。

这样定义的“字符串”并不是不可变的,因为它是引用的内存地址。 譬如要修改某个位置的字符,可以直接修改对应字节的值而不必重新分配整段字符串的内存空间。 又譬如要在某个位置截断字符串,就可以简单地把那个位置后面的一个字节置零。

EOF

EOF 是文件结束符(end-of-file),用于在读取文件流时指示已经读取完毕的状态。

广义地说,「流」是指一种类似于数组但又不完全一样的线性数据源。 流是单向的。 开发者可以很方便地从一个流中读取新的数据,但是已经读取过的数据不能再跳转回去重新读取。 同时,流不一定有确切的长度。 因此,在使用流时,程序必须在数据读取完毕之后结束读取,否则就会陷入一直读取下一个数据的死循环中。

所谓「文件流」,就是读取文件中数据的流。 在 C 语言标准库中,可以用函数 fgetc(pFile) 来从文件流中读取下一个字节(fgetc 就是「file get char」的缩写)。 在所有数据读取完毕之后,fgetc 就会返回 EOF,告知上游程序读取完毕的状态。 (这个 EOF 的值固定为 -1。)

看上去与 C 风格字符串的结尾很像,但是有一个重大区别: C 风格字符串的结尾是其数据的一部分,而 EOF 这一字节本身则并不包含于原本的文件之中,是 C 语言标准库所定义的文件流读取完毕之后的固定行为。 由于 ASCII 中并不含有 -1 所对应的字符,因此在处理纯 ASCII 文本文件时,EOF 并不会造成歧义。 因此,与其说是「文件结束“符”」,不如说 EOF 是「文件结束」这一事件的标志(flag,而非标记)。

当然,在处理其他编码或干脆是二进制文件时,EOF 有可能造成歧义; 但也没人会用 fgetc 来读取它们,而是提前用 fseek 确定文件大小,然后直接载入到内存里。

EOL

EOL 是行结束符(end-of-line)。

其实并不是。

EOL 是文本数据之间分行的标记。 通常,这个标记就是操作系统所使用的换行符(在 Windows 下为 CR LF,在类 UNIX 系统下为 CR); 但视实际应用,这标记可以是任意的,并没有什么通用标准。 例如,在 C++ 里使用 std::cin 读取字符串时,会默认以一切空白字符作为分隔符;一次只能读入一段连续非空白文本。 再宽泛地说,编程语言里用来分隔语句的符号(如 ;)都可以算作是 EOL

从这个角度上看,EOL 和 C 风格字符串里的 '\0' 别无二致,只不过后者是 C 标准库中的通用设定,而前者则需要开发者自己去实现处理逻辑罢了。

标准输入流

最后要提的一点:不仅在读取文件时会遭遇 EOF,从命令行里直接接受输入时也会遭遇 EOF。 Well,更准确地说应该是从标准输入流(standard input stream,stdin)中接受输入时。

通常,C/C++ 初学者会跟着教程做一些控制台里运行的黑框程序,用户可以在控制台里直接键入要输入的数据。 但那并不是控制台程序最常见的使用方法。 一般,控制台程序的数据并不直接从控制台里键入,而是接受其他程序的输出作为其输入。 (这样,用户就可以藉由图形界面或者 bash 来进行高阶操作。) 这种程序之间嘴对嘴喂食的管道,就叫做「标准输入/输出流」。

接收数据的一方只需要关心标准输入流即可。 而如前面所说,这个标准输入流,它也是个流,因此操作方法与文件流完全一样。 (开发者甚至不需要特地传进去文件指针,因为 C 标准库默认的目标就是标准输入输出流。)

书本上的示例程序会使用 getline 函数来实现「等待用户按下回车」的功能; 在实际程序里,这样做就会使得输入流跳到最近的一个 EOL 之后。 同理,getchar 函数可以从控制台里读取单个字符; 在实际程序里,如果已经到了输入流的末尾,getchar 同样会返回 EOF