zoukankan      html  css  js  c++  java
  • 走进C标准库(3)——"stdio.h"中的getc和ungetc

    接前文。

    再来看看getc和ungetc的实现。在看这两个函数的实现之前,我们先来想一想这两个函数分别需要做的工作。

    int getc(FILE *stream)

    说明:函数getc从stream指向的输入流中读取下一个字符(如果有的话),并把它由unsigned char类型转换为int类型,并且流的相关的文件定位符(如果定义的话)向前移动一位。

    返回值:函数getc返回stream指向的输入流的下一个字符,如果流处于文件结束处,则设置该流的文件结束指示符,函数getc返回EOF。如果发生了读错误,则设置流的错误指示符。函数getc返回EOF。

    int ungetc( int c , FILE *stream );

    说明:

    函数ungetc把c指定的字符(转换为unsigned char类型)退回到stream指向的输入流中,退回字符是通过对流的后续读取并按照退回的反顺序来返回的。如果中间成功调用(对同一个流)了一个文件定位函数(fseek、fsetpos或者rewind),那么就会丢弃流的所有退回的字符。流的相应的外部存储保持不变。

    退回的一个字符是受到保护的。如果同一个流调用ungetc函数太多次,中间又没有读或者文件定位操作,那么这种操作可能会失败。

    如果c的值和宏EOF的值相等,则操作失败且输入流保持不变。

    对ungetc的成功调用会清空流的文件结束符。读取或者丢弃所有回退的字符后,流的文件定位符的值和这些字符回退之前的值相同。对文本流来说,在对函数ungetc的一次成功调用之后,它的文件定位符的值是不明确的,直到所有回退字符被读取或者丢弃为止。对二进制流来说,每成功调用一次ungetc之后它的文件定位符都会减1。如果在一次调用之前它的值是零,那么调用之后它的值是不确定的。

    返回值:

    函数ungetc返回转换后的回退字符,如果操作失败,则返回EOF。

    大致来说,getc就是从stream中读入一个字符,并将流的定位符前移;ungetc就是将流的定位符后移,然后将一个字符回退到stream流该定位符的位置上。

    这时我们可能会考虑到一些问题:

    1.在stdio.h阅读笔记1中,我们看到在fopen函数调用之后,并没有为我们的stream分配buffer,那么这个分配会在真正进行stream读取的时候建立吗?

    2.如果在getc之前(即流定位符在_base)就进行ungetc操作,会产生什么样的效果呢?

    3.如原文所说:“如果同一个流调用ungetc函数太多次,中间又没有读或者文件定位操作,那么这种操作可能会失败。”,是因为文件定位符到达_base的原因吗?

    那我们就来看下getc和ungetc所做的工作:

    首先来看ungetc

    int __cdecl ungetc (
            REG2 int ch,
            FILE *str
            )
    {
            REG1 FILE *stream;
            _ASSERTE(str != NULL);
            /* Init stream pointer and file descriptor */
            stream = str;
            /* Stream must be open for read and can NOT be currently in write mode.
               Also, ungetc() character cannot be EOF. */
            if (
                  (ch == EOF) ||
                  !(
                    (stream->_flag & _IOREAD) ||
                    ((stream->_flag & _IORW) && !(stream->_flag & _IOWRT))
                   )
               )
                    return(EOF);
            /* If stream is unbuffered, get one. */
            if (stream->_base == NULL)
                    _getbuf(stream);
            /* now we know _base != NULL; since file must be buffered */
            if (stream->_ptr == stream->_base) {
                    if (stream->_cnt)
                            /* my back is against the wall; i've already done
                             * ungetc, and there's no room for this one
                             */
                            return(EOF);
                    stream->_ptr++;
            }
            if (stream->_flag & _IOSTRG) {
                /* If stream opened by sscanf do not modify buffer */
                    if (*--stream->_ptr != (char)ch) {
                            ++stream->_ptr;
                            return(EOF);
                    }
            } else
                    *--stream->_ptr = (char)ch;
            stream->_cnt++;
            stream->_flag &= ~_IOEOF;
            stream->_flag |= _IOREAD;       /* may already be set */
            return(0xff & ch);
    }

    清楚的看到,对于没有分配buffer的I/O控制块,进行了缓冲区的分配。另外,当文件定位符指向文件开始时且数据存储区域剩余字符数目为0时,文件定位符无需后移,修改的是文件开始位置的字符;其他情况下,都是文件定位符先后移,再修改当前文件定位符指向的字符。完成这些之后,修改I/O控制块中的_cnt剩余字符计数和_flag标记。

    看了下_getbuf函数的实现,从中可以看到FILE类中_charbuf变量的作用。删除了针对不同各个平台的#if代码之后,_getbuf函数核心代码如下:

     1 void __cdecl _getbuf (
     2         FILE *str
     3         )
     4 {
     5         REG1 FILE *stream;
     6         _ASSERTE(str != NULL);
     7         stream = str;
     8         if (stream->_base = _malloc_crt(_INTERNAL_BUFSIZ))
     9         {
    10                 stream->_flag |= _IOMYBUF;
    11                 stream->_bufsiz = _INTERNAL_BUFSIZ;
    12         }
    13         else {
    14                 stream->_flag |= _IONBF;
    15                 stream->_base = (char *)&(stream->_charbuf);
    16                 stream->_bufsiz = 2;
    17                 stream->_ptr = stream->_base;
    18                 stream->_cnt = 0;
    19         return;
    20 }

    可以看到,当_getbuf分配不成功时,_base指针会指向_charbuf作为一个1 int长度的空间(可能是因为平台的原因,这里将1int理解为2个char),此时的_buffersize就为2。通过_charbuf的使用,也保证了当buffer分配失败时,也能正常从文件中读取数据。

    接下来看下getc函数的实现:

    #define getc(_stream)     (--(_stream)->_cnt >= 0 \
                    ? 0xff & *(_stream)->_ptr++ : _filbuf(_stream))

    表达式 (--(_stream)->_cnt >= 0 ? 0xff & *(_stream)->_ptr++ : _filbuf(_stream)) 的意义为:

    数据存储区域剩余字符的数目大于0的时候,减少一个剩余字符计数,返回0xff & *(_stream)->_ptr++;

    否则,调用_filbuf(_stream)。

    通过0xff & *(_stream)->_ptr++,getc函数获得了当前文件定位符指向的内容(只取8位?难道char还能不止8位吗?),然后文件定位符前移

    剩下函数_filbuf(_stream),显然其中含有C语言从文件中获取文本的方法(前面都是文本的管理机制):

     1 int __cdecl _filbuf (
     2         FILE *str
     3         )
     4 {
     5         REG1 FILE *stream;
     6         _ASSERTE(str != NULL);
     7         /* Init pointer to _iob2 entry. */
     8         stream = str;
     9         if (!inuse(stream) || stream->_flag & _IOSTRG)
    10                 return(_TEOF);
    11         if (stream->_flag & _IOWRT) {
    12                 stream->_flag |= _IOERR;
    13                 return(_TEOF);
    14         }
    15         stream->_flag |= _IOREAD;
    16         /* Get a buffer, if necessary. */
    17         if (!anybuf(stream))
    18                 _getbuf(stream);
    19         else
    20                 stream->_ptr = stream->_base;
    21         stream->_cnt = _read(_fileno(stream), stream->_base, stream->_bufsiz);
    22         if ((stream->_cnt == 0) || (stream->_cnt == -1)) {
    23                      stream->_flag |= stream->_cnt ? _IOERR : _IOEOF;
    24                 stream->_cnt = 0;
    25                 return(_TEOF);
    26         }
    27 
    28         if (  !(stream->_flag & (_IOWRT|_IORW)) &&
    29               ((_osfile_safe(_fileno(stream)) & (FTEXT|FEOFLAG)) ==
    30                 (FTEXT|FEOFLAG)) )
    31                 stream->_flag |= _IOCTRLZ;
    32         /* Check for small _bufsiz (_SMALL_BUFSIZ). If it is small and
    33            if it is our buffer, then this must be the first _filbuf after
    34            an fseek on a read-access-only stream. Restore _bufsiz to its
    35            larger value (_INTERNAL_BUFSIZ) so that the next _filbuf call,
    36            if one is made, will fill the whole buffer. */
    37         if ( (stream->_bufsiz == _SMALL_BUFSIZ) && (stream->_flag &
    38               _IOMYBUF) && !(stream->_flag & _IOSETVBUF) )
    39         {
    40                 stream->_bufsiz = _INTERNAL_BUFSIZ;
    41         }
    42         stream->_cnt--;
    43         return(0xff & *stream->_ptr++);
    44 }

    在语句stream->_cnt = _read(_fileno(stream), stream->_base, stream->_bufsiz);中通过调用_read函数来将原来在硬盘文件中的数据读取到程序数据段的栈空间中,具体的功能看下_read的代码:

      1 /***
      2 *int _read(fh, buf, cnt) - read bytes from a file handle
      3 *
      4 *Purpose:
      5 *       Attempts to read cnt bytes from fh into a buffer.
      6 *       If the file is in text mode, CR-LF's are mapped to LF's, thus
      7 *       affecting the number of characters read.  This does not
      8 *       affect the file pointer.
      9 *
     10 *       NOTE:  The stdio _IOCTRLZ flag is tied to the use of FEOFLAG.
     11 *       Cross-reference the two symbols before changing FEOFLAG's use.
     12 *
     13 *Entry:
     14 *       int fh - file handle to read from
     15 *       char *buf - buffer to read into
     16 *       int cnt - number of bytes to read
     17 *
     18 *Exit:
     19 *       Returns number of bytes read (may be less than the number requested
     20 *       if the EOF was reached or the file is in text mode).
     21 *       returns -1 (and sets errno) if fails.
     22 *
     23 *Exceptions:
     24 *
     25 *******************************************************************************/
     26 
     27 int __cdecl _read (
     28         int fh,
     29         void *buf,
     30         unsigned cnt
     31         )
     32 {
     33         int bytes_read;                 /* number of bytes read */
     34         char *buffer;                   /* buffer to read to */
     35         int os_read;                    /* bytes read on OS call */
     36         char *p, *q;                    /* pointers into buffer */
     37         char peekchr;                   /* peek-ahead character */
     38         ULONG filepos;                  /* file position after seek */
     39         ULONG dosretval;                /* o.s. return value */
     40 
     41         /* validate fh */
     42         if ( ((unsigned)fh >= (unsigned)_nhandle) ||
     43              !(_osfile(fh) & FOPEN) )
     44         {
     45             /* bad file handle */
     46             errno = EBADF;
     47             _doserrno = 0;              /* not o.s. error */
     48             return -1;
     49         }
     50         bytes_read = 0;                 /* nothing read yet */
     51         buffer = buf;
     52         if (cnt == 0 || (_osfile(fh) & FEOFLAG)) {
     53             /* nothing to read or at EOF, so return 0 read */
     54             return 0;
     55         }
     56         if ((_osfile(fh) & (FPIPE|FDEV)) && _pipech(fh) != LF) {
     57             /* a pipe/device and pipe lookahead non-empty: read the lookahead
     58              * char */
     59             *buffer++ = _pipech(fh);
     60             ++bytes_read;
     61             --cnt;
     62             _pipech(fh) = LF;           /* mark as empty */
     63         }
     64 
     65         /* read the data */
     66 
     67         if ( !ReadFile( (HANDLE)_osfhnd(fh), buffer, cnt, (LPDWORD)&os_read,
     68                         NULL ) )
     69         {
     70             /* ReadFile has reported an error. recognize two special cases.
     71              *
     72              *      1. map ERROR_ACCESS_DENIED to EBADF
     73              *
     74              *      2. just return 0 if ERROR_BROKEN_PIPE has occurred. it
     75              *         means the handle is a read-handle on a pipe for which
     76              *         all write-handles have been closed and all data has been
     77              *         read. */
     78 
     79             if ( (dosretval = GetLastError()) == ERROR_ACCESS_DENIED ) {
     80                 /* wrong read/write mode should return EBADF, not EACCES */
     81                 errno = EBADF;
     82                 _doserrno = dosretval;
     83                 return -1;
     84             }
     85             else if ( dosretval == ERROR_BROKEN_PIPE ) {
     86                 return 0;
     87             }
     88             else {
     89                 _dosmaperr(dosretval);
     90                 return -1;
     91             }
     92         }
     93         bytes_read += os_read;          /* update bytes read */
     94         if (_osfile(fh) & FTEXT) {
     95             /* now must translate CR-LFs to LFs in the buffer */
     96 
     97             /* set CRLF flag to indicate LF at beginning of buffer */
     98             if ( (os_read != 0) && (*(char *)buf == LF) )
     99                 _osfile(fh) |= FCRLF;
    100             else
    101                 _osfile(fh) &= ~FCRLF;
    102 
    103             /* convert chars in the buffer: p is src, q is dest */
    104             p = q = buf;
    105             while (p < (char *)buf + bytes_read) {
    106                 if (*p == CTRLZ) {
    107                     /* if fh is not a device, set ctrl-z flag */
    108                     if ( !(_osfile(fh) & FDEV) )
    109                         _osfile(fh) |= FEOFLAG;
    110                     break;              /* stop translating */
    111                 }
    112                 else if (*p != CR)
    113                     *q++ = *p++;
    114                 else {
    115                     /* *p is CR, so must check next char for LF */
    116                     if (p < (char *)buf + bytes_read - 1) {
    117                         if (*(p+1) == LF) {
    118                             p += 2;
    119                             *q++ = LF;  /* convert CR-LF to LF */
    120                         }
    121                         else
    122                             *q++ = *p++;    /* store char normally */
    123                     }
    124                     else {
    125                         /* This is the hard part.  We found a CR at end of
    126                            buffer.  We must peek ahead to see if next char
    127                            is an LF. */
    128                         ++p;
    129 
    130                         dosretval = 0;
    131                         if ( !ReadFile( (HANDLE)_osfhnd(fh), &peekchr, 1,
    132                                         (LPDWORD)&os_read, NULL ) )
    133                             dosretval = GetLastError();
    134                         if (dosretval != 0 || os_read == 0) {
    135                             /* couldn't read ahead, store CR */
    136                             *q++ = CR;
    137                         }
    138                         else {
    139                             /* peekchr now has the extra character -- we now
    140                                have several possibilities:
    141                                1. disk file and char is not LF; just seek back
    142                                   and copy CR
    143                                2. disk file and char is LF; seek back and
    144                                   discard CR
    145                                3. disk file, char is LF but this is a one-byte
    146                                   read: store LF, don't seek back
    147                                4. pipe/device and char is LF; store LF.
    148                                5. pipe/device and char isn't LF, store CR and
    149                                   put char in pipe lookahead buffer. */
    150                             if (_osfile(fh) & (FDEV|FPIPE)) {
    151                                 /* non-seekable device */
    152                                 if (peekchr == LF)
    153                                     *q++ = LF;
    154                                 else {
    155                                     *q++ = CR;
    156                                     _pipech(fh) = peekchr;
    157                                 }
    158                             }
    159                             else {
    160                                 /* disk file */
    161                                 if (q == buf && peekchr == LF) {
    162                                     /* nothing read yet; must make some
    163                                        progress */
    164                                     *q++ = LF;
    165                                 }
    166                                 else {
    167                                     /* seek back */
    168                                     filepos = _lseek_lk(fh, -1, FILE_CURRENT);
    169                                     if (peekchr != LF)
    170                                         *q++ = CR;
    171                                 }
    172                             }
    173                         }
    174                     }
    175                 }
    176             }
    177 
    178             /* we now change bytes_read to reflect the true number of chars
    179                in the buffer */
    180             bytes_read = q - (char *)buf;
    181         }
    182         return bytes_read;              /* and return */
    183 }

    通过调用ReadFile的WINAPI函数,就可以完成从磁盘中把文件的内容读到数据段的栈内了。

    BOOL WINAPI ReadFile(
      _In_         HANDLE hFile,
      _Out_        LPVOID lpBuffer,
      _In_         DWORD nNumberOfBytesToRead,
      _Out_opt_    LPDWORD lpNumberOfBytesRead,
      _Inout_opt_  LPOVERLAPPED lpOverlapped
    );

    通过使用已经分配好的文件句柄,将数据读到lpBuffer指向的内存区域,读入的最大数据量通过nNumberOfBytesToRead变量进行设定,通过lpNumberOfBytesRead返回实际读取的字节数。

    _read函数在读到数据之后,如果设定的模式为文本流,则需要解决文本流中的换行回车的问题,这里就不深究了。有兴趣可以看文章:

    http://www.360doc.com/content/11/0113/20/3508740_86319358.shtml

    这样,我们的getc就成功将FILE类的_cnt设定为了剩余字符的数目,将_base指向了通过栈上存储数据的地址。然后,只要将当前文件位置指针_ptr中的数据返回,_ptr指向下一个位置,_cnt减一就好了。

    这时候,我们回过头来看FILE结构,就可以知道里面成员变量的意义了。

     1 typedef struct _iobuf {
     2     char *_ptr; 
     3     int _cnt; 
     4     char *_base; 
     5     int _flag; 
     6     int _file; 
     7     int _charbuf; 
     8     int _bufsiz; 
     9     char *_tmpfname;
    10 }FILE;

    _ptr为数据存储区域(可为缓冲区或_charbuf对应的存储区域)当前位置指针。

    _cnt为数据存储区域剩余字符数目。

    _base为数据存储区域首地址。

    _flag为当前I/O控制块所控制的I/O操作类型,有读、写等。

    _file为对应的文件描述符(详见博文《走进C标准库(2)》)

    _charbuf为缓冲区申请失败时的一个2字节的数据存储区

    _bufsiz为缓冲区大小

    _tmpfname为所使用存储一些中间操作数据的临时文件。

    到这里,我们就可以回答开始时的3个问题了。

    1.在getc和ungetc中如果没有分配buffer,都会尝试给I/O控制块分配buffer,但是ungetc分配完buffer后不会尝试从文件中将数据读到buffer中。

    2.可以在buffer的首地址处存储一个字符。如果_ptr指在_base,那么数据存储区域为空(为从文件中读取数据)时,ungetc才能在首地址处添加一个字符。

    3.多次ungetc使_ptr到达_base之后,就无法再进行ungetc了。

    就算黑夜也有星光。
  • 相关阅读:
    python 安装mysql-python模块
    Linux 目录结构
    python logging 配置
    mac下docker使用笔记
    centos yum 使用笔记
    python 正则使用笔记
    django 动态更新属性值
    nginx 反向代理
    yii2 高级版新建一个应用(api应用为例子)
    tfn2k工具使用介绍
  • 原文地址:https://www.cnblogs.com/shijiezhenmei/p/3659346.html
Copyright © 2011-2022 走看看