第五章:标准I/O(1)

第五章 标准I/O

这一章涉及到C标准库当中的内容,是在Linux操作系统之上进行的封装扩充,牛客Linux的课程没有涉及到,所以认真学

不仅是UNIX,很多其他操作系统都实现了标准I/O库,这个库由ISO C标准进行说明

标准库的I/O处理了很多细节,比如缓冲区分配,以优化的块长度执行I/O等等,在系统调用的基础上使得用户的调用更加的方便和严谨安全;这些处理使得用户不用担心如何正确选择正确的块长度;但是我们也要深入了解以下标准I/O库函数的操作,以及是如何与系统调用联系起来的,否则出了问题不知道怎么办

流和FILE对象

前面提到的I/O都是围绕文件描述符的,我们打开一个文件返回给我们一个文件描述符,然后我们通过对文件描述符对文件进行后续的操作;

但是对于标准I/O,所有的操作都是围绕流(stream)展开的,我们打开一个文件,标准I/O返回我们一个流用于进行和文件的关联

下面我们看一下单字节流和多字节流:

  • 对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符可以用多个字节表示。标准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呢?这表示我们尝试设置不成功,但是程序没有异常终止,这就是这个函数的特性了

image-20230905200000664

从上面我们可以看出,fwide函数没有办法改变已定向流的定向,并且没有出错返回;

如果流无效的时候,我们该怎么办呢?我们可以在调用fwide之前,清除errno,然后执行函数之后检查errno的值,这个倒不算重要

另外,当我们用fopen打开一个文件时,返回给我们一个操作文件的流,这个流包含了I/O管理这个文件的所有信息,包括内核实际上使用的文件描述符,指向该流使用缓冲区(这个缓冲区是用户区的那个)的指针,缓冲区的长度,当前缓冲区的字符数以及出错的标志等等

单字节和多字节

所以说了这么多,我们还是要区分一下单字节和多字节:

  • 单字节就是用一个字节就可以表示出所有的字符,也就是8位,也就是可以表示最多256个字符,这一点在英语当中是没有问题的,这也是ASCII字符集使用的字节表示方式
  • 但是单字节没有办法统一表示国际上的所有字符,比如不同国家就有自己的字符,汉字也有自己的字符,所以这个时候单字节就显得少了,所以引入了多字节,多字节中又可以分为统一有多少个字节表示的标准和可以由一个或者多个字符表示的标准,但是这不是我们了解的重点;
  • 重点是不同的标准下就对文件有了不同的编码,如果我们不使用统一的编码,文件中就很可能会出现乱码,现在普遍使用的编码方式就是utf-8,就是对应8位,单字节,对应的就是ASCII编码集;像GB2312这些就是多字节编码,我们后面都不考虑,只考虑单字节

标准输入,标准输出和标准错误(流)

与文件描述符的0 1 2类似的,内核对进程预定义了三个流,stdinstdoutstderr,这三个流就对应了系统调用中的文件描述符STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO

缓冲

标准I/O库提供缓冲的目的就是尽可能少的调用writeread函数,也是对每个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;
    }
    

    结果:

    可见标准输出对应的是行缓冲,当我把字符串和换行符分开,就没办法及时输出了,遇到换行符就会立即刷新缓冲区

    image-20230906145510209

    注意:

    • 标准库I/O提供的行缓冲区大小是有限的,所以如果填满了缓冲区,即使没有换行符,也会进行I/O操作(这里有点像上面的行缓冲)
    • 任何时候通过标准I/O库从一个不带缓冲的流,或者一个行缓冲的流中得到输入数据,那么系统会冲洗缓冲区输出流进行输出;对于第二个行缓冲,它可能需要从内核中读取数据,也可能不需要,因为数据可能在缓冲区中,但是对于不带缓冲的流肯定需要从内核的缓冲中获得数据
  • 不带缓冲

    标准I/O库不对字符进行缓冲存储(内核中还有缓冲),意思是我们希望数据尽快的输入或者输出;例如我们调用fputs函数输出一些字符到不带缓冲的流当中,我们就期望这些数据能够尽快输出,这时候在底层很可能就调用了系统API的write进行后续操作

    标准错误stderr通常是不带缓冲的,因为错误信息应该尽可能快的显示出来,而不需要管他们有没有换行符

