标准库提供了与系统调用lseek()类似的函数来定位流中的读写位置。
#include <stdio.h>
int fseek (FILE *stream, long offset, int whence);
long ftell(FILE *stream);
与lseek()用法类似,whence提供了以下选择:
SEEK_CUR-将流的读写位置设置为当前位置加上pos个字节,pos可以是正数或负数。
SEEK_END-将流的读写位置设置为文件尾加上pos个字节,pos可以是正数或负数。
SEEK_SET-将流的读写位置设置为pos对应的位置,pos为0时期表设置为文件起始位置。
函数调用成功后返回0并取消之前的ungetc()操作,毛病时返回⑴。
上述的函数中偏移量类型是long,若处理的文件大小超过long变量的范围时,可使用
#include <stdio.h>
int fseeko(FILE *stream, off_t offset, int whence);
off_t ftello(FILE *stream);
off_t在64位系统上被实现为64位大小,在32位系统上与long大小相同。
类似功能的函数有
#include <stdio.h>
int fsetpos (FILE *stream, fpos_t *pos);
int fgetpos(FILE *stream, fpos_t *pos);
除非为了源码兼容性,1般不使用这个函数。
#include <stdio.h>
void rewind (FILE *stream);
该调用将stream的读写位置设置为流初始,与fseek (stream, 0, SEEK_SET);
功能1致。由于该函数没有返回值,因此需要验证是不是正确的话调用之前应当将errno置0,调用知乎检查errno是不是为0。
格式化I/O是指将内容依照规定的格式整理后读取或输出。
格式化输出主要通过printf()系列函数:
#include <stdio.h>
int printf(const char *format, ...);//格式化到标准输出
int fprintf(FILE *stream, const char *format, ...);//格式化到流
int sprintf(char *str, const char *format, ...);//格式化到str中
int snprintf(char *str, size_t size, const char *format, ...);//与sprintf类似,更安全,其提供了可写缓冲区的长度
int dprintf(int fd, const char *format, ...);//格式化到文件描写符fd对应的文件中
上述函数的返回值均是真正格式化的长度,不包括字符串结束符\0
。
我们1般见到的printf()调用是printf("%d", i);
的情势,其实printf()系列函数的完全格式是:
% [flags] [fldwidth] [precision] [lenmodifier] convtype
%-是格式化字符串的起始,必须要有
flags-是控制格式化样式的标志,有以下取值:
标志 | 说明 |
---|---|
‘ | 将整数按千分位组字符 |
- | 在字段内左对齐输出 |
+ | 总是显示带符号转换的正负号 |
(空格) | 如果第1个字符不是正负号,则在其前面加上1个空格 |
# | 指定另外一种转换情势(例如,对106进制格式,加0x前缀) |
0 | 添加前导0(默许是空格)进行填充 |
fldwidth-控制被格式化内容的宽度,若宽度不够则用空格或0补齐。可以指定非负数也能够指定*来默许处理
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 | 有符号10进制 |
o | 无符号8进制 |
u | 无符号10进制 |
x,X | 无符号106进制 |
f, F | 双精度浮点数 |
e, E | 指数格式双精度浮点数 |
g, G | 根据转换后的值解释为f、F、e或E |
a, A | 106进制指数格式双进度浮点数 |
c | 字符 |
s | 字符串 |
p | 指向void的指针 |
n | 到目前为止,此printf调用输出的字符的数目将被写入到指针所指向的带符号整型中 |
% | 1个%字符 |
C | 宽字符 |
S | 宽字符串 |
下面是各个参数的效果:
#include <stdio.h>
int main(void)
{
printf("%+0.1lf\n", 1.23456); //+1.2
printf("%+0.1lf\n", -1.23456); //⑴.2
printf("%+8.2lf\n", -1.23456); // ⑴.23
printf("%8.6d\n", 123); // 000123
return 0;
}
标准库还提供了使用可变长参数的版本,功能与对应版本类似:
#include <stdio.h>
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
int vdprintf(int fd, const char *format, va_list ap);
格式化输入用于分析字符串并转换成对应类型变量保存起来,主要通过scanf()系列函数:
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
完全的参数为:
%[*] [fldwidth] [m] [lenmodifier] convtype
fldwidth-最大字符宽度
m-当要输入的是字符串时,该参数指定提供的缓冲区的大小
lenmodifier-转换后要赋值的参数大小
convtype-要转化的参数类型,与printf()系列函数级别1致。当该标志代表无符号变量且输入的数据是负数时,将转换为2进制相同的正数,例如⑴转为4294967295。
标准库一样提供了变长参数的版本,不再赘述。
在向1个流写入数据后数据并没有真正交给内核,而是在用户空间的缓冲区内保存,等待数据积累到适合大小后再要求内核。标准库提供了立行将缓冲区数据提交内核的函数。
#include <stdio.h>
int fflush (FILE *stream);
该函数调用后,stream中的数据会被flush到内核缓冲区,此时与直接调用write()的效果是1样的。如果需要确保数据被提交给硬盘,需要使用fysnc()
或相同功能的系统调用。1般fflush()后都要调用fsync()来确保数据从用户缓冲区到内核缓冲区再到硬盘。
fread()函数的返回值不能辨别产生毛病还是遇到了EOF,标准库提供了毛病检查函数:
#include <stdio.h>
int ferror (FILE *stream);
用于检测stream上是不是有毛病标志。毛病标志由标准I/O相干函数设置,如果存在毛病标志,该函数返回非0值,否则返回0。
#include <stdio.h>
int feof (FILE *stream);
用来检测stream是不是到了文件结尾。若到文件结尾,返回非0,否则返回0。
#include <stdio.h>
void clearerr (FILE *stream);
用于清算stream的errno和EOF标志。
与fdopen()相对,fileno()用于获得与流关联的文件描写符。但是不建议读写文件时将文件描写符和流混用。
#include <stdio.h>
int fileno (FILE *stream);
失败时返回⑴并设置errno。
标准I/O库提供了3种缓冲类型,分别为:
不缓冲
不履行用户空间缓冲,数据直接提交给内核。这类情况下使用标准I/O没有甚么优势。标准毛病就是这类缓冲模式。
行缓冲
遇到换行符时将缓冲区提交到内核。标准输出是这类缓冲模式,也叫全缓冲。
块缓冲
默许的缓冲模式,缓冲效果最好。
#include <stdio.h>
int setvbuf (FILE *stream, char *buf, int mode, size_t size);
控制缓冲类型,mode多是:
_IONBF-不缓冲
_IOLBF-行缓冲
_IOFBF-块缓冲
在_IONBF模式下,buf和size参数被疏忽。其他模式下标准I/O会使用buf作为缓冲区,其大小是size。当buf是NULL时,缓冲区被自动分配。默许的缓冲区大小为BUFSIZ
宏定义的,是块大小的整数倍。setbuf()必须在打开流后,做任何其他操作之前被调用,失败时返回非0并设置errno。
还要注意缓冲区是局部变量时,1定要在局部变量失效前关闭流,毛病的使用例如
#include <stdio.h>
int main(void)
{
char buf[BUFSIZ];
setbuf(stdin, buf);
printf("Hello, world!\n");
return 0;
}
标准I/O库提供了fmemopen()函数来打开位于内存的流,而不与底层文件相干联,其用用户指定的缓冲区单做文件读写的位置,返回1个FILE*。
#include <stdio.h>
FILE *fmemopen(void *buf, size_t size, const char *mode);
buf-缓冲区的起始地址,若该参数是NULL,库函数会帮助分配1个size大小的缓冲区,在关闭流的时候被释放。
size-缓冲区大小。
mode-读写模式,与fopen()参数类似。
注意事项:
1 当以追加方式打开内存流时,当前的文件读写位置是缓冲区中的第1个字符串结束符位置(‘\0’)。缓冲区中无字符串结束符时,文件位置是缓冲区结尾的后1个字节。
2 当内存流不是以追加方式打开时,当前文件位置是缓冲区开始的位置
3 buf是NULL,以只读或只写方式打开内存流没成心义。由于我们没办法知道分配的缓冲区的地址,因此只能读取我们没法写入的数据或写入我们没法读取的数据
4 增加内存流中数据或调用fclose()、fflush()、fseek()、fseeko()和fsetpos()时都会在当前位置增加1个字符串结束符
下面代码测试上述内容:
#include <stdio.h>
#include <string.h>
#include <iostream>
using namespace std;
int main(void)
{
//============
//追加模式下文件位置是第1个'\0'处,非追加模式下是缓冲区开始位置
FILE* fp =NULL;
char buffer[256] = "this is a buffer.";
fp = fmemopen(buffer, 256, "r+");
cout << ftell(fp) << endl;//0
fclose(fp);
fp = fmemopen(buffer, 256, "a+");
cout << ftell(fp) << " " << strlen(buffer) << endl;//17 17
fclose(fp);
//============
//============缓冲区内容增加,会自动写入'\0'
fp = fmemopen(buffer, 256, "w+");
fputc('a', fp);
fflush(fp);
cout << buffer << endl;//a
cout << &buffer[2] << endl;//is a buffer. 由于'th'变成了a'\0'
fclose(fp);
//============
return 0;
}
由于内存流依赖字符串结束符,因此以2进制的情势读写文件流其实不适合,由于2进制数据中’\0’出现的位置有多是1条数据的中间而不是结尾,使用内存流来读写2进制数据极可能会破坏数据。
类似的函数还有
#include <stdio.h>
FILE *open_memstream(char **ptr, size_t *sizeloc);//对char类型的字符串做操作
#include <wchar.h>
FILE *open_wmemstream(wchar_t **ptr, size_t *sizeloc);//对宽字节的字符串做操作
与fmemopen()区分在于:
* 创建的流没法指定读写模式,只能写打开
* 不能自行指定缓冲区,函数返回时指针指向标准库分配的缓冲区。由于缓冲区可能会被重新分配(例如1开始分配的缓冲区不够扩大了),因此*ptr指向的地址可能会变
* 关闭流后需要自行释放缓冲区
* 缓冲区会随着流数据的增多而变大,每次fflush()或fclose()后sizeloc指向的值可能会被改变
最直观的作用是其提供了1个处于内存中的文件指针,使我们可以像读写文件1样操作1块内存,方便的使用标准I/O提供的函数调用而没有真正读写文件的性能损失。在1些特殊的第3方API中,可能需要1个FILE*类型的参数,但是此时都在内存中,这时候将内存写入文件再传到第3方API中明显是不划算的,因此可以将对应的内存映照为打开的文件。另外对open_memstream()相干的函数来讲,其内部管理了缓冲区,使我们不需要担心缓冲区溢出的问题,此时可以方便的格式化或拼接字符串,例如格式化1段sql语句等。
多线程程序中线程同享进程的资源,因此需要对同享资源做线程同步操作,避免产生非预期的结构,标准I/O默许是线程安全的(即在多线程并发读写同1个文件时,读写操作在任意时刻只能运行1个,且上1个要求结束前不会被其他读写要求抢占CPU)。
下面的代码验证线程安全:
//编译时要-lpthread,链接pthread库
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thrd_func(void *arg);
FILE *stream;
int main()
{
pthread_t tid;
int *thread_ret = NULL;
stream = fopen("1.txt", "a");
if (!stream)
{
perror("fopen");
return -1;
}
if (pthread_create(&tid,NULL,thrd_func,NULL)!=0)//创建1个线程,使其与主线程同时向1个流输出内容。
{
printf("Create thread error!\n");
exit(1);
}
for(int i = 0; i<10000; ++i)
{
if (fputs("This is a test line,it should not be broken1\r\n", stream) == EOF)
{
printf("err!");
}
}
pthread_join(tid, (void**)&thread_ret );
return 0;
}
void *thrd_func(void *arg)
{
for(int i = 0; i<10000; ++i)
{
if (fputs("This is a test line,it should not be broken2\r\n", stream) == EOF)
{
printf("err!");
}
}
pthread_exit(NULL);
}
在多核的主机上运行上述代码,可以看到”This is a test line,it should not be broken1”和”This is a test line,it should not be broken2”交替出现,但是每行都是完全的。交替出现的缘由是不同的CPU核心会同时向文件写入字符串。
上面的代码也暴露了1些问题,假设现在有1个多线程的服务器,要在日志中打印出来内部1个map中的数据,那末在实际情况中极可能打印出的内容被其他线程的日志输出穿插,读日志时带来1些困难,这就需要线程同步来进行,标准I/O库提供了针对流的加锁功能。
用flockfile()给对应流加锁,用funlockfile()解锁。
#include <stdio.h>
void flockfile (FILE *stream);
void funlockfile (FILE *stream);
标准I/O库中的锁是递归锁(可重入锁),即1个线程可以屡次取得该锁而不被锁死或断言毛病。该锁使用计数,当flockfile()时,计数器+1;funlockfile()时,计数器⑴,因此调用funlockfile()的次数1定要与flockfile()次数1致,特别是毛病处理提早返回时更要谨慎。当计数器为0时,代表线程不再保持锁,此时其他线程对同1个流加锁的话能够无阻塞的取得锁。
当第1次加锁成功时flockfile()返回0,当前线程取得锁;已取得锁时本线程再次调用flockfile(),返回非0。
在输出map元素之前加锁,输出完成后释放锁,这样就能够保证map的数据在1起而不被其他信息穿插了。
既然开发人员选择手动控制锁的范围,那末就没必要在读写文件时再次加锁了。标准库提供了1系列不加锁的库函数。
#define _GNU_SOURCE
#include <stdio.h>
int fgetc_unlocked (FILE *stream);
char *fgets_unlocked (char *str, int size, FILE *stream);
size_t fread_unlocked (void *buf, size_t size, size_t nr,FILE *stream);
int fputc_unlocked (int c, FILE *stream);
int fputs_unlocked (const char *str, FILE *stream);
size_t fwrite_unlocked (void *buf, size_t size, size_t nr, FILE *stream);
int fflush_unlocked (FILE *stream);
int feof_unlocked (FILE *stream);
int ferror_unlocked (FILE *stream);
int fileno_unlocked (FILE *stream);
void clearerr_unlocked (FILE *stream);
这些函数除不再加锁外,行动与加锁版本1致。
感兴趣的同学可以做1下小练习,将之前校验标准I/O线程安全的代码改用非加锁的调用,试试看输出文件有甚么变化。
另外还可以用系统调用write()来测试1下write()是不是是线程安全的(事实上系统调用基本都是原子操作,即线程安全的,但是write()比较特殊,其内部是两个调用:定位和写入。不使用O_APPEND模式的话,可能会由于偏移量没有增加而致使写入内容被覆盖,这里有资料,因此在多线程读写时,如果不打算自己做线程同步的话,使用系统调用write()时1定要加上O_APPEND标志)
首先需要明确的是标准I/O提供了非常方便的用户空间缓冲机制,使开发人员无需关注系统的块大小而提高文件I/O效力;其次库函数提供了便利的操作,能够按行读写文本;另外由因而标准库,其代码可移植性非常高,使用也广泛。
但标准I/O库也有1些缺点,其中1个就是双副本问题。双副本问题是指,标准I/O库内部保护了1个缓冲区,从内核读取到的数据拷贝到该缓冲区中保护,在用户需要数据时,要再次拷贝到用户指定的地址中:1段数据在用户空间中有两个副本,同时也有两次拷贝操作,写入时也是类似情况。
对读取操作,1个改良方式是返回1个指向标准I/O缓冲区的指针,用户程序只有在修改读取内容或在缓冲区被清空之前拷贝数据便可。另外setvbuf()函数设置的用户缓冲区是否是也能减少1次拷贝?
对写操作,可使用[分散输入和集中输出]的I/O模式,见后面章节内容。
另外一些函数库也提供了相干解决方案,例如快速I/O库(fio、sfio)、映照文件(mmap函数)。