老齐教室

EOF是不是字符

编译:老齐

与本文相关的图书推荐:《Python大学实用教程》

本书是面向零基础学习者的Python入门读物,包含完整的Python语法知识、针对性的练习题,本书强调学习中的实战,案例和习题均从开发实践的角度进行设计。


什么是 EOF

百度百科上这样解释:EOF是一个计算机术语,为End Of File的缩写,在操作系统中表示资料源无更多的资料可读取。资料源通常称为档案或串流。通常在文本的最后存在此字符表示资料结束。

在这个解释中,认为EOF是表示文件结束的字符——这就是本文要重点讨论的,EOF是不是一个字符?

在Unix、Linux系统上,用C语言读写文件,经常会遇到EOF。之所以很多人认为EOF是一个字符串,可能是因为在C语言的程序中,会用getchar()getc()检查是否遇到了EOF。

1
2
3
4
#include <stdio.h>
...
while ((c = getchar()) != EOF)
putchar(c);

或者:

1
2
3
4
5
FILE *fp;
int c;
...
while ((c = getc(fp)) != EOF)
putc(c, stdout);

这样,getchar()getc()都从输入中获取下一个字符。 因此,这可能导致我们对EOF的本质感到困惑。当然,这仅仅是一种猜测。下面看看另外的理由。

什么是字符?字符可以看成是文本的最小组成党委,比如A, b, B等都是字符。在Unicode字符集中,每个字符都对应一个数字编码,例如大写字母A的字符编码是65(用十进制表示)。在Python 3中,可以这样查看:

1
2
3
4
>>> ord('A')
65
>>> chr(65)
'A'

或者,也可以在Unix/Linux中这样查看:

1
$ man ascii

下面用一下段C语言程序,来看看EOF。在ANSI C中,EOF在<stdio.h>标准库中,它的数字编码值一般是-1。将下面的程序保存为printeof.c,并运行:

1
2
3
4
5
6
7
#include <stdio.h>

int main(int argc, char *argv[])
{
printf("EOF value on my system: %d\n", EOF);
return 0;
}
1
2
3
4
$ gcc -o printeof printeof.c

$ ./printeof
EOF value on my system: -1

在Mac OS和Ubuntu系统上测试,都是输出-1

那么,那个“字符”的数字编码是-1呢?

那就用前面演示的Python中的函数,来检索一下,看看-1对应的字符是什么。

