I/O
这一章涉及到C标准库当中的内容,是在Linux
操作系统之上进行的封装扩充,牛客Linux
的课程没有涉及到,所以认真学
不仅是UNIX,很多其他操作系统都实现了标准I/O
库,这个库由ISO C
标准进行说明
标准库的I/O
处理了很多细节,比如缓冲区分配,以优化的块长度执行I/O
等等,在系统调用的基础上使得用户的调用更加的方便和严谨安全;这些处理使得用户不用担心如何正确选择正确的块长度;但是我们也要深入了解以下标准I/O
库函数的操作,以及是如何与系统调用联系起来的,否则出了问题不知道怎么办
前面提到的I/O
都是围绕文件描述符的,我们打开一个文件返回给我们一个文件描述符,然后我们通过对文件描述符对文件进行后续的操作;
但是对于标准I/O
,所有的操作都是围绕流(stream
)展开的,我们打开一个文件,标准I/O
返回我们一个流用于进行和文件的关联
下面我们看一下单字节流和多字节流:
I/O
文件流可用于单字节或多(宽)字节字符集。流的定向决定了所读、写的字符是单字节还是多字节。I/O
函数,则将该流的定向设置为宽定向的。若在未定向的流上使用一个单字节I/O
函数,则将该流的定向设置为字节定向的。freopen
函数清除一个流的定向;fwide
函数设置流的定向。#include
int fwide(FILE *stream, int mode);
// stream:打开文件返回的流对象
// mode:不同的值有不同的作用
// 0,则表示不试图设置流的走向,返回现在流走向的值
// 正数,则表示试图设置流的走向为宽(多字节)走向
// 负数,则表示试图设置流的走向为单字节走向
// 返回值:返回设置之后的字节流走向(传入正数或者负数) 或者 现在流走向的值(传入0)
我们写一个程序来加深一下印象:
代码中fopen
是标准I/O
库提供的打开文件的函数,其中第二个参数表示只读,对应open
函数的O_RDONLY
,这个后面再说
#include
using namespace std;
#include
int main() {
FILE* file = fopen("01.txt", "r");
if (nullptr == file) {
perror("fopen");
return -1;
}
int ret = fwide(file, 100);
cout << ret << endl;
// 现在我在已经设置宽定向的基础上再设置一次
ret = fwide(file, -100);
cout << ret << endl;
return 0;
}
结果:
返回1,文件刚打开的时候是未定向的流,标志值是0,然后我进行设置之后就返回宽定向的标志值,正数,这里返回了1;
但是第二次我设置未单字节流,为什么还是返回1呢?这表示我们尝试设置不成功,但是程序没有异常终止,这就是这个函数的特性了
从上面我们可以看出,fwide
函数没有办法改变已定向流的定向,并且没有出错返回;
如果流无效的时候,我们该怎么办呢?我们可以在调用fwide
之前,清除errno
,然后执行函数之后检查errno
的值,这个倒不算重要
另外,当我们用fopen
打开一个文件时,返回给我们一个操作文件的流,这个流包含了I/O
管理这个文件的所有信息,包括内核实际上使用的文件描述符,指向该流使用缓冲区(这个缓冲区是用户区的那个)的指针,缓冲区的长度,当前缓冲区的字符数以及出错的标志等等
所以说了这么多,我们还是要区分一下单字节和多字节:
utf
-8,就是对应8位,单字节,对应的就是ASCII
编码集;像GB2312
这些就是多字节编码,我们后面都不考虑,只考虑单字节与文件描述符的0 1 2类似的,内核对进程预定义了三个流,stdin
,stdout
和stderr
,这三个流就对应了系统调用中的文件描述符STDIN_FILENO
,STDOUT_FILENO
和STDERR_FILENO
标准I/O
库提供缓冲的目的就是尽可能少的调用write
和read
函数,也是对每个I/O
流自动进行缓冲管理,而不需要应用程序考虑这一点,可能带来麻烦;
标准I/O
库提供了三种不同的缓冲:
全缓冲
在这种情况下,当缓冲区被填满之后才会进行相应的I/O
操作,比如读需要等缓冲区被写满了再去读,写需要等缓冲区被读完了再去写;对于磁盘上的文件通常是由标准I/O
库实现全缓冲的;对于一个流,第一次执行I/O
操作的时候,通过malloc
函数去获得其需要使用的缓冲区
当然我们可以手动的冲洗缓冲,冲洗(flush
)这个术语用来说明标准I/O
的写操作;缓冲区可以由标准I/O
自动冲洗,比如缓冲区被填满的时候;我们也可以手动调用fflush
函数冲洗一个流
在UNIX中,冲洗有两种意思:在标准I/O
方面,表示将缓冲区的数据写到磁盘中(缓冲区可能是部分填满的),在终端驱动程序方面,flush
意味着丢弃存储在缓冲区的数据,这个我们后面再说
行缓冲
在这种情况下,在输入和输出遇到换行符的时候,标准库I/O
自动执行相应的I/O
操作;
当流涉及一个终端的时候,就是标准输入和标准输出,通常对应的就是行缓冲
我们写一个程序来验证:
#include
using namespace std;
int main() {
// cout << "hello";
printf("hello");
while (1)
;
cout << endl;
return 0;
}
结果:
可见标准输出对应的是行缓冲,当我把字符串和换行符分开,就没办法及时输出了,遇到换行符就会立即刷新缓冲区
注意:
I/O
提供的行缓冲区大小是有限的,所以如果填满了缓冲区,即使没有换行符,也会进行I/O
操作(这里有点像上面的行缓冲)I/O
库从一个不带缓冲的流,或者一个行缓冲的流中得到输入数据,那么系统会冲洗缓冲区输出流进行输出;对于第二个行缓冲,它可能需要从内核中读取数据,也可能不需要,因为数据可能在缓冲区中,但是对于不带缓冲的流肯定需要从内核的缓冲中获得数据不带缓冲
标准I/O
库不对字符进行缓冲存储(内核中还有缓冲),意思是我们希望数据尽快的输入或者输出;例如我们调用fputs
函数输出一些字符到不带缓冲的流当中,我们就期望这些数据能够尽快输出,这时候在底层很可能就调用了系统API的write进行后续操作
标准错误stderr通常是不带缓冲的,因为错误信息应该尽可能快的显示出来,而不需要管他们有没有换行符
ISO C标准要求缓冲有下列特征:
但是上面的说法并没有告诉我们,当标准输入和输出指向交互式设备的时候是行缓冲还是不带缓冲;标准错误是行缓冲还是不带缓冲,所以一般来说系统默认使用如下的缓冲:
关于标准I/O
缓冲后续会说的更具体
对于一个流,系统一般会默认给流缓冲的方式,比如标准输入输出使用行缓冲默认定向到终端,如果我们想要修改可以通过如下的函数进行修改
这两个函数的第一个参数都要求传入要给已经打开的流
#include
void setbuf(FILE *restrict stream, char *restrict buf);
// stream:想要修改的流
// buf:我们用户手动指定的用户缓冲区,系统在输入输出的时候数据都是先到这里
// 这个函数设置之后默认该流就是全缓冲的(一般是),想要设置行缓冲或者不带缓冲需要用下面的函数
// 当然如果buf传递的是nullptr,那么这个流会被设置未不带缓冲
int setvbuf(FILE *restrict stream, char *restrict buf, int mode, size_t size);
// 作用和上面的函数类似,但是有一些其他的功能
// mode:使用这个函数,我们可以具体的设置缓冲类型,具体如下:
// _IOFBF 全缓冲
// _IOLBF 行缓冲
// _IONBF 不带缓冲
// 如果我们指定为全缓冲或者行缓冲,则buf和size我们可以指定选择一个缓冲区和大小
// 系统给我们提供了一个默认的缓冲区大小的宏 BUFSIZ ,指是8192个字节
// 当然我们指定这个流是带缓冲的,但是我们给的buf是nullptr,那么标准`I/O`会自动分配给该流适当长度的缓冲区,比如可以是BUFSIZ
我们可以用下面这个图进行更详细的总结他们的运作:
我们要知道,虽然我们指定了缓冲区的大小,但是缓冲区中还可能存放着他自己的管理操作信息,所以可以存放在当中的实际字节数要小于缓冲区的大小,这个并不是很重要,因为缓冲区的大小一般都是往上开够用了
一般而言,我们可以让系统自己选择缓冲区的长度,然后自动分配缓冲区,这样关闭流的时候,标准I/O
库会自动释放缓冲区
任何时候,我们都可以强制冲洗一个流
这个函数会让所有未写的数据都被送至内核当汇总(内核中也有缓冲区,然后就可以进行后续的操作)
如果传递的是nullptr
,那么所有输出流都会被冲洗
#include
int fflush(FILE *stream);
我们写一个例子来实际操作一下
在这个程序当中,我将标准输出的缓冲区定向为我设置的outbuf
数组,然后分两次puts
一些内容;然后刷新,然后再写
#include
using namespace std;
#include
char outbuf[BUFSIZ]; // BUFSIZ是指默认给的缓冲区大小,是8192个字节
int main() {
setbuf(stdout, outbuf); // 把缓冲区与流相连
puts("This is a test of buffered output."); // puts最后会自带一个换行符
puts(outbuf); // 这里我除了写入自定义字符串,我把outbuf的内容也写一遍,所以会有两个换行符
sleep(3);
fflush(stdout); // 刷新
puts("This is a test of buffered output.");
sleep(3);
return 0;
}
结果:
前面三行(包括第三行的空行)经过3秒输出,最后一行又经过3行输出,符合我们的预期
我们可以看出这里的setbuf
给我们的是全缓冲
下面我给一个不带缓冲的例子
#include
using namespace std;
#include
int main() {
setvbuf(stdout, nullptr, _IONBF, 0);
puts("This is a test of buffered output."); // puts最后会自带一个换行符
sleep(3);
fflush(stdout); // 刷新
puts("This is a test of buffered output.");
sleep(3);
return 0;
}
结果:
这两行都是执行puts
函数之后立即输出的,可以看出不带缓冲
那怎么能少了行缓冲呢?还是全缓冲的例子,我这次设置为行缓冲
#include
using namespace std;
#include
char buf[BUFSIZ] = {0};
int main() {
setvbuf(stdout, buf, _IOLBF, sizeof(buf));
puts("This is a test of buffered output."); // puts最后会自带一个换行符
sleep(3);
fflush(stdout); // 刷新
puts("This is a test of buffered output.");
sleep(3);
return 0;
}
结果:
由于puts
会自带一个换行符,所以也是立即输出的
我们可以用以下的函数来打开一个标准I/O
流
#include
FILE *fopen(const char *restrict pathname, const char *restrict mode);
// 打开指定路径的文件,返回用于操作的标准I/O流,可以是绝对或者相对路径
FILE *fdopen(int fd, const char *mode);
// 通过一个已有的文件描述符,并用一个标准的I/O流和该文件描述符进行结合,让我们可以通过流的方式操作文件描述符
// 但是为什么我们不直接打开文件呢?因为这个函数主要是用于一些特殊的文件描述符的,比如创建管道和创建socket套接字获得的文件描述符,这些文件描述符没有路径,所以我们可以通过fdopen来与之结合
FILE *freopen(const char *restrict pathname, const char *restrict mode, FILE *restrict stream);
// 一般用来重定向标准输入,标准输出和标准错误(重要!)
// 意思是我们可以把输出到终端的数据输出到文件中;把从终端中输入变为从文件中输入读取
// pathname:我们指定的文件,可以替代标准输出的位置和标准输入的来源
上面的mode
参数是一个字符串类型,用于指定标准I/O
流的读写方式,具体如下图,他们和文件状态标志对应:
书上还给出了一些注意事项,如下:
打开文件返回标准I/O
流的例子就不写了,这里写一个重定向标准输入和标准输出的例子
这里我从文件中读取数据并经过简单计算然后写入另一个文件
注意打开之后需要关闭,这里就是关闭标准输入和输出,注意最好是在程序末尾,不然关闭了之后就没办法正常输入输出了,下面也给出了具体说明
#include
using namespace std;
int main() {
// 重定向stdin和stdout
freopen("03_src.txt", "r", stdin);
freopen("03_dest.txt", "w", stdout);
int a, b;
cin >> a >> b;
cout << a + b << endl;
// 关闭重定向的标准输入输出流,注意放在末尾,不然关了之后标准输入输出用不了
fclose(stdout);
fclose(stdin);
cout << "hello" << endl;
return 0;
}
结果:
确实关闭了之后hello
没有打印出来
我就放书上的图了
我们可以用flose
函数来关闭一个打开的流,这也是为什么前面关闭之后就没办法打印到终端了
#include
int fclose(FILE *stream);
注意:
一旦打开了流,我们有三种不同的方式的非格式化I/O
进行选择,然后进行读写操作:
I/O
,意思是一次读写一个字符;如果流是带缓冲的,那么标准I/O
会处理缓冲I/O
,我们可以借助标准I/O
函数fgets
和fputs
来实现,每行都以一个换行符终止I/O
,每次I/O
操作的时候读或者写指定的对象,这个对象具有一定的长度;比如fread
函数和fwrite
函数,他们可以让底层的read
和write
函数调用次数更少,因此执行效率更高以下的函数可以用于一次读一个字符
#include
// getc和fgetc都可以用于从一个流当中读取一个字符
// 区别是:getc可以被实现为宏,fgetc不可以
int getc(FILE *stream);
// 返回值:由于我们使用的编码是utf-8,所以是单字节定向,对应的是ASCII码集,返回的值是int类型,也就是字符的ASCII码值
// 当读取到文件末尾或者发生错误的时候,返回EOF,EOF是标准I/O定义的一个宏,值为-1
int fgetc(FILE *stream);
// 除了上面的一个区别其他相同
int getchar(void);
// 等效于getc(stdin); 就不赘述了
所以和read
函数类似的,我们要检验读取到文件末尾,但是这里不同的是当读取到文件末尾或者发生错误的时候这三个函数都返回相同的值,都是EOF
,所以我们要想办法区分
这两个函数就是用来检测到底是发生错误还是读取到末尾了;
标准I/O
为每个FILE
对象维护了两个标志,也就是出错标志和文件末尾标志,他们就可以分别被下面的ferror
和feof
来获得,这样我们就可以判断到底是什么状况
#include
// 获得文件的出错标志 1真 0假
int ferror(FILE *stream);
// 获得文件末尾标志 1真 0假
int feof(FILE *stream);
// 可以清除这两个标志
void clearerr(FILE *stream);
我们写一个程序来实操一下
我们做两次测试,一次正常读完,一次读取出错,如下所示:
#include
using namespace std;
int main() {
// 复习一下,将标准输入重定向
freopen("04.txt", "r", stdin);
// freopen("05.txt", "r", stdin);
int ret;
while (EOF != (ret = getc(stdin)))
cout << (unsigned char)ret;
// 我们来看一下EOF对应的是哪个状态
if (ferror(stdin))
cout << "ferror: read error" << endl;
if (feof(stdin))
cout << "feof: end of file" << endl;
// 关闭
fclose(stdin);
return 0;
}
结果:
正常读完
读取错误
当从流当中读取数据之后,我们可以调用ungetc
把字符压回流当中
这里我准备从一个文件当中读取数据,然后读取之前我压入了一些字符到流中
#include
using namespace std;
int main() {
// 打开一个文件
FILE* file_stream = fopen("04.txt", "r");
// 先压入几个字符
for (int i = 0; i < 10; ++i)
ungetc((int)'a' + i, file_stream);
int ret = EOF;
while (EOF != (ret = getc(file_stream)))
cout << (unsigned char)ret;
return 0;
}
结果:
我压入的顺序是a
到j
,但是输出的顺序是j
到a
,可以压入和输出的关系是一个栈的关系,这一点注意一下
注意:
EOF
,因为下一次就读到他表示错误或者文件结束,但是其实并没有,就会出问题EOF
,之所以能这样做是一次成功的ungetc
会调用clearerr
函数清除两个标志,因为压入之后这两个标志应该会不存在对应上面的输入函数,也有一个输出函数,就是一次输出一个字符到输出缓冲区
#include
// 完全和上面的对应,这里就不写了
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
putc
函数和ungetc
函数都可以向流中写数据,putc
一次写一个字符到输出流缓冲区中,ungetc
和他的区别大了,他是往读取缓冲区写,这样读的时候就会先读取我写的数据,注意如果不加设置为不带缓冲他们会先写到缓冲区当中
我们用一个程序深刻理解上面的意思
这里我打开一个文件用读和追加的方式打开,然后向文件中写数据,我人为指定为全缓冲,由系统给我分配,然后写完我休眠几秒,然后再读取,我们看一下程序的结果
#include
using namespace std;
#include
int main() {
// 打开一个文件
FILE* file_stream = fopen("04.txt", "a+");
setvbuf(file_stream, nullptr, _IOFBF, 0);
// 先写入几个字符
for (int i = 0; i < 10; ++i)
putc((int)'a' + i, file_stream);
// 最后写一个换行符
putc((int)'\n', file_stream);
sleep(5);
// 重置文件偏移量指针
fseek(file_stream, 0, SEEK_SET);
int ret = EOF;
while (EOF != (ret = getc(file_stream)))
cout << (unsigned char)ret;
return 0;
}
结果:
程序睡了5秒之后才输出,这也证明了是数据是先到缓冲区当中了,然后后面调用了fseek
或者getc
函数刷新了缓冲区,因此就能正常读到数据,注意这里putc
和getc
使用的都是系统给我分配的全缓冲,我们在两次操作之间缓冲区被冲洗了;最后输出到终端上用的标准输出的行缓冲,这个与上面无关
下面的函数提供了每次输入一行的功能
#include
// 这两个函数都制定了缓冲区的地址,然后将读到的行送到buf中
// fgets函数可以指定从指定的流读取
char* fgets(char* restrict buf, int n, FILE* restrict fp);
// gets从标准输入读
char* gets(char* buf);
注意:
注意这里buf
是我们存储数据的地方,不是缓冲区,就像read
函数需要buf
来存放数据一样,缓冲区需要我们指定大小,就是n
,如果我们没用setvbuf
设置缓冲区的属性,那么默认就由系统给我们分配,然后程序结束系统给我们回收(这里要理解好)
对于fgets
,我们必须指定缓冲区的长度,这个函数直到读到下一个换行符为止,由于字符串的末尾有一个'\0'
符号,所以我们实际上只能读取到n-1
个字符,这也是标准I/O
为我们提供的保护;但是如果该行包括最后一个换行符超出了n-1
个字符,则fgets
只返回一个不正常的行,但是缓冲区只以null
字节结尾,下一次的调用会从这里继续
我们不推荐使用gets
函数,因为他没有手动指定缓冲区的大小,这就可能造成缓冲区溢出,然后导致内存泄漏,这一点的危害是致命的,所以我们不推荐,而且它也很局限,只能从标准输入中读取
#include
using namespace std;
#include
int main() {
FILE* file_stream = fopen("05.txt", "r");
if (!file_stream) {
perror("fopen");
return -1;
}
char buf[1024] = {0};
fgets(buf, 5, file_stream); // 如果读的数据超过缓冲区大小,那么最后一个字符会留为'\0',然后冲洗缓冲区到buf当中
cout << buf;
sleep(5);
return 0;
}
结果:
程序睡了五秒之后然后输出,为什么只有4个字符?因为缓冲区满了,但是他会留出末尾一个’\0’符号,然后送到存储位置buf
中,然后由于标准输出是行缓冲,没有换行符,所以就睡了五秒再输出
#include
int fputs(const char *restrict s, FILE *restrict stream);
// 将一个以null字节结尾的字符串写入到指定的流,尾部的null('\0')不写出
// 注意,这个函数的判断是以null字节,他不会在末尾给你补上换行符'\n'
// 还有,是把这个字符串写到目标流的缓冲区中,那什么时候写入就需要看流是什么缓冲了
// 通常我们都喜欢在null字节前,放一个回车换行符'\n'
int puts(const char *s);
// 写到标准输出,并且在后面自带一个换行符,可能是写到标准输出默认写道终端,换行更美观
// 我们尽量避免使用puts函数,虽然他并不像gets可能让缓冲区爆掉,因为这样我们就可以不用关心是否需要手动写一个换行符
// 但是有时候我们打印一行数据用puts还是挺香的
#include
using namespace std;
#include
int main() {
const char* str = "helloworld";
fputs(str, stdout); // 不带换行符
sleep(3);
puts(str); // 自带换行符
return 0;
}
结果:
程序睡了3秒输出,因为标准输出是行缓冲,fputs
函数输出的字符串不带回车符,puts
带有
上面的函数以一次一个字符或者一次一行进行操作,如果我们进行二进制I/O操作,那么我们更愿意一次读写一个完整的结构
getc
和putc
函数,那么我们一次只能读或者写一个字符,必须通过循环进行整个结构的读写;fputs
和fgets
函数,fgets
函数遇到换行符’\n’或者缓冲区满或者遇到null
字节会停止,fputs
函数遇到null字节就会停止,这样想要读完也要循环,也是相对比较麻烦的因此类似于系统调用的read
和write
函数,我们这里有两个二进制I/O
操作可以读取一个完整的结构
#include
size_t fread(void *restrict ptr, size_t size, size_t count, FILE *restrict stream);
size_t fwrite(const void *restrict ptr, size_t size, size_t count, FILE *restrict stream);
// 参数:
// ptr:要存放读取数据的存储区或者写出数据的数据来源区
// size:读取或者写入的数据单元的大小,我们一般读文件都是读字符串,然后就给char的大小1就好
// count:需要读或者写的大小,读的话我们不知道stream的数据有多大,可以给存储区ptr的大小,这也说明了返回值小于count不一定是错误,也有可能是文件读到末尾了,因为我们事先不知道stream的数据有多大;写的话就给ptr数组数据的个数就好了,如果不相等就是错误
// stream:标准I/O文件流
这两个函数的作用在下面给出:
有两种常见的用法:
这两种用法其实有相同的地方,第一个参数传入的是想要读或者写入的结构单位,第二个参数传入的是这个单位的大小,第三个参数传入的是想要读或者写的个数,第四个参数给定的是指定的流,当然首先是读入或者写入缓冲区,后面根据情况判断什么时候才会到达目标位置
这两个函数返回读或者写的对象个数
count
,这个时候可以用ferror
或者feof
来判断是哪一种情况count
,那么就是出错了第一个例子,我们把一些二进制数据和结构进行标准输出
#include
using namespace std;
#include
struct Person {
string name;
string sex;
double height;
double weight;
};
int main() {
// 将二进制数组的一些元素写到标准输出
char data[10] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'};
fwrite(&data[2], sizeof(char), 3, stdout);
puts(""); // 输出一个空行
// 写一个结构到标准输出
Person p{"Paul", "boy", 190, 88.5};
fwrite(&p, sizeof(p), 1, stdout);
puts(""); // 输出一个空行
return 0;
}
结果:
字符串是可以正常输出的,其他类型的数据可能因为编码或者类型问题会出现乱码,但是我们一般都是处理字符串,所以问题不大
第二个例子,我们从文件当中读取数据,然后输出,注意体会第二个参数size
和第三个参数count
的含义
#include
#include
using namespace std;
#define MAX_BUFFER_SIZE 1024
int main() {
// 从06.txt中读取数据,然后输出到屏幕上
FILE* file_stream = fopen("06.txt", "r");
if (!file_stream) {
perror("fopen");
return -1;
}
char buf[MAX_BUFFER_SIZE] = {0};
// 读到buf中
fread(buf, 1, sizeof(buf), file_stream);
// 标准输出
int ret = fwrite(buf, 1, strlen(buf), stdout);
if (ret != strlen(buf)) {
perror("fwrite");
return -1;
}
return 0;
}
结果:
我们读取和写出的单位都是字符,所以size
给的是1,读取的时候不知道stream
数据到底有多少,所以我们给大一点,可以给buf
数据区的大小,所以返回值不为count有可能是读到末尾或者失败;写的时候就给buf
的实际长度。
注意,buf
可不是file_stream
的读写缓冲区,buf
是我们指定的存储数据的地方,从写之后strlen(buf)
不为0这一点也可以看出来
注意fread
,fwrite
函数和read
,write
函数的联系和区别,他们的使用方式还是有区别的
有三种方法定位I/O流,如下图:
我们了解下面的函数即可:
#include
long ftell(FILE *stream);
// 返回当前文件位置的偏移量,错误则返回long(-1),并且修改错误号
int fseek(FILE *stream, long offset, int whence);
// 和lseek函数一样,可以设置文件偏移量指针
// 第一个参数是文件流指针,第二个参数是我们给的偏移量,这个偏移量可正可负,也就是说,我们的指针可以往前移
// 第三个参数`whence`:
// - `SEEK_SET` 设置偏移量,从头开始
// - `SEEK_CUR` 设置偏移量:当前位置 + 第二参数`offset`的值
// - `SEEK_END` 设置偏移量:文件大小 + 第二参数`offset`的值
// 返回值是文件指针的新位置,失败返回-1并且修改`errno`
void rewind(FILE *stream);
// 移动文件指针到开头
还有两个函数,他们除了类型和前面的函数不一样之外,是off_t,其他相同,但是我们还是倾向于用上面的函数:
#include
typedef long off_t
// 这里的类型是off_t,前面是long
off_t ftello(FILE *stream);
int fseeko(FILE *stream, off_t offset, int whence);
#include
using namespace std;
#define MAX_BUFFER_SIZE 1024
int main() {
FILE* file_stream = fopen("07.txt", "r");
if (!file_stream) {
perror("fopen");
return -1;
}
long pos = ftell(file_stream);
cout << pos << endl;
// 现在我读取一个字符
char buf[MAX_BUFFER_SIZE] = {0};
fread(buf, 1, 1, file_stream);
cout << buf << endl;
pos = ftell(file_stream);
cout << pos << endl;
// 设置文件偏移指针到开头
// rewind(file_stream);
fseek(file_stream, 0, SEEK_SET);
fread(buf, 1, 2, file_stream); // 这里第二次读从buf的地址位置开始写入,所以之前的数据会被覆盖
cout << buf << endl;
pos = ftell(file_stream);
cout << pos << endl;
return 0;
}
注意里面第二次fread的注释,为什么会被覆盖
结果:
显然在我们的预期内
格式化I/O
是通过printf
系列函数来处理的
#include
// 将格式化数据写到标准输出
int printf(const char *restrict format, ...);
// 将格式化数据写到指定的流
int fprintf(FILE *restrict stream, const char *restrict format, ...);
// 将格式化数据写到指定的文件描述符
int dprintf(int fd, const char *restrict format, ...);
// 前三个函数成功返回输出字符数,如果输出错误,返回负值
// 将格式化数据写到我们指定的buf存储区
int sprintf(char *restrict buf, const char *restrict format, ...);
// 成功返回输入的字符数,如果编码错误,返回负值
// 为了避免sprintf函数可能造成的buf装满而爆掉,引入了snprintf函数,需要给出指定的长度
int snprintf(char *restrict buf, size_t size, const char *restrict format, ...);
// 如果给定的size足够大,返回将要存入数组的字符数,如果编码错误,返回负值
这显然就涉及到格式化的标准了,当然这里需要我们自己进行对数据进行合适的格式化处理
这些标志其中括号里面是可选的,convtype
是不可选的,如下图:
以下几个函数用作格式化输入
#include
// 从标准输入中读取
int scanf(const char *restrict format, ...);
// 从标准输入中读取写入流中(是先写入流缓冲区中)
int fscanf(FILE *restrict stream, const char *restrict format, ...);
// 从标准输入中读取写入字符串str中
int sscanf(const char *restrict str, const char *restrict format, ...);
这几个函数返回值我们可以不用判断,判断了也没有什么作用,所以只提一嘴:返回赋值的输入项数,若输入出错或者在任一转换之前已经到达文件末端,则返回EOF
同前面,截图: