c++标准文件流文件尾符的处理原理

标准文件流中对文件结尾符处理的原理是: eof()判断流标识位的eofbit是否设置了,若是则返回-1,文件结束。

    bool __CLR_OR_THIS_CALL eof() const
        {    // test if eofbit is set in stream state
        return ((int)rdstate() & (int)eofbit);
        }

这么看来,在每个对文件读取而导致文件指针移动的标准流函数中,如ifstream::read(),c++标准文件流系统应该负责检测文件读取缓存中可用字符是否已读完,即流指针是否已到文件结束符。然而,它却忘记了!看下面的代码:

    _Myt& __CLR_OR_THIS_CALL read(_Elem *_Str, streamsize _Count)
        {    // read up to _Count characters into buffer
        _DEBUG_POINTER(_Str);
        ios_base::iostate _State = ios_base::goodbit;
        _Chcount = 0;
        const sentry _Ok(*this, true);

        if (_Ok)
            {    // state okay, use facet to extract
            _TRY_IO_BEGIN
            const streamsize _Num = _Myios::rdbuf()->sgetn(_Str, _Count);
            _Chcount += _Num;
            if (_Num != _Count)
                _State |= ios_base::eofbit | ios_base::failbit;    // short read
            _CATCH_IO_END
            }

        _Myios::setstate(_State);
        return (*this);
        }

上面的read()函数仅在检测到实际读取的字符数小于所要求读取的字符数时才设置文件结束标记。这太大意了,或者是故意为之,因为标准流还提供了另一函数fstream::peek(),它用来预读下一个字符,文件指针并不移动,若peek()发现正在预读的字符是文件结束符则会设置文件结束标识eofbit,这时我们使用fstream::eof()来测试文件是否结束时才正确! peek()代码如下:

int_type __CLR_OR_THIS_CALL peek()
        {    // return next character, unconsumed
        ios_base::iostate _State = ios_base::goodbit;
        _Chcount = 0;
        int_type _Meta = 0;
        const sentry _Ok(*this, true);

        if (!_Ok)
            _Meta = _Traits::eof();    // state not okay, return EOF
        else
            {    // state okay, read a character
            _TRY_IO_BEGIN
            if (_Traits::eq_int_type(_Traits::eof(),
                _Meta = _Myios::rdbuf()->sgetc()))
                _State |= ios_base::eofbit;
            _CATCH_IO_END
            }

        _Myios::setstate(_State);
        return (_Meta);
        }

peek()详解:sgetc()判断读缓存中可用字符是否大于0, 是,则返回所读字符;否,返回EOF=-1。 然后流状态的eofbit被设置,标识着文件结束了; 这里的_Traits::eof()是标准文件流内部处理使用的,总是返回常量EOF,不是程序开发者使用的ifstream::eof();

    int_type __CLR_OR_THIS_CALL sgetc()
        {    // get a character and don't point past it
        return (0 < _Gnavail() // 判断读取缓存rdbuf中可用字符数是否大于0,若是则返回当前指针gptr()所在处的字符*gptr(), 否则取指针下一位置的字符,即是文件尾符-1了.
            ? _Traits::to_int_type(*gptr()) : underflow());
        }

注意,文件结束符-1是不可用字符。即当文件读取缓存rdbuf中可用字符数为0时,缓存中只剩下一个文件结尾符了,它的值就是-1;

综上所述,c++标准文件流判断文件结束符的代码框架如下:

    std::ifstream ifs("abc.txt", std::ios::binary);
    
    while(!ifs.eof())
    {
        ifs.read(...);
        ifs.peek();
    }

错误框如下,就少了一行peek();

    std::ifstream ifs("abc.txt", std::ios::binary);
    
    while(!ifs.eof())
    {
        ifs.read(...);
    }

将-1作为文件结束符是否有问题呢,正常的二进制文件内容中非常可能也会出现-1。没问题,因为还有另一个限制条件:缓存中可用字符数是否为0,若缓存可用字符数大于0,则出现的-1是正常的数据,不是流结束符。

进一步的理解:

1,文件尾符并不在文件中,比如新建一个空文件abc.txt, 它的大小就是0字节而非1字节。

2,文件尾符存在于标准文件流的读缓冲区rdbuf中,是流系统自动加入的一个值-1。

基于以上原理, 若我们故意向文件中写入一个字符char c = -1,按字符char读出来后仍是-1, 若改用peek()预读一个字节时返回整数255。这与计算机中数据的存储原理有关。

简单的说是,正数按原码存,最高位补0; 负数按其绝对值的补码存(按位取反末位加1),最高位补1。如char c = -1, 真值(绝对值)的二进制为 0000001,按位取反末位加1,得1111111,高位补1作符号为 11111111, 转换为整数是 0x000000ff = 255。

当读缓存中可用字符为0时,系统返回一个整数值-1给peek(),然后peek()将此-1返回;

因此用peek()也能区分文件尾符-1与正常数据-1。由此上,文件结尾也可以如下处理, 遇到char c = -1时 peek()返回255;

    std::ifstream ifs("of.txt", std::ios::binary);
    
    while(ifs.peek() != EOF)
    {
        //ifs.read((char*)ta, 2*ii);
        ifs.read(&ct, 1);
        //ifs.peek();
    }