ISO C标准要求缓冲有下列特征:

  • 标准输入和标准输入通常是行缓冲的,当且仅当他们不指向交互式设备(例如,键盘,鼠标,显示器等等),才是全缓冲
  • 标准错误绝不可能是全缓冲的

但是上面的说法并没有告诉我们,当标准输入和输出指向交互式设备的时候是行缓冲还是不带缓冲;标准错误是行缓冲还是不带缓冲,所以一般来说系统默认使用如下的缓冲:

  • 标准输入和标准输出,指向交互式设备的时候是行缓冲(例如在终端屏幕上输出信息),否则就是全缓冲
  • 标准错误是不带缓冲的

关于标准I/O缓冲后续会说的更具体

函数setbuf和setvbuf

对于一个流,系统一般会默认给流缓冲的方式,比如标准输入输出使用行缓冲默认定向到终端,如果我们想要修改可以通过如下的函数进行修改

这两个函数的第一个参数都要求传入要给已经打开的流

#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(1)_第1张图片

我们要知道,虽然我们指定了缓冲区的大小,但是缓冲区中还可能存放着他自己的管理操作信息,所以可以存放在当中的实际字节数要小于缓冲区的大小,这个并不是很重要,因为缓冲区的大小一般都是往上开够用了

一般而言,我们可以让系统自己选择缓冲区的长度,然后自动分配缓冲区,这样关闭流的时候,标准I/O库会自动释放缓冲区

函数fflush

任何时候,我们都可以强制冲洗一个流

这个函数会让所有未写的数据都被送至内核当汇总(内核中也有缓冲区,然后就可以进行后续的操作)

如果传递的是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给我们的是全缓冲

image-20230907150903812

不带缓冲

下面我给一个不带缓冲的例子

#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函数之后立即输出的,可以看出不带缓冲

image-20230907151214997

行缓冲

那怎么能少了行缓冲呢?还是全缓冲的例子,我这次设置为行缓冲

#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会自带一个换行符,所以也是立即输出的

image-20230907151444480

打开流

我们可以用以下的函数来打开一个标准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(1)_第2张图片

书上还给出了一些注意事项,如下:

第五章:标准I/O(1)_第3张图片
例子

打开文件返回标准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没有打印出来

第五章:标准I/O(1)_第4张图片

其他注意事项(了解)

我就放书上的图了

第五章:标准I/O(1)_第5张图片 第五章:标准I/O(1)_第6张图片
函数fclose

我们可以用flose函数来关闭一个打开的流,这也是为什么前面关闭之后就没办法打印到终端了

#include 

int fclose(FILE *stream);

注意:

  • 在文件关闭之前,系统会自动冲洗输出缓冲中的所有数据,然后进行输出;输入缓冲区中的所有数据将被丢弃
  • 如果标准I/O库已经为该流自动分配了一个缓冲区,那么系统会自动释放该缓冲区
  • 同样当一个进程终止或者结束的时候,也会自动冲洗输出缓冲中的所有数据,输入缓冲区的数据会被丢弃;所有打开的标准I/O流都会被关闭

读和写流

一旦打开了流,我们有三种不同的方式的非格式化I/O进行选择,然后进行读写操作:

  • 每次一个字符的I/O,意思是一次读写一个字符;如果流是带缓冲的,那么标准I/O会处理缓冲
  • 每次一行的I/O,我们可以借助标准I/O函数fgetsfputs来实现,每行都以一个换行符终止
  • 直接I/O,每次I/O操作的时候读或者写指定的对象,这个对象具有一定的长度;比如fread函数和fwrite函数,他们可以让底层的readwrite函数调用次数更少,因此执行效率更高