1
2
3
4
5
# 在Python交互模式中
>>> chr(-1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: chr() arg not in range(0x110000)

没有!在ASCII字符集中没有任何一个字符的数字编码是-1

所以,现在可以断言:EOF不是一个字符

再换一个角度考察。

如果EOF是字符,你就能在文件末尾“看”到它。下面检测一下文本文件helloworld.txt的内容,并且用xxd指令输出这个文件的二进制/十六进制形式。

1
2
3
4
5
$ cat helloworld.txt
Hello world!

$ xxd helloworld.txt
00000000: 4865 6c6c 6f20 776f 726c 6421 0a Hello world!.

在以十六进制表示的输出内容中,此文件是以0a结尾的,那么这个0a是什么呢?

1
2
3
# Python交互模式
>>> chr(0x0a)
'\n'

事实再次说明,EOF不是字符。

它是什么?

EOF(end-of-file)是操作系统内核提供的一个条件,它可以被程序检测到。

下面我们来看一下,几种不同的编程语言在通过高级I/O接口读一个文本文件的时候,是如何检测到这条件的(用于检测的所有程序,可以从代码仓库获得:https://github.com/rspivak/2x25/tree/master/eofnotchar)

  1. C语言程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /* mcat.c */
    #include <stdio.h>

    int main(int argc, char *argv[])
    {
    FILE *fp;
    int c;

    if ((fp = fopen(*++argv, "r")) == NULL) {
    printf("mcat: can't open %s\n", *argv);
    return 1;
    }

    while ((c = getc(fp)) != EOF)
    putc(c, stdout);

    fclose(fp);

    return 0;
    }

    编译:

    1
    $ gcc -o mcat mcat.c

    执行:

    1
    2
    $ ./mcat helloworld.txt
    Hello world!
    • 此程序通过命令行参数打开一个文件
    • while循环一次一个字节地将文件中的内容复制到标准输出,一直到文件末尾
    • 如果遇到EOF,则关闭文件,并返回客户端
  2. Python 3 程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # mcat.py
    import sys

    with open(sys.argv[1]) as fin:
    while True:
    c = fin.read(1) # read max 1 char
    if c == '': # EOF
    break
    print(c, end='')
    1
    2
    $ python mcat.py helloworld.txt
    Hello world!

    在Python3.8+中,还可以用海象运算符,精简程序:

    1
    2
    3
    4
    5
    6
    # mcat38.py
    import sys

    with open(sys.argv[1]) as fin:
    while (c := fin.read(1)) != '': # read max 1 char at a time until EOF
    print(c, end='')
    1
    2
    $ python3.8 mcat38.py helloworld.txt
    Hello world!
  3. Go 程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // mcat.go
    package main

    import (
    "fmt"
    "os"
    "io"
    )

    func main() {
    file, err := os.Open(os.Args[1])
    if err != nil {
    fmt.Fprintf(os.Stderr, "mcat: %v\n", err)
    os.Exit(1)
    }

    buffer := make([]byte, 1) // 1-byte buffer
    for {
    bytesread, err := file.Read(buffer)
    if err == io.EOF {
    break
    }
    fmt.Print(string(buffer[:bytesread]))
    }
    file.Close()
    }
    1
    2
    $ go run mcat.go helloworld.txt
    Hello world!
  4. JavaScript(node.js)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /* mcat.js */
    const fs = require('fs');
    const process = require('process');

    const fileName = process.argv[2];

    var readable = fs.createReadStream(fileName, {
    encoding: 'utf8',
    fd: null,
    });

    readable.on('readable', function() {
    var chunk;
    while ((chunk = readable.read(1)) !== null) {
    process.stdout.write(chunk); /* chunk is one byte */
    }
    });

    readable.on('end', () => {
    console.log('\nEOF: There will be no more data.');
    });
    1
    2
    3
    4
    $ node mcat.js helloworld.txt
    Hello world!

    EOF: There will be no more data.

    上面的示例中的高级I/O例程如何确定文件结束条件?

在Linux系统上,例程直接或间接使用内核提供的read()系统调用,例如,C语言中的getc()使用read()系统调用,当指示到end-of-file条件,则返回EOread()`系统调用返回0代表EOF条件。

下面把前面的C语言程序改写一下,注意观察:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* syscat.c */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
int fd;
char c;

fd = open(argv[1], O_RDONLY, 0);

while (read(fd, &c, 1) != 0)
write(STDOUT_FILENO, &c, 1);

return 0;
}
1
2
3
4
$ gcc -o syscat syscat.c

$ ./syscat helloworld.txt
Hello world!

上面的代码中,注意观察read()函数,返回的0就代表EOF,当然,Pytyon程序也可以改写。

1
2
3
4
5
6
7
8
9
10
11
# syscat.py
import sys
import os

fd = os.open(sys.argv[1], os.O_RDONLY)

while True:
c = os.read(fd, 1)
if not c: # EOF
break
os.write(sys.stdout.fileno(), c)
1
2
$ python syscat.py helloworld.txt
Hello world!

Python3.8+的程序

1
2
3
4
5
6
7
8
# syscat38.py
import sys
import os

fd = os.open(sys.argv[1], os.O_RDONLY)

while c := os.read(fd, 1):
os.write(sys.stdout.fileno(), c)
1
2
3
# 执行结果
$ python3.8 syscat38.py helloworld.txt
Hello world!

至此,应该明确了一下几点:

  • EOF不是Unicode字符集中的字符
  • 在Unix/Linux系统中,文件的最后找不到所谓的EOF字符,根本就没有这样一个字符
  • EOF是程序能够检测到的Unix/Linux内核提供的一个条件

参考资料:https://ruslanspivak.com/eofnotchar/

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

关注微信公众号,读文章、听课程,提升技能