IO详解三 C 语言 IO 原理与接口 Shepard-Wang

四 C 语言 IO 原理与接口

1、原理

io11.jpg

int main()
{	
	printf("printf ");
	write(1, "write ", 6);	

	fork();
	return 0;
}

输出:

write printf printf #   

这是 stdio 自带缓冲,而操作系统 io 无缓冲的又一证明。c 库 printf 函数打印了两次,而系统调用只打印了一次。

fork 创建子进程的时候,子进程“继承”了父进程还没有刷新到内核的缓冲区。当其中任一进程要刷新该缓冲的时候,会发生 写时拷贝,拷贝一份缓冲区并进行刷新,另一进程会再刷新一次缓冲区。

2、接口

fopen

FILE *fopen( const char *filename, const char *mode );(C99 前)

FILE *fopen( const char *restrict filename, const char *restrict mode );(C99 起)

filename - 关联到文件系统的文件名 mode - 确定访问模式的空终止字符串

若成功,则返回指向新文件流的指针。流为完全缓冲,除非 filename 表示一个交互设备。错误时,返回空指针

![](io9.png

fopen 默认是自带缓冲区的,可以使用 setvbuf() 自定义缓冲区。使用 fflush() 刷新缓冲区到内核缓冲中。

fflush

int fflush(FILE *stream );

对于输出流(及最后操作为输出的更新流),从 stream 的缓冲区写入未写的数据到关联的输出设备。

对于输入流(及最后操作为输入的更新流),行为未定义。

stream 是空指针,则冲入所有输出流,包括操作于库包内者,或在其他情况下程序无法直接访问者。

参数:stream - 要写出的文件流

返回值:成功时返回零。否则返回 EOF

调用:

fflush(fp);

为和 fp 相关联的文件清洗了缓冲区。调用:

fflush(NULL);

清洗了全部输出流。

setvbuf

int setvbuf(FILE *stream, char *buffer, int mode, size_t size );(C99 前)

int setvbuf(FILE *restrict stream, char *restrict buffer, int mode, size_t size );(C99 起)

mode 所指示值更改给定文件流 stream 的缓冲模式。另外,

  • buffer 为空指针,则重设内部缓冲区大小为 size
  • buffer 不是空指针,则指示流使用始于 buffer 而大小为 size 的用户提供缓冲区。必须在 buffer 所指向的数组的生存期结束前(用 fclose )关闭流。成功调用 setvbuf 后,数组内容不确定,而任何使用它的尝试是未定义行为。

参数:

stream-要设置缓冲的文件流
buffer-指向要使用的流缓冲区的指针,或若仅更改大小和模式则为空指针
mode-使用的缓冲模式。它能是下列值之一:_IOFBF全缓冲:当缓冲区为空时,从流读入数据。或者当缓冲区满时,向流写入数据。_IOLBF行缓冲:每次从流中读入一行数据或向流中写入一行数据。_IONBF无缓冲:直接从流中读入数据或直接向流中写入数据,缓冲设置无效。
size-缓冲区的大小

返回值:成功时为 0 ,失败时为非零。

例如,下面这个 setvbuf 函数的调用利用 buffer 数组中的 N 个字节作为缓冲区,而把 stream 的缓冲变成了满缓冲:

char buffer[N];
...
setvbuf(stream, buffer, _IOFBF, N);

注意:此函数仅可在已将 stream 关联到打开的文件后,但要在任何其他操作前使用。

setbuf (discard)

void setbuf(FILE *stream, char *buffer );(C99前)

void setbuf(FILE *restrict stream, char *restrict buffer );(C99起)

设置用于流操作的内部缓冲区。其长度至少应该为 BUFSIZ 个字符。

buffer 非空,则等价于 setvbuf(stream, buffer, _IOFBF, BUFSIZ) 。

buffer 为空,则等价于 setvbuf(stream, NULL, _IONBF, 0) ,这会关闭缓冲。

参数:

stream-要设置缓冲区的文件流
buffer-指向文件流所用的缓冲区的指针。若提供 NULL ,则关闭缓冲。

我们把 setbuf 看作是陈旧的内容,不建议大家在新程序中使用。

注意:使用 setvbuf 函数或 setbuf 函数时,一定要确保在释放缓冲区之前已经关闭了流。特别是,如果缓冲区是局部函数的,并且具有自动存储期限,一定要确保在函数返回前关闭流。

fgets

char* fgets( char *str, int count, FILE *stream );(C99前)

char* fgets( char *restrict str, int count, FILE *restrict stream );(C99起)

定义:从给定文件流读取最多 count - 1 个字符并将它们存储于 str 所指向的字符数组。若文件尾出现或发现换行符则终止分析,后一情况下 str 将包含一个换行符。若读入字节且无错误发生,则紧随写入到 str 的最后一个字符后写入空字符。

参数:

str - 指向 char 数组元素的指针

count - 写入的最大字符数(典型的为 str 的长度)

stream - 读取数据来源的文件流

返回值:

成功时为 str ,失败时为空指针。

若遇到文件尾条件导致了失败,则设置 stream 上的文件尾指示器(见 feof() )。这仅若它导致未读取字符才是失败,该情况下返回空指针且不改变 str 所指向数组的内容(即不以空字符覆写首字节)。

若某些其他错误导致了失败,则设置 stream 上的错误指示器(见 ferror() )。 str 所指向的数组内容是不确定的(甚至可以不是空终止)。

fgets 读取文件时,如果行尾包含换行符,则会一并读取

fgets(buf, sizeof buf, pf);	

if(ferror(pf))
{
    perror("error:");
    exit(2);	
}

puts(buf);

假设读取的文件如下:

ni hao

则该程序会输出:

ni hao#这个换行符时原文件的
#这里多了一行是因为 puts 会多输出一个换行符
检测文件读取错误和文件结尾

void clearerr(FILE *stream );

头文件: <stdio.h>

定义:重置给定文件流的错误标志和 EOF 指示器。

参数:stream - 要重置错误标志的文件流

int feof(FILE *stream );

头文件: <stdio.h>

定义:检查是否已抵达给定文件流的结尾。

参数:stream - 要检验的文件流

返回值: 若已抵达流尾则为非零值,否则为 0

int ferror(FILE *stream );

头文件: <stdio.h>

定义:检查给定文件流的错误。

参数:stream - 要检验的文件流

返回值: 若文件流已出现错误则为非零值,否则为 0

如果要求 ...scanf函数读入并存储 n 个数据项,那么希望它的返回值就是 n 。如果返回值小于 n ,那么一定是出错了。一共有 3 种可能:

  • 文件末尾 。函数再完全匹配格式字符串之前遇到了文件末尾。
  • 读取错误 。函数不能从流中读取字符。
  • 匹配失败 。数据项的格式是错误的。

每个流都有与之相关的两个指示器:错误指示器(error indicator)和文件末尾指示器(end-of-file indicator),当打开流时会清除这些指示器。遇到文件末尾就设置文件末尾指示器,遇到读错误就设置错误指示器。(输出流上的写错误也设置错误指示器。)匹配失败不会改变任何一个指示器。

一旦设置了错误指示器和文件末尾指示器,它就会保持这种状态直到被显示清除(可通过 clearerr 函数)。clearerr 会同时清除两个指示器:

clearerr(fp);

我们可以调用 feof函数和 ferror函数来测试的指示器,从而确定出先前在流上的操作失败的原因。

为了清楚这两个函数的用法,我们现在来编写一个函数。此函数用来搜索文件以整数起始的行。下面时函数调用的方式:

n = find_int("foo");

其中,"foo"要搜索的文件的名字。

int find_int(const char* filename) {
	
	FILE* fp = fopen(filename, "r");
	int n;

	if (fp == NULL)
		return -1; // can't open file

	while (fscanf(fp, "%d", &n) != 1) {
		if (ferror(fp)) {
			fclose(fp);
			return -2; // input error
		}
		if (feof(fp)) {
			fclose(fp);
			return -3; // interger not find
		}
		fscanf(fp, "%*[^\n]"); // skips rest of lines
	}

	fclose(fp);
	
	return n;
}

注意转换说明%*[^\n]跳过全部字符直到下一个换行符为止的用法。

fputs

int fputs( const char *str, FILE *stream );(C99前)

int fputs( const char *restrict str, FILE *restrict stream );(C99起)

头文件:<stdio.h>

定义:将以NULL结尾的字符串 str 的每个字符写入到输出流 stream ,如同通过重复执行 fputc

不将 str 的空字符写入。

参数:

str - 要写入的空终止字符串

stream - 输出流

返回值:

成功时,返回非负值。

失败时,返回 EOF 并设置 stream 上的错误指示器。


不同的实现返回不同的非负数:一些返回最后写入的字符,一些返回写入的字符数(或若字符串长于 INT_MAX 则为该值),一些简单地非负常量,例如零。

注意:相关函数 puts 后附新换行符到输出,而 fputs 写入不修改的字符串。

对注意的理解:

	puts("Hello ");
	puts("World");

	fputs("Hello ", stdout);
	fputs("World", stdout);

输出:

Hello
World
Hello World
fscanf

int fscanf( FILE *stream, const char *format, ... );(C99 前)

int fscanf( FILE *restrict stream, const char *restrict format, ... );(C99 起)

从文件流 stream 读取数据

参数: stream - 要读取的输入文件流

buffer - 指向要读取的空终止字符串的指针

format - 指向指定读取输入方式的空终止字符串的指针。

返回值: 成功赋值的接收参数的数量(可以为零,在首个接收用参数赋值前匹配失败的情况下),者若输入在首个接收用参数赋值前发生失败,则为EOF。

注意: fscanf 会跳过开头的空白字符,遇到第一个非空白字符后,如果遇到了空白字符,则会结束输入。 如果像让 fscanf 读取整行数据,format 应该使用 %[^\n] ,表示读取除了换行外的所有字符。但是 fscanf 不会跳过换行符,会将换行符放回流中,需要使用 fgetc() 将换行符跳过。

fscanf(pf, "%[^\n]", buf);
fgetc(pf);
ungetc

int ungetc ( int character, FILE * stream );

Unget character from stream

A character is virtually put back into an input stream, decreasing its internal file position as if a previous getc operation was undone.

This character may or may not be the one read from the stream in the preceding input operation. In any case, the next character retrieved from stream is the character passed to this function, independently of the original one.

Notice though, that this only affects further input operations on that stream, and not the content of the physical file associated with it, which is not modified by any calls to this function.

If successful, the function clears the end-of-file indicator of stream (if it was currently set), and decrements its internal file position indicator if it operates in binary mode; In text mode, the position indicator has unspecified value until all characters put back with ungetc have been read or discarded.

A call to fseek, fsetpos or rewind on stream will discard any characters previously put back into it with this function.

If the argument passed for the character parameter is EOF, the operation fails and the input stream remains unchanged

若调用 ungetc 多于一次,而无中间读取或重寻位,则可能失败(换言之,保证大小为 1 的回放缓冲区,但任何更大的缓冲区是实现定义的)。若成功进行多次 ungetc ,则读取操作以 ungetc 的逆序取得回放的字符。

Return Value

  • On success, the character put back is returned.
  • If the operation fails, EOF is returned.

ungetc 这个函数很有意思,首先它支持将任意(非 EOF)字符“放回流中”。但是 fseekfsetpos,和rewind 函数调用会消除此前任何 ungetc 的调用。如果 ungetc 真的将流改变了,那么消除 ungetc 的效果有两种可能的实现:

  1. ungetc时对原本的 buffer 做了备份
  2. 重新调用 read 函数从内核缓存中取出该 buffer

答案是前者。我们先来看一下 FILE 结构体都有什么内容:

/* The tag name of this struct is _IO_FILE to preserve historic
   C++ mangled names for functions taking FILE* arguments.
   That name should not be used in new code.  */
struct _IO_FILE
{
  int _flags;                /* High-order word is _IO_MAGIC; rest is flags. */
  
   /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;        /* Current read pointer */
  char *_IO_read_end;        /* End of get area. */
  char *_IO_read_base;       /* Start of putback+get area. */
  
  char *_IO_write_base;      /* Start of put area. */
  char *_IO_write_ptr;       /* Current put pointer. */
  char *_IO_write_end;       /* End of put area. */
  
  char *_IO_buf_base;        /* Start of reserve area. */
  char *_IO_buf_end;         /* End of reserve area. */
  
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base;       /* Pointer to start of non-current get area. */
  char *_IO_backup_base;     /* Pointer to first valid character of backup area */
  char *_IO_save_end;        /* Pointer to end of non-current get area. */
  
   struct _IO_marker *_markers;
  struct _IO_FILE *_chain;
  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

下面这三个成员就是负责给 buffer 备份的:

char *_IO_save_base;       /* Pointer to start of non-current get area. */
char *_IO_backup_base;     /* Pointer to first valid character of backup area */
char *_IO_save_end;        /* Pointer to end of non-current get area. */

ungetc 函数的实现:

#include "libioP.h"
int
_IO_ungetc (int c, FILE *fp)
{
  int result;
  CHECK_FILE (fp, EOF);
  if (c == EOF)
    return EOF;
  if (!_IO_need_lock (fp))
    return _IO_sputbackc (fp, (unsigned char) c);
  _IO_acquire_lock (fp);
  result = _IO_sputbackc (fp, (unsigned char) c);
  _IO_release_lock (fp);
  return result;
}

_IO_sputbackc 的实现:

int
_IO_sputbackc (FILE *fp, int c)
{
  int result;
  if (fp->_IO_read_ptr > fp->_IO_read_base
      && (unsigned char)fp->_IO_read_ptr[-1] == (unsigned char)c)
    {
      fp->_IO_read_ptr--;
      result = (unsigned char) c;
    }
  else
    result = _IO_PBACKFAIL (fp, c);
  if (result != EOF)
    fp->_flags &= ~_IO_EOF_SEEN;
  return result;
}

_IO_PBACKFAIL (fp, c);实际调用了 JUMP1 ,glibc 中没有该函数(?或者某种指令)的实现:

/* The 'pbackfail' hook handles backing up.
   It matches the streambuf::pbackfail virtual function. */
typedef int (*_IO_pbackfail_t) (FILE *, int);
#define _IO_PBACKFAIL(FP, CH) JUMP1 (__pbackfail, FP, CH)
#define _IO_WPBACKFAIL(FP, CH) WJUMP1 (__pbackfail, FP, CH)

从注释中我们不难看出这个函数是负责备份的。

结合源代码和已有知识我们可以总结出 ungetc 的行为:

  • 如果参数 c 为 EOF(-1),直接返回 EOF
  • 如果参数 c 为先前调用 fgetc 等函数取出的字符,则直接修改文件位置指针
  • 如果参数并非原先 buffer 中读取的字符,则需要对 buffer 进行备份。字符 c 放回的是备份的 buffer
fseek

int fseek( FILE *stream, long offset, int origin );

定义于头文件 **参数**: stream - 要修改的文件流 offset - 相对 origin 迁移的字符数 origin - offset 所加上的位置。它能拥有下列值之一: SEEK_SET 、 SEEK_CUR 、 SEEK_END **返回值**: 成功时为 0 ,否则为非零。


定义: 设置文件流 stream 的文件位置指示器为 offset 所指向的值。

若 stream 以二进制模式打开,则新位置准确地是文件起始后(若 origin 为 SEEK_SET )或当前文件位置后(若 origin 为 SEEK_CUR ),或文件结尾后(若 origin 为 SEEK_END )的 offset 字节。不要求二进制流支持 SEEK_END ,尤其是是否输出附加的空字节。

stream 以文本模式打开,则仅有的受支持 offset 值为零(可用于任何 origin )和先前在关联到同一个文件的流上对 ftell 的调用的返回值(仅可用于 SEEK_SETorigin )。

stream 为宽面向,则一同应用对文本和二进制流的限制(允许 ftell 的结果与 SEEK_SET 一同使用,并允许零 offset 以 SEEK_SET 和 SEEK_CUR 但非 SEEK_END 为基准)。

除了更改文件位置指示器, fseek 还撤销 ungetc 的效果并清除文件尾状态,若可应用。

若发生读或写错误,则设置流的错误指示器( ferror )而不影响文件位置。

SEEK_SET 从头开始 SEEK_CUR 从当前位置开始 SEEK_END 从尾开始

来看一下 fseek 的实现:

int
fseek (FILE *fp, long int offset, int whence)
{
  int result;
  CHECK_FILE (fp, -1);
  _IO_acquire_lock (fp);
  result = _IO_fseek (fp, offset, whence);
  _IO_release_lock (fp);
  return result;
}

_IO_fseek

#define _IO_fseek(__fp, __offset, __whence) \
  (_IO_seekoff_unlocked (__fp, __offset, __whence, _IOS_INPUT|_IOS_OUTPUT) \
   == _IO_pos_BAD ? EOF : 0)

_IO_seekoff_unlocked

off64_t
_IO_seekoff_unlocked (FILE *fp, off64_t offset, int dir, int mode)
{
  if (dir != _IO_seek_cur && dir != _IO_seek_set && dir != _IO_seek_end)
    {
      __set_errno (EINVAL);
      return EOF;
    }
  /* If we have a backup buffer, get rid of it, since the __seekoff
     callback may not know to do the right thing about it.
     This may be over-kill, but it'll do for now. TODO */
  if (mode != 0 && ((_IO_fwide (fp, 0) < 0 && _IO_have_backup (fp))
                    || (_IO_fwide (fp, 0) > 0 && _IO_have_wbackup (fp))))
    {
      if (dir == _IO_seek_cur && _IO_in_backup (fp))
        {
          if (_IO_vtable_offset (fp) != 0 || fp->_mode <= 0)
            offset -= fp->_IO_read_end - fp->_IO_read_ptr;
          else
            abort ();
        }
      if (_IO_fwide (fp, 0) < 0)
        _IO_free_backup_area (fp);
      else
        _IO_free_wbackup_area (fp);
    }
  return _IO_SEEKOFF (fp, offset, dir, mode);
}

_IO_free_backup_area

void
_IO_free_backup_area (FILE *fp)
{
  if (_IO_in_backup (fp))
    _IO_switch_to_main_get_area (fp);  /* Just in case. */
  free (fp->_IO_save_base);
  fp->_IO_save_base = NULL;
  fp->_IO_save_end = NULL;
  fp->_IO_backup_base = NULL;
}

_IO_switch_to_main_get_area

/* Switch current get area from backup buffer to (start of) main get area. */
void
_IO_switch_to_main_get_area (FILE *fp)
{
  char *tmp;
  fp->_flags &= ~_IO_IN_BACKUP;
  /* Swap _IO_read_end and _IO_save_end. */
  tmp = fp->_IO_read_end;
  fp->_IO_read_end = fp->_IO_save_end;
  fp->_IO_save_end= tmp;
  /* Swap _IO_read_base and _IO_save_base. */
  tmp = fp->_IO_read_base;
  fp->_IO_read_base = fp->_IO_save_base;
  fp->_IO_save_base = tmp;
  /* Set _IO_read_ptr. */
  fp->_IO_read_ptr = fp->_IO_read_base;
}

_IO_free_backup_area_IO_switch_to_main_get_area 两个函数也为我们揭露了 buffer 的 back up 方式的一角。

fflush

向磁盘驱动器传入数据或者从磁盘驱动器传出数据都是相对较慢的操作。因此,在每次程序想读或写字符时都直接访问磁盘文件是不可行的。获得较好性能的诀窍就是缓冲(buffering):把写入流的数据存储在内存的缓冲区域内;当缓冲区满了(或者关闭流)时,对缓冲区进行“清洗”(写入实际的输出设备)。输入流可以用类似的方法进行缓冲:缓冲区包含来自输入设备的数据,从缓冲区读数据而不是从设备本身读数据。缓冲在效率上可以取得巨大的收益,因为从缓冲区读字符或者在缓冲区内存储字符几乎不花什么时间。当然,把缓冲区的内容传递给磁盘,或者从磁盘传递给缓冲区是需要花时间的,但是一次大的“块移动”比多次小字节移动要快很多。

<stdio.h>中的函数会在缓冲有用时自动进行缓冲操作。缓冲发生在后台,我们通常不需要关心它的操作。然而,极少情况需要我们起到更主动的作用。

当程序向文件中写输出时,数据通常会向放在缓冲区。当缓冲区满了或关闭了文件时,缓冲区会自动清洗。

int fflush(FILE *stream );

对于输出流(及最后操作为输出的更新流),从 stream 的缓冲区写入未写的数据到关联的输出设备。

对于输入流(及最后操作为输入的更新流),行为未定义。

stream 是空指针,则冲入所有输出流,包括操作于库包内者,或在其他情况下程序无法直接访问者。

参数:stream - 要写出的文件流

返回值:成功时返回零。否则返回 EOF

调用:

fflush(fp);

为和 fp 相关联的文件清洗了缓冲区。调用:

fflush(NULL);

清洗了全部输出流。

除了 fflush 外,fclose以及当 main 函数结束时都会刷新缓冲

3、练习

1.用C语言编程,打开/etc/passwd文件,找出已经注册的普通用户账号

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

const char* to_find = "/home/";

int main()
{
	FILE* pf = fopen("/etc/passwd", "r");
	if(pf == NULL)
	{
		perror("open file [/etc/passwd] failed");
		exit(1);
	}

	FILE* out = fopen("./usr.txt", "w");
	if(out == NULL)
	{
		perror("open file [./usr.txt] failed");
		exit(1);
	}

	char buf[1024] = {0};
	while(1)
	{
		fgets(buf, sizeof buf, pf);	
		// fscanf(pf, "%[^\n]", buf);
		// fgetc(pf);

		if(ferror(pf))
		{
			perror("error:");
			exit(2);	
		}
		printf("%s", buf);
		//puts(buf);
		if(feof(pf))
		{
			puts("end of file");
			break;
		}
		
		char *str = strstr(buf, to_find);
		if(str != NULL)
		{
			//fputs(, out);
			char* start = str + strlen(to_find);
			while(*start != 0 && *start != ':')
				fputc(*start++ ,out);
			// fputs 不像 puts 会在字符串结尾加空行
			fputs("\n", out);
		}		 
	}	
	
	fclose(pf);
    fclose(out);
	return 0;
}

2. 简易 scanf 实现

示例:展示 ungetc 的原目的:实现 scanf

#include <ctype.h>
#include <stdio.h>
 
void demo_scanf(const char* fmt, FILE* s) {
    if(*fmt == '%') {
        int c;
        switch(*++fmt) {
            case 'u': while(isspace(c=getc(s))) {} // 跳过空白符
                      unsigned int num = 0;
                      while(isdigit(c)) {
                          num = num*10 + c-'0';
                          c = getc(s);
                      }
                      printf("%%u scanned %u\n", num);
                      ungetc(c, s); // 重处理非数字
            case 'c': c = getc(s);
                      printf("%%c scanned '%c'\n", c);
        }
    } 
}
 
int main(void)
{
    FILE* f = fopen("input.txt", "r+");
    fputs("123x", f);
	// 将文件流的位置指示器置为文件开头,这一步必须要有
	fseek(f, 0L, SEEK_SET);
    demo_scanf("%u%c", f);
    fclose(f);
}

输出:

%u scanned 123
%c scanned 'x'

3. 对同一文件进行读写

usr.txt

line 1
line 2
line 2
line 4

main.c

int main()
{
	FILE* f = fopen("usr.txt", "r+");
	if(f == NULL) 
	{
		perror("open file failed");
		exit(1);
	}

	char buf[1024];
	
    // 读两次文件
	char* ret = fgets(buf, 1024, f);
	if(ferror(f))
	{
		perror("fgets failed");
		exit(2);
	}
	printf("%s", buf);

	fgets(buf, 1024, f);
	if(ferror(f))
	{
		perror("fgets failed");
		exit(2);
	}
	printf("%s", buf);
	
    // 写文件
	if( fputs(buf, f) == EOF )
	{
		perror("fputs failed");
		exit(3);
	}
	
    // 再读一次
	fgets(buf, 1024, f);
	if(ferror(f))
	{
		perror("fgets failed");
		exit(2);
	}
	printf("%s", buf);



	fclose(f);

	return 0;
}

输出如下:

line 1
line 2
line 4

文件变为:

line 1
line 2
line 2
line 4

读文件与写文件的操作共享同一个文件位置指示器。

什么是 EOF

如果一个文件含有n个字符,那么while循环的内部操作会运行n+1次。所以,最保险的写法是像下面这样:

int c = fgetc(fp);

while (c != EOF) {

    do something;

    c = fgetc(fp);

}

if (feof(fp)) {

    printf("\n End of file reached.");

} else {

    printf("\n Something went wrong.");

}

除了表示文件结尾,EOF还可以表示标准输入的结尾。

int c;

while ((c = getchar()) != EOF) {

	putchar(c);

}

但是,标准输入与文件不一样,无法事先知道输入的长度,必须手动输入一个字符,表示到达EOF。

Linux中,在新的一行的开头,按下Ctrl-D,就代表EOF(如果在一行的中间按下Ctrl-D,则表示输出”标准输入”的缓存区,所以这时必须按两次Ctrl-D);Windows中,Ctrl-Z表示EOF。(顺便提一句,Linux中按下Ctrl-Z,表示将该进程中断,在后台挂起,用 fg 命令可以重新切回到前台;按下Ctrl-C表示终止该进程。)

那么,如果真的想输入Ctrl-D怎么办?这时必须先按下Ctrl-V,然后就可以输入Ctrl-D,系统就不会认为这是EOF信号。Ctrl-V表示按”字面含义”解读下一个输入,要是想按”字面含义”输入Ctrl-V,连续输入两次就行了。

参考:

EOF是什么? - 阮一峰的网络日

查找命令
grep -rn "struct _IO_FILE {" --include="*.h" /usr/include