文件流
标准I/O文件流可用于单字节或多字节字符集。流的定向决定了所读写的是单字节还是多字节。流在最初创建时,并没有定向,此时如果在为定向的流上使用多字节I/O函数,那么该流被设置为宽定向的;如果在为定向的流中使用单字节I/O函数,那么该流被设置为字节定向的。
如下两个函数可用于改变流的定向:
#include <stdio.h> #include <wchar.h> int fwide(FILE* fp, int mode); 返回值:流为宽定向,返回正值;流为字节定向,返回负值;流为定向,返回0 说明: fwide不改变已定向的流的定向。 mode > 0,则fwide试图将流设置为宽定向; mode < 0,则fwide试图将流设置为字节定向; mode = 0,则fwide不设置流的定向,而是返回标识当前流定向的值。 FILE* freopen(const char* restrict pathname, const char* restrict type, FILE* restrict fp); 说明: 在指定的流上打开一个指定的文件,如果该流已经打开,则先关闭该流;若该流已经定向,则清除该定向。该函数常用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出、标准错误 |
缓冲
标准I/O提供缓冲的目的是尽可肯地减少read和write的次数,有3种类型:
类型 |
说明 |
全缓冲 |
在填满整个缓冲区后才进行实际的I/O操作。可以调用fflush函数将缓冲区的数据写入磁盘中。 |
行缓冲 |
当在输入和输出中遇到换行符或者缓冲区已满时,执行实际的I/O操作。 |
无缓冲 |
标准I/O不对字符进行缓冲存储。标准错误流stderr通常不带缓冲区,这就使得可以尽快显示出错信息。 |
对于一个给定的流,也可以使用以下函数更改系统默认的缓冲类型:
#include <stdio.h> void setbuf(FILE* restrict fp, char* restrict buf); int setvbuf(FILE* restrict fp, char* restrict buf, int mode, size_t size); 返回值:成功,0;失败,-1 说明: setbuf用于打开或关闭流缓冲机制,参数buf指向一个长度为BUFSIZ(该常量在<stdio.h>中定义)的缓冲区;如果要关闭缓冲,则将buf设置为NULL即可。 setvbuf用于精确地设置所需的缓冲类型,mode取值如下:_IOFBF(全缓冲)/_IOLBF(行缓冲)/_IONBF(无缓冲);如果指定了mode为带缓冲类型,而buf却为NULL,则系统会自动分配BUFSIZ个字节的缓冲区。 int fflush(FILE* fp); 说明: 强制冲刷一个流到磁盘中。如果fp为NULL,则系统中的所有输出流将被冲刷。 |
打开流
#include <stdio.h> FILE* fopen(const char* restrict pathname, const char* restrict type); FILE* freopen(const char* restrict pathname, const char* restrict type, FILE* restrict fp); FILE* fdopen(int fd, const char* type); 说明: fopen打开指定路径pathname的文件;freopen用于将一个指定文件打开为一个预定义的流:标准输入、标准输出、标准错误;fdopen根据fd返回一个打开的流,常用于由创建管道和网络通信函数返回的描述符。 type参数指定了对该I/O流的读写方式。 |
读写流
在打开流后,有3种不同类型的非格式化I/O(格式化I/O是诸如printf和scanf等的函数):
类型 |
说明 |
每次处理一个字符 |
#include <stdio.h> int getc(FILE* fp); int fgetc(FILE* fp); int getchar(void); 返回值:成功,返回下一个字符;失败或到达文件尾端,返回EOF 说明: getchar等价于getc(stdin)。 getc可以被实现为宏,而fgetc一定是个函数,因此fgetc调用的时间通常长于getc。 为了区分引起EOF的原因是到达文件尾端还是读入失败,引入如下三个函数: int ferror(FILE* fp); int feof(FILE* fp); void clearerr(FILE* fp); 返回值:条件为真,非0;否则,0 说明: 每个流在FILE对象中维护了两个标志:出错标志、文件结束标志,调用clearerr可以清除这两个标志。 有时在读一个输入流时,我们需要先查看下一个字符是否是需要读入的字符,此时需要利用ungetc函数将字符压回输入流中: int ungetc(int c, FILE* fp); 返回值:成功,返回c;失败,EOF 说明: 用ungetc压回字符,并非将其写到底层文件或设备中,只是将其写回标准I/O的缓冲区中。 int putc(int c, FILE* fp); int fputc(int c, FILE* fp); int putchar(int c); 返回值:成功,返回c;失败,EOF 说明: putchar(c)等价于putc(c, stdout)。 putc可被实现为宏,而fputc只能是一个函数。 |
每次处理一行 |
#include <stdio.h> char* fgets(char* restrict buf, int n, FILE* restrict fp); char* gets(char* buf); 返回值:成功,返回buf;失败,返回NULL 说明: gets从标准输入读,fgets从指定的文件读。 每次读取直至遇到换行符或者达到缓冲区长度n(由于缓冲区以null字节结尾,有效长度实则是n - 1),如果fgets读取的行超过n – 1个字节,那么返回前n – 1个字节,对fgets的下一次调用将继续读取该行其余部分。 gets由于不能指定缓冲区长度n,因此在最新的ISO C中已被忽略,因此不推荐使用gets函数。 int fputs(const char* restrict str, FILE* restrict fp); int puts(const char* str); 返回值:成功,返回非负值;失败,返回EOF 说明: fputs将一个以null字节终止的字符串写到指定的流中,null终止符不写出。 虽然,puts并没有太大的安全隐患,但还是避免使用它,因为puts每次写出数据后还附加写出一个换行符。 |
直接I/O或二进制I/O |
比如我们需要一次读写一个完整的结构,如果使用getc/putc,那么必须循环遍历整个结构,每次处理一个字节,非常麻烦;如果使用fgets/fputs,那么由于其遇到null字节就停止了,而在结构中可能含有null字节,因此也不能很好地工作。因此,提供了如下两个函数: #include <stdio.h> size_t fread(void* restrict ptr, size_t size, size_t nobj, FILE* restrict fp); size_t fwrite(const void* restrict ptr, size_t size, size_t nobj, FILE* restrict fp); 返回值:成功读写的结构个数 例如:将data数组的第2~5个元素(一共4个)写至一个文件上: float data[10]; if(fwrite(&data[2], sizeof(float), 4, fp) != 4) printf(“fwrite error”); |
示例程序:将标准输入复制到标准输出:
()每次处理一个字符(getc/putc)
[root@benxintuzi IO]# cat getc.c
#include <stdio.h> int main(void)
{
int c; while((c = getc(stdin)) != EOF)
if(putc(c, stdout) == EOF)
printf("output error\n"); if(ferror(stdin))
printf("input error\n"); return ;
} [root@benxintuzi IO]# ./getc
hello
hello
benxintuzi
benxintuzi
^C
[root@benxintuzi IO]# () 每次处理一行(fgets/fputs)
[root@benxintuzi IO]# cat fgets.c
#include <stdio.h> #define MAXLINE 4096 int main(void)
{
char buf[MAXLINE]; while(fgets(buf, MAXLINE, stdin) != NULL)
if(fputs(buf, stdout) == EOF)
printf("output error\n"); if(ferror(stdin))
printf("input error\n"); return ;
} [root@benxintuzi IO]# ./fgets
benxin
benxin
tuzi
tuzi
^C
[root@benxintuzi IO]#
流定位
#include <stdio.h> long ftell(FILE* fp); offset ftello(FILE* fp); 返回值:成功,返回文件当前位置;失败,返回-1L int fseek(FILE* fp, long offset, int whence); int fseeko(FILE* fp, off_t offset, int whence); 返回值:成功,0;失败,-1 说明: whence取值如下:SEEK_SET/SEEK_CUR/SEEK_END。 fseek和fseeko唯一的区别是offset的类型不同。 void rewind(FILE* fp)将一个流设置到文件的起始位置。 int fgetpos(FILE* restrict fp, fpos_t* restrict pos); int fsetpos(FILE* fp, const fpos_t* pos); 返回值:成功,0;失败,-1 说明: fgetpos将当前文件指针存入pos中;fsetpos将pos的值设为当前文件位置。 |
格式化I/O
格式化输出: #include <stdio.h> int printf(const char* restrict format, ...); int fprintf(FILE* restrict fp, const char* restrict format, ...); int dprintf(int fd, const char* restrict format, ...); int sprintf(char* restrict buf, const char* restrict format, ...); int snprintf(char* restrict buf, size_t n, const char* restrict format, ...); 返回值:成功,返回写入的字符数;失败,返回负值 说明: printf写到标准输出,fprintf写到文件,dprintf写到文件描述符,sprintf写到buf中,但是可能会溢出,snprintf也写到buf中,但是由于指定了缓冲区长度n,可能截断但不会溢出。 格式化输入: #include <stdio.h> int scanf(const char* restrict format, ...); int fscanf(FILE* restrict fp, const char* restrict format, ...); int sscanf(const char* restrict buf, const char* restrict format, ...); |
格式控制:
%[flags][fldwidth][precision][lenmodifier]convtype
flags |
说明 |
' |
将整数按千位分组输出 |
- |
左对齐输出 |
+ |
显示带符号转换的正负号 |
空格 |
如果第一个字符不是正负号,则用空格代替 |
# |
指定转换格式,例如0x前缀 |
0 |
用0而非空格进行填充 |
fldwidth:最小字段宽度。若转换后字符数小于该值,则用空格填充。该值可以是一个非负整数或*。
precision:精度表示,整数位数、浮点数小数位数、字符串最大字节数。该值可以是一个.加上非负整数或者*。
lenmodifier:参数长度。hh(signed或unsigned char)/h(signed或unsigned short)/l(signed或unsigned long)/ll(signed或unsigned long long)/j(intmax_t或uintmax_t)/z(size_t)/t(ptrdiff_t)/L(long double)。
convtype:转换类型:
d、i |
有符号十进制 |
o |
无符号八进制 |
u |
无符号十进制 |
x、X |
无符号十六进制 |
f、F |
双精度浮点数 |
e、E |
指数格式双精度浮点数 |
g、G |
根据转换后的值解释为f、F、e、E |
a、A |
十六进制指数格式双精度浮点数 |
c |
字符 |
s |
字符串 |
C |
宽字符 |
S |
宽字符串 |
% |
%本身 |
p |
void*指针 |
临时文件
每个标准I/O流都有一个相关联的文件描述符,可以调用int fileno(FILE* fp)来获得这个描述符。如下两个函数用于创建临时文件:
#inlcude <stdio.h> char* tmpnam(char* ptr); FILE* tmpfile(void); 说明: tmpnam产生一个与现有文件名不同的临时文件名,每次调用时,都产生一个不同的临时文件名。最多的调用次数为TMP_MAX(定义在<stdio.h>中)。若ptr为NULL,则所产生的临时文件名存放在一个静态区中,指向该静态区的指针作为函数值返回;如果ptr不为NULL,则其指向长度大于等于L_tmpnam个字符的数组,所产生的临时文件名存放在该数组中,ptr作为函数值返回。 tmpfile创建一个临时的二进制文件(wb+),在关闭文件或程序结束时自动删除该文件。 #include <stdlib.h> char* mkdtemp(char* template); int mkstemp(char* template); 说明: mkdtemp创建一个目录,返回指向目录名的指针;mkstemp创建一个文件,返回文件描述符。 template的后六位设置为XXXXXX。函数将这些占位符替换成不同的字符来构建一个唯一的名称。 |
[root@benxintuzi IO]# cat tmp.c
#include <stdio.h> int main(void)
{
char name[L_tmpnam], line[];
FILE* fp; printf("%s\n", tmpnam(NULL)); /* first temp name */ tmpnam(name); /* second temp name */
printf("%s\n", name); if((fp = tmpfile()) == NULL) /* create temp file */
printf("tmpfile error\n");
fputs("write one line to tmpfile\n", fp); /* write to temp file */ rewind(fp); /* then read it back */
if(fgets(line, sizeof(line), fp) == NULL)
printf("fgets error\n");
fputs(line, stdout); /* print the line */ return ;
}
[root@benxintuzi IO]# gcc tmp.c -o tmp
/tmp/cc6sXVXs.o: In function `main':
tmp.c:(.text+0x14): warning: the use of `tmpnam' is dangerous, better use `mkstemp'
[root@benxintuzi IO]# ./tmp
/tmp/filekyJuQu
/tmp/fileKA5UyL
write one line to tmpfile
[root@benxintuzi IO]#
[root@benxintuzi IO]# cat mkstemp.c
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h> void make_temp(char *template); int main(void)
{
char good_template[] = "/tmp/dirXXXXXX"; /* right way */
char *bad_template = "/tmp/dirXXXXXX"; /* wrong way*/ printf("trying to create first temp file...\n");
make_temp(good_template);
printf("trying to create second temp file...\n");
make_temp(bad_template);
exit();
} void make_temp(char *template)
{
int fd;
struct stat sbuf; if ((fd = mkstemp(template)) < )
printf("can't create temp file\n");
printf("temp name = %s\n", template);
close(fd);
if (stat(template, &sbuf) < ) {
if (errno == ENOENT)
printf("file doesn't exist\n");
else
printf("stat failed\n");
} else {
printf("file exists\n");
unlink(template);
}
} [root@benxintuzi IO]# gcc mkstemp.c -o mkstemp
[root@benxintuzi IO]# ./mkstemp
trying to create first temp file...
temp name = /tmp/dirnemrcT
file exists
trying to create second temp file...
Segmentation fault (core dumped)
[root@benxintuzi IO]# 说明:
对于第一个模板,由于使用了数组,则数组内容存储在栈上;但是第二个模板,只有指针本身存储在栈上,而具体字符串内容却存放在可执行文件的只读段中,因此当mkstemp函数试图修改字符串时,出现segment fault。
内存流
在内存流中,所有的I/O都是通过在缓冲区与主存之间来回传送字节来完成的。由于避免了缓冲区溢出,因此内存流非常适用于创建字符串。内存流只访问主存,不访问磁盘上的文件,所以性能方面会有显著的提升。有三个函数用于内存流的创建:
#include <stdio.h> FILE* fmemopen(void* restrict buf, size_t size, const char* restrict type); 返回值:成功,返回流指针;失败,返回NULL 说明: fmemopen函数允许调用者指定自己的缓冲区用于内存流。buf指向缓冲区的开始位置,size指定了缓冲区的大小,type参数控制流的使用方式:r/rb/w/wb/a/ab/r+/r+b/rb+/w+/w+b/wb+/a+/a+b/ab+ 注意点: (1)以追加方式打开内存流时,当前文件位置设为缓冲区中的第一个null字节;如果缓冲区中不存在null字节,则当前文件位置设为缓冲区结尾的后一个字节。 (2)以其他方式打开内存流时,当前文件位置设为缓冲区的开始位置。 (3)如果buf为null,则打开内存流没有任何意义。 (4)增加流缓冲区中的数据或者调用fclose、fflush、fseek、fseeko、fsetpos时都会在当前位置写入一个null字节。 #include <stdio.h> FILE* open_menstream(char** bufp, size_t* sizep); #include <wchar.h> FILE* open_wmemstream(wchar_t** bufp, size_t sizep); 返回值:成功,返回流指针;失败,返回NULL 说明: open_memstream函数创建的流是面向字节的,其与fmemopen的区别如下: 创建的流只能写打开; 不能指定自己的缓冲区,可以通过bufp和sizep访问缓冲区地址和大小; 关闭流后需要自行释放缓冲区; 对流添加字节会增加缓冲区的大小。 |
以下程序说明了如何在我们自己提供的缓冲区上操作内存流:
[root@benxintuzi IO]# cat memstr.c
#include <stdio.h>
#include <stdlib.h> #define BSZ 48 int main(void)
{
FILE *fp;
char buf[BSZ]; memset(buf, 'a', BSZ-);
buf[BSZ-] = '\0';
buf[BSZ-] = 'X';
if ((fp = fmemopen(buf, BSZ, "w+")) == NULL)
printf("fmemopen failed\n");
printf("initial buffer contents: %s\n", buf);
fprintf(fp, "hello, world");
printf("before flush: %s\n", buf);
fflush(fp);
printf("after fflush: %s\n", buf);
printf("len of string in buf = %ld\n", (long)strlen(buf)); memset(buf, 'b', BSZ-);
buf[BSZ-] = '\0';
buf[BSZ-] = 'X';
fprintf(fp, "hello, world");
fseek(fp, , SEEK_SET);
printf("after fseek: %s\n", buf);
printf("len of string in buf = %ld\n", (long)strlen(buf)); memset(buf, 'c', BSZ-);
buf[BSZ-] = '\0';
buf[BSZ-] = 'X';
fprintf(fp, "hello, world");
fclose(fp);
printf("after fclose: %s\n", buf);
printf("len of string in buf = %ld\n", (long)strlen(buf)); return();
} [root@benxintuzi IO]# ./memstr
# 用a字符改写缓冲区
initial buffer contents: # fmemopen在缓冲区开始处放置null字节
before flush: # 流冲刷后缓冲区才会变化
after fflush: hello, world
len of string in buf = # null字节加到字符串结尾
# 现在用b字符改写缓冲区
after fseek: bbbbbbbbbbbbhello, world # fseek引起缓冲区冲刷
len of string in buf = # 再次追加写null字节
after fclose: hello, worldcccccccccccccccccccccccccccccccccc # 现在用c字符改写缓冲区
len of string in buf = # 没有追加写null字节
[root@benxintuzi IO]#