输入函数
getc系列

以下的函数可以用于一次读一个字符

#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,所以我们要想办法区分

函数ferror和feof

这两个函数就是用来检测到底是发生错误还是读取到末尾了;

标准I/O为每个FILE对象维护了两个标志,也就是出错标志和文件末尾标志,他们就可以分别被下面的ferrorfeof来获得,这样我们就可以判断到底是什么状况

#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;
}

结果:

正常读完

第五章:标准I/O(1)_第7张图片

读取错误

第五章:标准I/O(1)_第8张图片

函数ungetc

当从流当中读取数据之后,我们可以调用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;
}

结果:

我压入的顺序是aj,但是输出的顺序是ja,可以压入和输出的关系是一个栈的关系,这一点注意一下

image-20230907171454664

注意:

  • 我们不能回送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函数刷新了缓冲区,因此就能正常读到数据,注意这里putcgetc使用的都是系统给我分配的全缓冲,我们在两次操作之间缓冲区被冲洗了;最后输出到终端上用的标准输出的行缓冲,这个与上面无关

第五章:标准I/O(1)_第9张图片

每行一行I/O

每次输入一行(fgets系列)

下面的函数提供了每次输入一行的功能

#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,然后由于标准输出是行缓冲,没有换行符,所以就睡了五秒再输出

image-20230909093226773

每次输出一行(fputs系列)
#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带有

image-20230909095214017

二进制I/O

上面的函数以一次一个字符或者一次一行进行操作,如果我们进行二进制I/O操作,那么我们更愿意一次读写一个完整的结构

  • 如果使用getcputc函数,那么我们一次只能读或者写一个字符,必须通过循环进行整个结构的读写;
  • 如果使用fputsfgets函数,fgets函数遇到换行符’\n’或者缓冲区满或者遇到null字节会停止,fputs函数遇到null字节就会停止,这样想要读完也要循环,也是相对比较麻烦的

因此类似于系统调用的readwrite函数,我们这里有两个二进制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文件流

这两个函数的作用在下面给出:

  • 有两种常见的用法:

    • 读或者写一个二进制数组

      第五章:标准I/O(1)_第10张图片

    • 读或者写一个结构

      第五章:标准I/O(1)_第11张图片

    这两种用法其实有相同的地方,第一个参数传入的是想要读或者写入的结构单位,第二个参数传入的是这个单位的大小,第三个参数传入的是想要读或者写的个数,第四个参数给定的是指定的流,当然首先是读入或者写入缓冲区,后面根据情况判断什么时候才会到达目标位置

  • 这两个函数返回读或者写的对象个数

    • 对于读,如果出错或者到达文件末尾,返回的值不为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;
}

结果:

字符串是可以正常输出的,其他类型的数据可能因为编码或者类型问题会出现乱码,但是我们一般都是处理字符串,所以问题不大

image-20230909105848043

第二个例子,我们从文件当中读取数据,然后输出,注意体会第二个参数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这一点也可以看出来

第五章:标准I/O(1)_第12张图片

注意freadfwrite函数和readwrite函数的联系和区别,他们的使用方式还是有区别的

定位流

有三种方法定位I/O流,如下图:

第五章:标准I/O(1)_第13张图片

我们了解下面的函数即可:

#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(1)_第14张图片

格式化I/O

格式化输出
printf系列

格式化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是不可选的,如下图:

第五章:标准I/O(1)_第15张图片 第五章:标准I/O(1)_第16张图片

第五章:标准I/O(1)_第17张图片

vprintf函数(了解)
第五章:标准I/O(1)_第18张图片
格式化输入
scanf系列

以下几个函数用作格式化输入

#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

格式化标准

同前面,截图:

第五章:标准I/O(1)_第19张图片

第五章:标准I/O(1)_第20张图片

vscanf系列(了解)

第五章:标准I/O(1)_第21张图片

第五章:标准I/O(1)_第22张图片

你可能感兴趣的:(《UNIX环境高级编程》,linux,c++)