如果你曾经学过“比较低级”的高级编程语言,例如 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
。