有时候我们需要使用 C 语言读取外部文件中的数据,或者是将计算处理后的结果写入到外部文件中,这时候就需要对文件的操作了。
文件流
C 语言在处理文件的时候是将文件看成了字符的序列,也可以说成是字符流或者是文件流。也就是说不同的文件可能只是文件内容的组织格式不同,但是交给计算机处理的时候都是大致相似的。
文件类型
不同的文件有不同的类型,详细的可以看这篇文章
乱码问题
先看下边的程序:
#include <stdio.h>
int main()
{
int a = 123456;
FILE *fp = fopen("ASCII.txt","w");
fprintf(fp,"%d",a);
fclose(fp);
FILE *fd = fopen("BIN.txt","w");
fwrite(&a,2,1,fd);
fclose(fd);
return 0;
}
在 BIN.txt 文件中会出现不能理解的东西,这就是通常所说的乱码。
对于某个文件来说,程序或者进程首先读取文件存储位置上的二进制比特流,然后按照所选择的解码方式对流进行解读,然后将解释结果按照我们选择的方式呈现出来,比如文本编辑器工具。
通常情况下,ASCII 码一般会使用与之对应的解码方式,然后程序会 8 位 8 位地读取流从而进行显示。而二进制数据在计算机内部不是以 ASCII 码形式保存的,再用 ASCII 码的解码方式打开就会出现乱码。
文件缓冲
通常我们使用 printf 函数进行输出时,会习惯性的在后方加上 “\n”,除了使光标换行,还有一个比较重要的作用就是刷新缓冲区,如果在连续的 printf 语句后方都没有 “\n”,可能间隔一段时间才会输出,这也是因为缓冲区的存在,也就是说:
- 缓冲区能够整合文件和内存的速度
- 缓冲区有一定的空间大小
- 刷新缓冲区能够输出当前缓冲区的内容
那么为什么要有缓冲区:
- 内存的读取速度和文件的读取速度有很大的差别
- 文件读写需要用到类如 open、read、write 等系统底层函数,而用户进程每次调用系统函数都要从用户态切换到内核态,执行完成后再返回用户态,这种切换是有成本的
文件的打开和关闭
FILE
FILE 是定义的结构体类型,能够记录缓冲区和文件读写状态,也就是说,对文件的操作可以认为是通过 FILE 完成的。
FILE 大致是这么个东西:
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
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;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};
fopen/fclose
fopen
语法
#include <stdio.h> FILE *fopen( const char *fname, const char *mode );
描述
- fopen() 函数打开由 fname(文件名) 指定的文件,并返回一个关联该文件的流
- 如果发生错误 fopen() 返回 NULL
- mode(方式) 是用于决定文件的用途(例如 用于输入,输出,等等),mode 都包含:
"r" 打开一个用于读取的文本文件 "w" 创建一个用于写入的文本文件 "a" 附加到一个文本文件 "rb" 打开一个用于读取的二进制文件 "wb" 创建一个用于写入的二进制文件 "ab" 附加到一个二进制文件 "r+" 打开一个用于读/写的文本文件 "w+" 创建一个用于读/写的文本文件 "a+" 打开一个用于读/写的文本文件 "rb+" 打开一个用于读/写的二进制文件 "wb+" 创建一个用于读/写的二进制文件 "ab+" 打开一个用于读/写的二进制文件 上面的 mode 在某些场景下会有不同的处理方式:
mode 作用 当文件不存在时 当文件存在时 文件写入 文件读取 r 读取 error 打开文件 no yes w 写入 建立新文件 覆盖原有文件 yes no a 追加 建立新文件 在原有文件后追加 yes no r+ 读取/写入 error 打开文件 yes no w+ 写入/读取 建立新文件 覆盖原有文件 yes no a+ 读取/追加 建立新文件 在原有文件后追加 yes no 但是,unix/linux 不区分文本和二进制文件。
fclose
语法
#include <stdio.h> int fclose( FILE *stream );
描述
- 函数fclose()关闭给出的文件流, 释放已关联到流的所有缓冲区
- fclose() 执行成功时返回 0,否则返回 EOF
其它文件操作函数
clearerr
语法
#include <stdio.h> void clearerr( FILE *stream );
描述
- clearerr 函数重置错误标记和给出的流的 EOF 指针
- 当发生错误时,你可以使用 perror() 判断实际上发生了何种错误
feof
语法
#include <stdio.h> int feof( FILE *stream );
描述
- 函数 feof() 在到达给出的文件流的文件尾时返回一个非零值
ferror
语法
#include <stdio.h> int ferror( FILE *stream );
描述
- ferror() 函数检查 stream(流) 中的错误, 如果没发生错误返回 0,否则返回非零
- 如果发生错误, 使用 perror() 检测发生什么错误
fflush
语法
#include <stdio.h> int fflush( FILE *stream );
描述
- 如果给出的文件流是一个输出流,那么 fflush() 把输出到缓冲区的内容写入文件
- 如果给出的文件流是输入类型的,那么 fflush() 会清除输入缓冲区
- fflush() 在调试时很实用,特别是对于在程序中输出到屏幕前发生错误片段时,直接调用 fflush(STDOUT) 输出可以保证你的调试输出可以在正确的时间输出
fgetc
语法
#include <stdio.h> int fgetc( FILE *stream );
描述
- fgetc() 函数返回来自 stream(流) 中的下一个字符
- 如果到达文件尾或者发生错误时返回 EOF
fgetpos
语法
#include <stdio.h> int fgetpos( FILE *stream, fpos_t *position );
描述
- fgetpos() 函数保存给出的文件流(stream)的位置指针到给出的位置变量(position)中
- position变量是 fpos_t 类型的(它在stdio.h中定义)并且是可以控制在 FILE 中每个可能的位置对象
- fgetpos() 执行成功时返回 0,失败时返回一个非零值
fgets
语法
#include <stdio.h> char *fgets( char *str, int num, FILE *stream );
描述
- 函数 fgets() 从给出的文件流中读取 [num - 1] 个字符并且把它们转储到 str(字符串) 中
- fgets() 在到达行末时停止,在这种情况下,str(字符串) 将会被一个新行符结束
- 如果 fgets() 达到 [num - 1] 个字符或者遇到 EOF, str(字符串) 将会以 null 结束
- fgets() 成功时返回str(字符串),失败时返回 NULL.
fprintf
语法
#include <stdio.h> int fprintf( FILE *stream, const char *format, ... );
描述
- fprintf() 函数根据指定的 format(格式)(格式) 发送信息(参数)到由 stream(流) 指定的文件
- fprintf() 只能和 printf() 一样工作
- fprintf() 的返回值是输出的字符数,发生错误时返回一个负值
fputc
语法
#include <stdio.h> int fputc( int ch, FILE *stream );
描述
函数 fputc() 把给出的字符ch写到给出的输出流
返回值是字符,发生错误时返回值是 EOF
fputs
语法
#include <stdio.h> int fputs( const char *str, FILE *stream );
描述
fputs() 函数把str(字符串)指向的字符写到给出的输出流
成功时返回非负值, 失败时返回 EOF
fread
语法
#include <stdio.h> int fread( void *buffer, size_t size, size_t num, FILE *stream );
描述
函数 fread() 读取 [num] 个对象(每个对象大小为size(大小)指定的字节数),并把它们替换到由buffer(缓冲区)指定的数组
数据来自给出的输入流
函数的返回值是读取的内容数量
使用 feof() 或 ferror() 判断到底发生哪个错误
freopen
语法
#include <stdio.h> FILE *freopen( const char *fname, const char *mode, FILE *stream );
描述
freopen() 函数常用于再分配一个已存在的流给一个不同的文件和方式(mode)
在调用本函数后,给出的文件流将会用mode(方式)指定的访问模式引用fname(文件名)
freopen() 的返回值是新的文件流,发生错误时返回NULL.
fscanf
语法
#include <stdio.h> int fscanf( FILE *stream, const char *format, ... );
描述
函数 fscanf() 以 scanf() 的执行方式从给出的文件流中读取数据
fscanf() 的返回值是事实上已赋值的变量的数,如果未进行任何分配时返回 EOF
fseek
语法
#include <stdio.h> int fseek( FILE *stream, long offset, int origin );
描述
函数 fseek() 为给出的流设置位置数据
origin 的值应该是下列值其中之一(在 stdio.h 中定义):
名称 说明 SEEK_SET 从文件的开始处开始搜索 SEEK_CUR 从当前位置开始搜索 SEEK_END 从文件的结束处开始搜索
- fseek() 成功时返回 0,失败时返回非零
- 可以使用 fseek() 移动超过一个文件,但是不能在开始处之前
- 使用 fseek() 清除关联到流的 EOF 标记.
fsetpos
语法
#include <stdio.h> int fsetpos( FILE *stream, const fpos_t *position );
描述
fsetpos() 函数把给出的流的位置指针移到由position对象指定的位置
fpos_t 是在 stdio.h 中定义的
fsetpos() 执行成功返回 0,失败时返回非零.
ftell
语法
#include <stdio.h> long ftell( FILE *stream );
描述
ftell() 函数返回stream(流)当前的文件位置,如果发生错误返回 -1
fwrite
语法
#include <stdio.h> int fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
描述
fwrite() 函数从数组buffer(缓冲区)中, 写count个大小为size(大小)的对象到stream(流)指定的流
返回值是已写的对象的数量
remove
语法
#include <stdio.h> int fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
描述
remove() 函数删除由fname(文件名)指定的文件
remove() 成功时返回 0,如果发生错误返回非零.
rename
语法
#include <stdio.h> int rename( const char *oldfname, const char *newfname );
描述
函数 rename() 更改文件oldfname的名称为newfname
rename() 成功时返回 0,错误时返回非零
rewind
语法
#include <stdio.h> void rewind( FILE *stream );
描述
函数 rewind() 把文件指针移到由stream(流)指定的开始处,同时清除和流相关的错误和 EOF 标记
setbuf
语法
#include <stdio.h> void setbuf( FILE *stream, char *buffer );
描述
setbuf() 函数设置stream(流)使用buffer(缓冲区),如果buffer(缓冲区)是null,关闭缓冲
如果使用非标准缓冲尺寸,它应该由BUFSIZ字符决定长度.
setvbuf
语法
#include <stdio.h> int setvbuf( FILE *stream, char *buffer, int mode, size_t size );
描述
函数 setvbuf() 设置用于stream(流)的缓冲区到buffer(缓冲区),其大小为size(大小)
mode(方式)可以是:
_IOFBF 完全缓冲 _IOLBF 线缓冲 _IONBF 无缓存 ungetc
语法
#include <stdio.h> int ungetc( int ch, FILE *stream );
描述
函数 ungetc() 把字符ch放回到stream(流)中
注意事项
先看一个程序:
#include <stdio.h>
int main()
{
FILE *fp = fopen("test.txt","w+");
char buf[26];
char ch;
if (NULL == fp)
{
printf("FILE OPEN ERROR!");
return -1;
}
for (int i = 0;i < 26;i++)
{
buf[i] = (char)fputc((char)(97+i),fp);
printf("buf[%d] = %c\n",i,buf[i]);
}
rewind(fp);
while((ch = fgetc(fp)) != EOF)
printf("fgetc(fp) = %c\n",ch);
fclose(fp);
return 0;
}
结果为:
buf[0] = a
buf[1] = b
buf[2] = c
buf[3] = d
buf[4] = e
buf[5] = f
buf[6] = g
buf[7] = h
buf[8] = i
buf[9] = j
buf[10] = k
buf[11] = l
buf[12] = m
buf[13] = n
buf[14] = o
buf[15] = p
buf[16] = q
buf[17] = r
buf[18] = s
buf[19] = t
buf[20] = u
buf[21] = v
buf[22] = w
buf[23] = x
buf[24] = y
buf[25] = z
fgetc(fp) = a
fgetc(fp) = b
fgetc(fp) = c
fgetc(fp) = d
fgetc(fp) = e
fgetc(fp) = f
fgetc(fp) = g
fgetc(fp) = h
fgetc(fp) = i
fgetc(fp) = j
fgetc(fp) = k
fgetc(fp) = l
fgetc(fp) = m
fgetc(fp) = n
fgetc(fp) = o
fgetc(fp) = p
fgetc(fp) = q
fgetc(fp) = r
fgetc(fp) = s
fgetc(fp) = t
fgetc(fp) = u
fgetc(fp) = v
fgetc(fp) = w
fgetc(fp) = x
fgetc(fp) = y
fgetc(fp) = z
由上边的结果可以知道:
- 和利用 malloc 进行内存分配所要进行的步骤一样,利用 FILE 进行的文件操作也要经过打开、判空、读写、关闭等步骤
- 要根据所要进行的操作,选择合适的 mode 打开文件
- 要利用好各个函数的返回值
- 未关闭文件前对文件进行读取和写入操作时,要注意当前流的位置
- 利用 fgetc 可以判断是否到文件尾,一般都会加在读取文件之前,作为结束条件
- 可以利用 fgetc,gputc 函数进行单个字符的读取和写入
- 同时可以利用 fputs,fgets 函数进行整行文本的读取和写入
#include <stdio.h> int main() { FILE *fp = fopen("test.txt","w+"); char ch[10]; if (NULL == fp) { printf("FILE OPEN ERROR!"); return -1; } for (int i = 0;i < 26;i++) fputs("abc\n",fp); rewind(fp); while(fgets(ch,10,fp) != NULL) printf("fgets(fp) = %s",ch); fclose(fp); return 0; }
只是要注意是 char ch[10],而不是 char *ch,如果定义指针的话,由于没有初始化,对应写入的地址是不确定的。
- 也可以利用 fwrite,fread 函数进行整块文本的读取和写入(二进制操作)
#include <stdio.h> #include <string.h> int main() { FILE *fp = fopen("test.txt","w+"); char ch[10]; if (NULL == fp) { printf("FILE OPEN ERROR!"); return -1; } char *p = "abcdefg \n hijklmn \0 opqrst \t uvwxyz \\"; fwrite(p,1,strlen(p),fp); rewind(fp); while(fgets(ch,10,fp) != NULL) printf("%s",ch); putchar(10); putchar(10); rewind(fp); fwrite(p,1,100,fp); rewind(fp); while(fgets(ch,10,fp) != NULL) printf("%s",ch); putchar(10); fclose(fp); return 0; }
结果为:
abcdefg hijklmn abcdefg hijklmn uvwxyz \;0�����������
也就是说,使用 fread,fwrite 时,是按照块读取的,而并不关心块里的内容。并且 fread 的返回值有时候也会不是我们想象的那样:
#include <stdio.h> int main() { FILE *fp = fopen("test.txt","r+"); char ch[100]; int n; if (NULL == fp) { printf("FILE OPEN ERROR!"); return -1; } n = fread(ch,1,100,fp); printf("n = %d\n",n); rewind(fp); n = fread(ch,100,1,fp); printf("n = %d\n",n); putchar(10); rewind(fp); n = 1; while(n != 0) { n = fread(ch,4,1,fp); printf("n = %d\n",n); } putchar(10); rewind(fp); n = 1; while(n != 0) { n = fread(ch,1,4,fp); printf("n = %d\n",n); } fclose(fp); return 0; }
test.txt 中的内容为:
aaaaaaaaaa bbbbbbbbbb cccccccccc dddddddddd eeeeeeeeee
结果为:
n = 55 n = 0 n = 1 n = 1 n = 1 n = 1 n = 1 n = 1 n = 1 n = 1 n = 1 n = 1 n = 1 n = 1 n = 1 n = 0 n = 4 n = 4 n = 4 n = 4 n = 4 n = 4 n = 4 n = 4 n = 4 n = 4 n = 4 n = 4 n = 4 n = 3 n = 0
从上面的结果可以看出:
- fread 的返回值是读取的内容数量,而该数组最大不会超过字节块数的值
- 根据字节数和字节块数设置的不同,fread 的执行次数可能会不同
- 一般情况下,设置字节数为 1,能够读取实际的文件大小
- 字节数设置的过大,如果读不满该字节数返回为 0
- 既然 fread,fwrite 是针对块进行读写的,那么就可以对文件进行一些特殊操作,即加密等
- 之前提到过的 feof 函数也要慎重使用
feof 函数是读标志位 EOF 来判断文件是否结束的。换句话说就是该函数在读到文件尾部的时候再读一次,标志位才会置位,函数才会给出文件结束的返回值。
#include <stdio.h> int main() { FILE *fp = fopen("test.txt","w+"); char buf[10]; char ch; if (NULL == fp) { printf("FILE OPEN ERROR!"); return -1; } for (int i = 0;i < 10;i++) { buf[i] = (char)fputc((char)(97+i),fp); printf("buf[%d] = %c\n",i,buf[i]); } printf("***********************\n"); rewind(fp); while((ch = fgetc(fp)) != EOF) printf("fgetc(fp) = %c\n",ch); printf("***********************\n"); rewind(fp); while(!feof(fp)) printf("fgetc(fp) = %c\n",fgetc(fp)); printf("***********************\n"); rewind(fp); while((ch = fgetc(fp)) && !feof(fp)) printf("fgetc(fp) = %c\n",ch); printf("***********************\n"); fclose(fp); return 0; }
从上边的结果可以看出:
- 中间一种输出方式是有问题的,多输出了一次
- 第一,第三中输出方式是正确的,但建议采用第一种方式,简单
- feof 函数的返回值是个 int
再看看如果是整行读取的结果:
#include <stdio.h> int main() { FILE *fp = fopen("test.txt","r+"); char buf[10]; if (NULL == fp) { printf("FILE OPEN ERROR!"); return -1; } while(fgets(buf,10,fp)) printf("fgets(fp) = %s\n",buf); printf("***********************\n"); rewind(fp); while(!feof(fp)) printf("fgets(fp) = %s\n",fgets(buf,10,fp)); printf("***********************\n"); rewind(fp); while(fgets(buf,10,fp) && !feof(fp)) printf("fgets(fp) = %s\n",buf); printf("***********************\n"); fclose(fp); return 0; }
test.txt 中的内容为:
aaaaaaa bbbbbbb ccccccc ddddddd eeeeeee
结果为:
fgets(fp) = aaaaaaa fgets(fp) = bbbbbbb fgets(fp) = ccccccc fgets(fp) = ddddddd fgets(fp) = eeeeeee *********************** fgets(fp) = aaaaaaa fgets(fp) = bbbbbbb fgets(fp) = ccccccc fgets(fp) = ddddddd fgets(fp) = eeeeeee fgets(fp) = (null) *********************** fgets(fp) = aaaaaaa fgets(fp) = bbbbbbb fgets(fp) = ccccccc fgets(fp) = ddddddd fgets(fp) = eeeeeee ***********************
可以看出,中间一种输出方式是有问题的,多输出了一次。
- windows 和 linux 的换行也是不同的
先看下边的程序:
//Linux 中,wb,w,rb,r 的任意组合方式运行结果都是一样的 #include <stdio.h> int main() { FILE *fp = fopen("test.txt","wb"); char ch = '\n'; if (NULL == fp) { printf("FILE OPEN ERROR!"); return -1; } fputc('a',fp); fputc('b',fp); fputc(ch,fp); fclose(fp); fp = fopen("test.txt","rb"); while((ch = fgetc(fp)) != EOF) printf("bit = %x\n",ch); fclose(fp); return 0; }
结果为:
bit = 61 bit = 62 bit = a
再看下边的程序:
//Windows 中,wb-rb,wb-r,w-r 的输出结果与 Linux 相同,但是 w-rb 的输出结果就有差别了 #include <stdio.h> int main() { FILE *fp = fopen("test.txt","w"); char ch = '\n'; if (NULL == fp) { printf("FILE OPEN ERROR!"); return -1; } fputc('a',fp); fputc('b',fp); fputc(ch,fp); fclose(fp); fp = fopen("test.txt","rb"); while((ch = fgetc(fp)) != EOF) printf("bit = %x\n",ch); fclose(fp); return 0; }
结果为:
bit = 61 bit = 62 bit = d bit = a
- windows 文本方式写入时,遇到“\n”(0A),自动将其换为“\r\n”(0D0A),然后再写入文件
- windows 文本方式读取时,遇到“\r\n”(0D0A),自动将其换为“\n”(0A),然后再读入缓存
- 二进制读写时,不存在转换,直接将缓存区中的数据写入文件
- 以 wb 方式写文件时,写进文件的"\n",windows 的文本编辑工具不能识别
- 以 rb 方式读文件时,windows 会将“\n“读成”\r\n“,可能会给程序带来影响
- 这一点在跨平台开发的时候尤其要注意