标准 I/O

news2024/11/18 17:32:01

标准 I/O

引言

I/O 是一切实现的基础,其分为标准 I/O 和文件 I/O。

文件 I/O 依赖操作系统,因系统的实现方式而定,对于程序员来说会造成很大困扰。如打开文件,Linux 系统调用为 open() 函数,而 Windows 的系统调用为 openfile() 函数。于是就有了标准 I/O,提供了一套标准实现的库函数,如打开文件使用 fopen() 函数,它本质上也是调用文件 I/O,但是合并了文件 I/O 的调用,方便程序员调用。不仅是 Unix,很多其他操作系统都实现了标准 I/O 库,这些库由 ISO C 标准说明。通过下图可以清楚理解标准 I/O 的在系统中的位置

在这里插入图片描述

在两个 I/O 均可以使用的情况下,应优先使用标准 I/O,因为使用标准 I/O 的程序移植性更好。

使用标准 I/O 进行打开或创建一个文件操作时,会使用一个流与一个文件相关联。这个流是一个指向 FILE 对象的指针,标准 I/O 的所有操作都是围绕这个流进行。该对象通常是一个结构,包含了标准 I/O 库为管理该流需要的所有信息,包括用于实际 I/O 的文件描述符、指向用于该流缓冲区的指针、缓冲区长度、当前在缓冲区中的字符以及出错标志等。

标准 I/O 有以下常用的库函数,分类如下:

  • 打开/关闭流fopen/fclose
  • 读写流fgetcfputcfgetsfputsfreadfwriteprintf 族、scanf
  • 定位流fseekftellrewind
  • 缓冲区刷新flush

打开 & 关闭流

打开流 fopen

#include <stdio.h>

/**
  * @param
  *   pathname:文件的路径名
  *   mode:文件的打开方式
  * @return: 成功返回指向 FILE 对象的指针,失败返回 NULL,并用 errno 指示错误
  */
FILE *fopen(const char *pathname, const char *mode);

mode 方式打开文件名为 pathname 的文件,并为其关联一个流。参数 mode 指向以下序列之一开头的字符串(如 mode 的字符串是 read,在实际运行时只会识别 r,忽略其他字符):

  • r:以只读的方式打开文件,并将文件流指针定位于文件开始处
  • r+:以读写的方式打开文件,并将文件流指针定位于文件开始处
  • w:以写的方式创建文件(文件不存在)或截断文件(文件存在),并将文件流指针定位于文件开始处
  • w+:以读写的方式创建文件(文件不存在)或截断文件(文件存在),并将文件流指针定位于文件开始处
  • a:以追加的方式创建文件(文件不存在)或打开文件(文件存在),并将文件流指针定位于文件结尾处
  • a+:以读和追加的方式创建文件(文件不存在)或打开文件(文件存在),如果是进行写操作,则文件流指针总是定位于文件结尾处;如果是进行读操作,在 POSIX 中没有说明此模式初始读取位置,glibc 在此模式下初始读取位置在文件开始处,Android/BSD/MacOs 在此模式下初始读取位置在文件结尾处

只有模式 rr+ 要求文件必须存在,其它模式都是文件存在则清空,不存在则创建。这个 mode 字符串还可以包含字符 'b' 作为最后一个字符或上述任意双字符字符串的字符中间,来表示处理的文件二进制文件。但是在 POSIX 标准的系统中,内核并不会对两种文件进行区分,字符 'b' 作为 mode 的一部分并无作用,如 Unix;而在其他标准的系统中则必须加上字符 'b',如 Windows。如果读取的文件是二进制文件,并且希望程序在多个操作系统中运行,建议加上字符 'b'

当时使用 wa 模式创建一个新文件,我们无法说明该文件的访问权限位,POSIX.1 要求实现使用如下的权限位集来创建文件:

S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH (0666)

我们可以通过调整 umask 值来限制这些权限。

如果有多个进程用标准 I/O 追加写方式打开同一个文件,那么来自每个进程的数据都将正确的写到文件中。当以读和写类型打开一个文件时(type 中 + 号),具有一下限制:

  • 如果中间没有 fflushfseekfsetposrewind,则在输出的后面不能直接跟随输入
  • 如果中间没有 fseekfsetposrewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出

关闭流 fclose

#include <stdio.h>

/**
  * @param:
  *   stream —— 需要关闭的流(此流必须是已经被打开的)
  * @return: 关闭成功返回 0,关闭失败返回 EOF,并用 errno 指示错误
  */
int fclose(FILE *stream);

fclose 会先刷新 stream 指定的流,然后关闭底层的文件描述符。如果这个指针是非法的或已经被关闭的流,则该行为是未定义的。

当一个进程正在终止时(直接调用 exit 函数,或从 main 函数返回),则所有带未写缓冲数据的标准 I/O 流都被冲洗,所有打开的标准 I/O 流都被关闭。

打开 & 关闭流的使用案例

#include <stdio.h>
#include <stdlib.h>

int main() {
  FILE *fp = fopen("tmp", "r");
  if (fp == NULL) {
    perror("open file failed");
    exit(EXIT_FAILURE);
  }

  puts("OK!");
  fclose(fp);

  return 0;
}

FILE * 所指对象在内存中的位置

fopen() 库函数返回的是一个指向 FILE 对象的指针,对于库函数返回指针类型需要思考一个问题 —— 这个指针所指对象是存储在哪一块内存空间?是栈?还是静态区?还是堆区?

  • 栈空间:栈空间中的变量都是自动存储类型,也就是说 tmp 是自动类型,一旦函数调用结束,tmp 的内存就会被释放,如果返回 tmp 的地址就是一个未定义行为(相当于野指针),因此不可能是栈空间
FILE *fopen(const char *filename, const char *mode) {
  FILE tmp;

  // 给结构体成员赋值初始化
  tmp.xxx = xxx;
  tmp.yyy = yyy;
  // ...

  return &tmp;
}
  • 静态区:静态区中的每一个对象都只能有一份,如果该结构体对象在静态区的话,如果我们连续打开多个文件,那么这个结构体就会被之后所打开文件的信息所覆盖,也就指向最后一个打开的文件流对象。也就是说,如果此对象在静态区,就只能操作一个打开的文件,但实际情况是可以操作多个打开的文件,因此流对象也不在静态区
FILE *fopen(const char *filename, const char *mode) {
  static FILE tmp;

  // 给结构体成员赋值初始化
  tmp.xxx = xxx;
  tmp.yyy = yyy;
  // ...

  return &tmp;
}
  • 堆区:堆区中的对象只有在调用 free 后才会释放其内存,而我们的流对象就是在每次调用 fclose() 之后才关闭,因此流对象最有可能是在堆区。如下,变量 tmp 具有动态存储期,从调用 malloc 分配内存到调用 free 释放内存为止,而 free 会在 fclose 函数中被调用,因此可以确定是在堆区。
FILE *fopen(const char *filename, const char *mode) {
  FILE *tmp = NULL;
  tmp = malloc(sizeof(FILE));

  // 给结构体成员赋值初始化
  tmp->xxx = xxx;
  tmp->yyy = yyy;
  // ...

  return tmp;
}

如何判断一个库函数返回的指针所指向对象的所在内存位置:存在互逆操作的库函数,返回的指针一般来说都是存在于堆区;其它库函数返回的指针既可能在堆区,也可能在静态区。

进程最大打开文件数

打开一个文件会在堆中开辟一块内存空间保存文件的相关信息,因此会占用系统资源,那么我们打开文件的数量就必须有上限,这个数量是多少呢?通过下面的程序进行测试:

#include <stdio.h>

int main() {
  FILE *fp = NULL;
  int count = 0;
  while (1) {
    fp = fopen("/tmp/out", "w");
    if (NULL == fp) {
      perror("fopen() error");
      break;
    }
    count++;
  }

  printf("max file opens is  %d\n", count);

  return 0;
}

输出结果为:

open file failed: Too many open files
counts = 1021

虽然打印出来的结果是 1021,但并不是说只能打开 1021 个文件,因为在不更改当前系统默认的环境情况下,有三个文件流是默认打开的,分别是:stdinstdoutstderr,我们也可以通过 Linux 命令 ulimit -a 来查看系统的所有资源使用上限(使用 ulimit -n 来修改可打开文件的上限)。

读和写流

读取一个字符的 I/O

函数原型:

#include <stdio.h>

/**
  * @param:
  *   stream:指定读取的流,从该流中获取下一个字符
  * @return:以无符号 char 类型强制转换成 int 类型的形式返回读取的字符
  *          如果到达文件末尾或发生错误,则返回 EOF
  */
int fgetc(FILE *stream);
int getc(FILE *stream);

// getchar 等价于 getc(stdin)
int getchar(void);

getcfgetc 的区别:getc 被实现为宏,而 fgetc 是普通函数实现。因为宏只占用编译时间,不占用调用时间,而函数刚好相反,因此 fgetc 所需时间很可能比调用 getc 时间要长。因为 fgetc 是函数实现,允许将 fgetc 的地址作为一个参数传送给另一个函数。

不管是出错还是到达文件末尾,上面三个函数都是返回 EOF,为了区分这两种不同的情况,可以调用 feof()ferror()来判断具体的情况,函数原型如下:

#include <stdio.h>

int feof(FILE *stream);      // 如果流到达文件末尾返回非 0,否则返回 0
int ferror(FILE *stream);    // 流出错返回非 0, 否则返回 0

在大多数实现中,为每个流在 FILE 对象中维护两个标志:

  • 出错标志
  • 文件结束标志

可以调用 clearerr 来清楚这些标志,函数原型:

void clearerr(FILE *stream); // 清除流到达文件尾和流出错的标志 

输出一个字符的 I/O

函数原型:

#include <stdio.h>

/**
  * @param:
  *   stream:指定输出的流,在该流中输出一个字符
  * @return:以无符号 char 类型强制转换成 int 类型的形式返回输出的字符
  *          如果发生错误,则返回 EOF
  */
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);

// putchar 等价于 putc(c, stdout)
int putchar(int c);

putcfputc 的区别如同 getcfgetc 一样。

使用字符输入输出流实现简单文件拷贝程序

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  if (3 != argc) {
    fprintf(stderr, "Usage: %s <source> <dest>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  // 打开两个文件,一个进行读,另一个进行写
  FILE *sfp = fopen(argv[1], "r");
  if (NULL == sfp) {
    perror("source fopen() error");
    exit(EXIT_FAILURE);
  }

  FILE *dfp = fopen(argv[2], "w");
  if (NULL == dfp) {
    perror("dest fopen() error");
    fclose(sfp);
    exit(EXIT_FAILURE);
  }

  int ch = 0;
#if 1
  while (EOF != (ch = getc(sfp)))
    putc(ch, dfp);
#else
  while (EOF != (ch = fgetc(sfp)))
    fputc(ch, dfp);
#endif

  // 有打开就要有关闭
  fclose(dfp);
  fclose(sfp);

  return 0;
}

在 Linux 中可以通过 diff 命令来比较两个文件是否一致,如果两个文件一致则不会有任何输出,否则就是不一致。那么如何实现 diff 命令?在后续的学习中理解并实现。

每次读取一行的 I/O

函数原型:

#include <stdio.h>

/**
  * @param:
  *   s:存储流中读取数据的内存空间
  *   size:存储内存空间的大小
  *   stream:指定的读取流
  * @return:读取成功返回读取的字符串,读取到文件末尾没有数据可读或读取失败返回 NULL
  */
char *fgets(char *s, int size, FILE *stream);

// 不要使用这个函数
char *gets(char *S);

fgets 是从指定的流 stream 读取一行,并把它存储在 s 所指向的字符串内。当读取 size-1 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。因此 fgets 读取结束的条件为(满足其一即可):1. 读到 size-1 个字符时停止,size 位置存放 \0;2. 读到换行符 \n 停止;3. 读到文件末尾 EOF

注意:勿用 gets

  • 我们在 C 语言中学到过 gets,也说过这个函数是一个不安全函数。对 fgets,必须指定缓冲的长度 size,此函数一直读到下一个换行符为止,但是不会超过 size-1 个字符,读入的字符会被送入缓冲区,该缓冲区会以 null 结尾。如果该行包括最后一个换行符的字符超过了 size-1,则 fgets 只返回一个不完整的行,但是缓冲区总是以 null 结尾。对 fgets 的下一次调用会继续该行。

  • gets 的调用者在使用此函数时不能指定缓冲区的长度。这样就可能造成缓冲区溢出(如若该行长于缓冲区长度),写到缓冲区之后的存储空间中,从而产生不可预料的后果。这种缺陷曾被利用,造成 1998 年的因特网蠕虫事件。

  • 如果函数的调用者提供一个指向堆栈的指针,并且 gets 函数读入的字符数量超过了缓冲区的空间(即发生溢出),gets 函数会将多出来的字符继续写入堆栈中,这样就会覆盖堆栈中原来的内容,破坏一个或多个不相关变量的值。

虽然 fgets 函数能够读取一整行的字符串,比 gets 更加安全,但是 fgets 本身还是存在缺陷,并不能完全一次性读完一整行的内容,只能读取指定大小内的内容,看下面的例子:

#define BUFFSIZE 5      // 假设缓冲区大小是 5
char buffer[BUFFSIZE];  // 创建一个固定大小的缓冲区
fgets(buffer, BUFFSIZE, stream);  // 从某一个 stream 获取

// 情况一
// 如果 stream = "abc"
// 则 buffer = "abc\n\0"(读到换行符),文件指针指向 EOF,能够一次读取完

// 情况二
// 如果 stream = "abcde"
// 则 buffer = "abcd\0"(读到 size-1),文件指针指向 e,并不能一次读取完

// 情况三
// 如果 stream = "abcd\n",则 fgets 需要两次才能完全读完一行
// 第一次 buffer = "abcd\0"(读到 size-1),文件指针指向 \n
// 第二次 buffer = "\n\0"(读到换行符),文件指针指向 EOF

每次输出一行的 I/O

函数原型:

#include <stdio.h>

/**
  * @param:
  *   s:保存一行数据的内存空间的地址
  *   size:存储内存空间的大小
  *   stream:指定的输出流
  * @return:将一个以 null 字符终止的字符串写到指定的流中,尾端的终止符 null 不写出
  *          成功返回一个非负数,失败返回 EOF
  */
int fputs(const char *s, FILE *stream);

// 等价与 fputs(s, stdout);
int puts(const char *s);

puts 并不像它所对应的 gets 那样不安全,但是我们也应该避免使用它,以免需要记住它在最后是否添加一个换行符。

使用每次一行的 I/O 实现文件拷贝程序

#include <stdio.h>
#include <stdlib.h>

#define BUFFERSIZE 1024

int main(int argc, char *argv[]) {
  if (3 != argc) {
    fprintf(stderr, "Usage: %s <source> <dest>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  // 打开两个文件,一个进行读,另一个进行写
  FILE *sfp = fopen(argv[1], "r");
  if (NULL == sfp) {
    perror("source fopen() error");
    exit(EXIT_FAILURE);
  }

  FILE *dfp = fopen(argv[2], "w");
  if (NULL == dfp) {
    perror("dest fopen() error");
    fclose(sfp);
    exit(EXIT_FAILURE);
  }

  char str[BUFFERSIZE] = {0};
  while (NULL != fgets(str, BUFFERSIZE, sfp))
    fputs(str, dfp);

  // 有打开就要有关闭
  fclose(dfp);
  fclose(sfp);

  return 0;
}

二进制 I/O

函数原型:

#include <stdio.h>

/**
  * @param
  *   ptr:指向一定大小内存块的指针,用于写入或读取
  *   size:读取或写入每个元素的大小
  *   nmemb:读取或写入元素的个数
  *   stream:指定的流
  * @return:成功返回读取或写入元素的个数,出错返回值会比元素个数要小或 0
  */
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

这两个函数的在使用需谨慎,最好是将这两个函数当做是一个按字节读取的函数,如下面的示例:

// 情况一:buffer 的大小是 10 字节,文件中数据足够
fread(buffer, 1, 10, fp); // 读 10 个元素,每个元素 1 字节。返回 10(读到 10 个对象),读到 10 个字节
fread(buffer, 10, 1, fp); // 读 1 个元素,每个元素 10 个字节。返回 1(读到 1 个对象),读到 10 个字节

// 情况二:文件中数据不足,buffer 只有 5 个字节
fread(buffer, 1, 10, fp); // 返回 5(读到 5 个对象),读到 5 个字节
fread(buffer, 10, 1, fp); // 返回 0(读不到一个对象,因为一个对象需要 10 字节,而文件只有 5 字节)

使用二进制 I/O 的基本问题是,它只能用于读在同一系统上已写的数据。在以前这种并没有问题,但是现在有很异构系统通过网络相互连接起来,而且,这种情况非常普遍。常常会有这种情形,在一个系统上写的数据,要在另一个系统上进行处理。在这种环境下,这两个函数是可能就不能正常工作,其原因是:

  1. 在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同,如字节对齐方式。

  2. 用来存储多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同。

使用二进制 I/O 实现文件拷贝程序

#include <stdio.h>
#include <stdlib.h>

#define BUFFERSIZE 1024

int main(int argc, char *argv[]) {
  if (3 != argc) {
    fprintf(stderr, "Usage: %s <source> <dest>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  // 打开两个文件,一个进行读,另一个进行写
  FILE *sfp = fopen(argv[1], "r");
  if (NULL == sfp) {
    perror("source fopen() error");
    exit(EXIT_FAILURE);
  }

  FILE *dfp = fopen(argv[2], "w");
  if (NULL == dfp) {
    perror("dest fopen() error");
    fclose(sfp);
    exit(EXIT_FAILURE);
  }

  char buffer[BUFFERSIZE] = {0};
  int rlen = 0;
  while (0 != (rlen = fread(buffer, 1, BUFFERSIZE, sfp)))
    fwrite(buffer, 1, rlen, dfp);

  // 有打开就要有关闭
  fclose(dfp);
  fclose(sfp);

  return 0;
}

格式化输出 I/O

函数原型:

#include <stdio.h>

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

sprintf 函数可能会造成由 str 指向的缓冲区的溢出,调用者有责任确保该缓冲区足够大。因为缓冲区溢出会造成程序不稳定甚至安全隐患,为了解决这个溢出问题,引入了 snprintf 函数。在该函数中,缓冲区长度是一个显示参数,超过缓冲区尾端写的所有字符都被丢弃。如果缓冲区足够大,snprintf 函数就会返回写入缓冲区的字符数,与 sprintf 相同,该返回值不包括结尾的 null 字节。若 snprintf 函数返回小于缓冲区长度 size 的正值,那么没有阶段输出。若发生一个编码的错误,snprintf 返回一个负值。

格式说明控制其余参数如何编写,其基本形式如下

在这里插入图片描述

具体的各种标志、转换类型可以参考下图

在这里插入图片描述

格式化输入 I/O

函数原型:

#include <stdio.h>

int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

可以使用 scanf 将字符序列转换成指定类型的变量,在格式化之后的各参数包含了变量的地址,用转换结果堆这些变量赋值。格式转换说明如下:

在这里插入图片描述

* 星号可用来抑制转换。

定位流

函数原型:

#include <stdio.h>

/**
  * @param
  *   stream:需要修改文件指示器的流
  *   offset:距离起始位置的偏移量,可正可负,也可以是 0
  *   whence:文件指示器起始位置
  * @return:设置成功返回 0,否则返回 -1,并用 errno 表明错误类型
  */
int fseek(FILE *stream, long offset, int whence);

/**
  * @oaram
  *   stream:需要获取位置的流
  * @return:返回文件流当前指针位置距文件头的位置,失败返回 -1,并用 errno 表明错误类型
  */
long ftell(FILE *stream);


/**
  * @oaram
  *   stream:需要复位的流
  */
void rewind(FILE *stream);  // 将文件指针位置定位到文件开始处,相当于 fseek(stream, 0L, SEEK_SET)

fseek 中的起始位置总共有三种:

  • SEEK_SET:文件的开头
  • SEEK_CUR:文件指针的当前位置
  • SEEK_END:文件的末尾

fseekftell 的缺陷

通过这两个函数的原型我们可以发现,对于 offset 的修饰类型是 long,这是一种十分不好的写法,这种写法也就意味着只能对 2G 左右大小的文件进行操作,否则会超出类型范围。

为了解决这个缺陷,后来 Single UNIX Specification 引入 fseekoftello,这两个函数中的偏移量类型使用的是一个 typedef 定义的 off_t 类型,具体类型由操作系统决定,因此不存在文件大小的限制。在一些计算机架构上,off_t 是 32 位的,但是如果加上 _FILE_OFFSET_BITS 宏定义,就会变成 64 位(在包含任何头文件之前)。

在 ISO C 中引入了 fgetposfsetpos 函数,它们使用一个抽象数据类型 fpos_t 记录文件的位置,这种数据类型可以根据需要定义为一个足够大的数。

函数原型分别是:

int fseeko(FILE *stream, off_t offset, int whence);

off_t ftello(FILE *stream);

int fgetpos(FILE *stream, fpos_t *pos);

int fsetpos(FILE *stream, const fpos_t *pos);

使用定位流获取文件的大小

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  if (2 != argc) {
    fprintf(stderr, "Usage: %s <file>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  FILE *fp = fopen(argv[1], "r");
  if (NULL == fp) {
    perror("fopen() error");
    exit(EXIT_FAILURE);
  }

  fseek(fp, 0l, SEEK_END);
  long count = ftell(fp);

  printf("file size is %ld\n", count);
  fclose(fp);

  return 0;
}

缓冲区

标准 I/O 库提供缓冲的目的就是尽可能减少使用 readwrite 调用的次数,同时也对每个 I/O 流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。

标准 I/O 提供了以下 3 种类型的缓冲:

  1. 全缓冲:在这种情况下,只有填满标准 I/O 缓冲区后才进行实际 I/O 操作。进行文件读写操作时,通常是由标准 I/O 库实施全缓冲,在这个流上执行第一次 I/O 操作时,标准 I/O 函数通常调用 malloc 获得需使用的缓冲区。也可以通过 fflush 进行强制缓冲区刷新的操作,每调用一次就会将缓冲区中的内容写到磁盘上(针对文件读写)或丢弃缓冲区的数据(终端驱动程序)。
  2. 行缓冲:在这种情况下,除了上述的两种情况会刷新缓冲区,在输入输出中遇到换行符时,标准库也会执行 I/O 操作。当流涉及一个终端设备时,通常使用行缓冲。
  3. 不带缓冲:标准 I/O 库不对字符进行缓冲存储,数据会立即读入内存或输出到外存文件和设备上,例如 stderr

大多数系统使用下列类型的缓冲:

  • 标准错误是不带缓冲的;
  • 若是指向终端设备的流,则是行缓冲,否则是全缓冲。

强制刷新缓冲区

#include <stdio.h>

/**
  * @param
  *   stream:需要刷新的流
  * @return:成功返回 0,失败返回 EOF
  */
int fflush(FILE *stream);

对于输出流,fflush 会通过底层的写入函数强制写到输出流或更新流的所有用户空间缓冲区;对于可寻文件相关的输入流(如磁盘文件,但不是管道和终端),fflush 会丢弃任何从底层文件获取但尚未被应用程序使用的缓冲数据;如果参数是 NULL 则刷新所有打开的输出流。

在下面的代码示例中,需要输出的内容在缓冲区内,无法在终端显示,这是因为数据都在缓冲区中,因此无法在终端上显示

#include <stdio.h>

int main() {
  printf("Brfore while");
  while(1);
  printf("After while");

  return 0;
}

如果要将缓冲区中的刷新,对于标准输出有以下几个时机:

  • 输出缓冲区满;
  • 遇到换行符 \n
  • 强制刷新或进程结束。

对上面的代码进行优化,即可在终端进行输出,具体如下:

#include <stdio.h>

int main() {
  printf("Brfore while");
  fflush(NULL); // 强制刷新
  while(1);
  printf("After while\n"); // 遇到 \n 刷新

  return 0;
}

扩展

对于任何一个给定的流,如果我们并不喜欢这些系统默认的缓冲类型,可以设置流的缓冲类型,使用 stevbuf 函数,但是一般都不会去设置,了解即可。这个函数一定要在流已被打开后调用,而且也应在对该流执行任何一个其他操作之前调用。

函数原型void setvbuf(FILE *stream, char *buf, int mode, size_t size);

参数描述

  • stream —— 需要设置缓冲模式的流
  • buf —— 分配给用户的缓冲,如果设置为 NULL,该函数会自动分配一个指定大小的缓冲
  • mode —— 指定流的缓冲模式
  • size —— 指定缓冲的大小
模式描述
_IOFBF全缓冲:对于输出,数据在缓冲填满时被一次性写入。对于输入,缓冲会在请求输入且缓冲为空时被填充
_IOLBF行缓冲:对于输出,数据在遇到换行符或者在缓冲填满时被写入,具体视情况而定。对于输入,缓冲会在请求输入且缓冲为空时被填充,直到遇到下一个换行符
_IONBF无缓冲:不使用缓冲,每个 I/O 操作都被即时写入,buffersize 参数被忽略

如何获取一整行的数据

之前介绍的所有函数都不能获得完整的一整行数据(有缓冲区大小限制),而 getline 函数可以获取完整的一行数据。此函数是动态分配内存,当装不下完整的一行时,又会申请额外的内存来存储,从而一次获取完整的一行数据。

getline 是 C++ 标准库函数,但不是 C 标准库函数,而是 POSIX 所定义的标准库函数(在 POSIX IEEE Std 1003.1-2008 标准出来之前,则只是 GNU 扩展库里的函数)。在 gcc 编译器中,对标准库 stdio 进行了扩展,加入了一个 getline 函数。

getline 会生成一个包含一串从输入流读入字符的字符串,直到以下情况发生会导致生成的此字符串结束:

  • 到文件结束
  • 遇到函数的定界符
  • 输入到达最大限度
#include <stdio.h>

/**
  * @param
  *   lineptr:保存读取数据的内存地址
  *   n:保存读取数据的大小
  *   stream:指定的输入流
  * @return:成功返回读取的字节数,失败返回 -1
  */
ssize_t getline(char **lineptr, size_t *n, FILE *stream);

如果在调用此函数之前将 *lineptr 设置为空指针,*n 设置为 0,getline 将会自动分配内存保存读取的数据,并且在使用完之后会自动释放内存。

从指定的输入流中读取完整的一行数据,并将包含文本的缓冲区的地址存储到 *lineptr,该缓冲区以空字符结尾,并且包含换行符(如果找到);或者在调用之前可以包含指向 malloc 分配的缓冲区的指针,大小是 *n,如果缓冲区不够大,无法容纳该行,会使用 realloc 调整大小;无论哪一种情况,在成功调用后,*lineptr*n 都会被更新,所以这两个值一定要有初始值。

使用 getline 实现拷贝文件程序

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  if (3 != argc) {
    fprintf(stderr, "Usage: %s <source> <dest>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  // 打开两个文件,一个进行读,另一个进行写
  FILE *sfp = fopen(argv[1], "r");
  if (NULL == sfp) {
    perror("source fopen() error");
    exit(EXIT_FAILURE);
  }

  FILE *dfp = fopen(argv[2], "w");
  if (NULL == dfp) {
    perror("dest fopen() error");
    fclose(sfp);
    exit(EXIT_FAILURE);
  }

  char *buffer = NULL;
  size_t size = 0;
  ssize_t rlen = 0;
  while (-1 != (rlen = getline(&buffer, &size, sfp)))
    fputs(buffer, dfp);
  // 有打开就要有关闭
  fclose(dfp);
  fclose(sfp);

  return 0;
}

理解 getline 的原理和使用,自己如何实现一个类似 getline 的程序,其基本逻辑是:

  1. 先判断传入的参数是否合法
  2. 判断存储数据的内存空间是否为空,为空则以默认的内存空间大小动态创建空间
  3. 逐字节获取输入并统计是否超过默认的内存空间大小
    • 超过就动态扩展内存空间
    • 没有超过就将数据保存到内存中
    • 判断是否读取完一行,读取完则退出
  4. 读取错误处理
  5. 将最后的以为字符赋值为 null
ssize_t my_getline(char **lineptr, size_t *n, FILE *stream) {
  if (NULL == lineptr || NULL == n || NULL == stream)
    return -1;

  size_t default_size = DEFAULTSIZE;
  if (NULL == *lineptr) {
    *lineptr = malloc(default_size);
    if (NULL == *lineptr) {
      perror("malloc() error");
      return -1;
    }
    *n = default_size;
  }

  int count = 0;
  int ch;
  while (EOF != (ch = fgetc(stream))) {
    if (count+1 >= default_size) {
      default_size += DEFAULTSIZE;
      char *temp = realloc(*lineptr, default_size);
      if (NULL == temp) {
        free(*lineptr);
        perror("realloc() error");
        return -1;
      }
      *lineptr = temp;
      *n = default_size;
    }

    (*lineptr)[count++] = ch;

    if ('\n' == ch)
      break;
  }

  if (EOF == ch || 0 == count)
    return -1;

  (*lineptr)[count] = '\0';
  return count;
}

临时文件

在项目开发中,我们可能需要产生一些临时文件,但产生临时文件可能需要解决两个问题:

  • 如何保证产生的临时文件命名不冲突
  • 如何保证产生的临时文件能够及时销毁

ISO C 标准 I/O 提供了两个函数以帮助创建临时文件,函数原型如下:

#include <stdio.h>

/**
  * @param:
  *   s:文件名
  * @return:成功返回指向唯一路径名的指针,失败返回 NULL
  */
char *tmpnam(char *s);  // 避免使用此函数,应尽可能使用 tmpfile 和 mkstemp

// 成功返回文件指针,失败返回 NULL
FILE *tmpfile(void);

tmpnam 函数产生一个与现有文件名不同的一个有效路径名字符串。每次调用它时,都产生一个不同的路径名,并且在某个时间点并不存在具有此文件名的文件。最多调用次数是 TMP_MAXTMP_MAX 定义在 <stdio.h> 中。

如果参数 sNULL,这个文件名将在内部静态缓冲区中生成,并可能被下一次调用 tmpnam 时覆盖;如果不是 NULL,文件名被复制到 s 指向的字符数组中(至少是 L_tmpnam 大小的数组),成功时返回 s

使用 tmpnam 有一个缺点:在返回唯一的路径名和用该名字创建文件之间存在一个时间窗口,在这个时间窗口中,另一个进程可以用相同的名字创建文件。因此应该使用 tmpfilemkstemp 函数,这两个函数不存在这个问题。

tmpfile 以二进制更新模式(w+b)创建临时文件,被创建的文件会在流被关闭或程序终止时自动删除。该函数创建的临时文件没有名字,只返回指向流的指针,因此不存在命名冲突的问题,同时会自动删除,因此可以及时销毁。

tmpfile 函数经常使用的标准 UNIX 技术是先调用 tmpnam 产生一个唯 一的路径名,然后,用该路径名创建一个文件,并立即 unlink 它。对一个文件解除链接并不删除其内容,关闭该文件时才删除其内容。而关闭文件可以是显式的,也可以在程序终止时自动进行。

创建临时文件函数的使用

#include <stdio.h>
#include <stdlib.h>

#define MAXSIZE 64

int main() {
  char name[L_tmpnam] = {0};
  char line[MAXSIZE] = {0};
  printf("%s\n", tmpnam(NULL));
  tmpnam(name);
  printf("%s\n", name);

  FILE *fp = NULL;
  if ((fp = tmpfile()) == NULL) {
    fprintf(stderr, "tmpfile error\n");
    exit(EXIT_FAILURE);
  }

  fputs("one line of output\n", fp);
  rewind(fp);
  if (fgets(line, sizeof(line), fp) == NULL) {
    fprintf(stderr, "fgets error\n");
    exit(EXIT_FAILURE);
  }

  fputs(line, stdout);

  return 0;
}

用此 tmpnam 函数创建的临时文件,使用 ls 命令是看不见的,但是的确在磁盘上产生了,这种文件是一种匿名文件。

内存流

分享一个学习资料仓库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2172768.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在新ARM板上移植U-Boot和Linux指南

序言 从支持一个定制板子在U-Boot和Linux中的过程中得到经验以一个带有知名SoC&#xff08;i.MX6&#xff09;且IP已经得到支持的板子为例&#xff0c;这次讨论几乎不涉及编码技能&#xff0c;更多地聚焦在U-Boot部分 一般原则 如果您有您的BSP&#xff08;板级支持包&#…

推荐、nlp、算法题等相关复习(0922-0929)

1. 算法题&#xff1a;路径总和三 求一棵树中所有路径和为targetsum的值&#xff0c;其实有点像和为k的数组&#xff0c;用前缀和来做 先求出前缀和数组&#xff0c;再类似两数之和问题&#xff0c;每次插入一个数&#xff0c;如果target-当前数在哈希表里存在&#xff0c;更…

Vscode: reason: oom, code: -536870904

最近使用github+插件github copilot开发时遇到这个问题, 出现原因:在chat窗口输入了过大的内容。 现象是:每次打开vscode后正常,且打开其他文件夹,再打开chat窗口运行正常。但当再次打开原来的文件夹并打开chat窗口时,则出现此崩溃问题。 尝试以下办法: 1、将D盘虚拟…

Android——添加联系人

概述 第一步 往手机联系人应用中的raw_contacts表添加一条记录 raw_contacts表 ContentValues values new ContentValues();// 往 raw_contacts 添加联系人记录&#xff0c;并获取添加后的联系人编号Uri uri resolver.insert(ContactsContract.RawContacts.CONTENT_URI, …

AI绘画相似风格的作品

目标&#xff1a;根据一张图风格&#xff0c;画出更好的图片 步骤一 等待几分钟&#xff0c;就出来了&#xff0c;点击获取第一个关键词并复制 然后会得到这个四张图片 选中其中的一张&#xff0c;比如第一张 很快就拿到了结果

图解FTP服务器配置:实体用户方式访问案例

任务要求&#xff1a; 某企业搭建一个内部ftp服务器&#xff0c;要求实现如下功能&#xff1a; 用户登录时显示一些欢迎信息&#xff1b;系统账户root、bin不能登录主机&#xff1b;实体用户ligang、liuqiang能够登录ftp服务器&#xff1b;实体用户ligang、liuqiang不能登录本…

数据治理005-血缘关系

数据血缘是元数据产品的核心能力&#xff0c;但数据血缘是典型的看起来很美好但用起来门槛很高的技术&#xff0c;只要你采买过元数据产品就知道了。这篇文章对数据血缘的特征、价值、用途和方法做了系统阐述&#xff1a; 1、特征&#xff1a;归属性、多源性、可追溯及层次性 2…

DOM元素导出图片与PDF:多种方案对比与实现

背景 在日常前端开发中&#xff0c;经常会有把页面的 DOM 元素作为 PNG 或者 PDF 下载到本地的需求。例如海报功能&#xff0c;简历导出功能等等。在我们自家的产品「代码小抄」中&#xff0c;就使用了 html2canvas 来实现代码片段导出为图片&#xff1a; 是不是还行&#xff…

【项目经验分享】深度学习自然语言处理技术毕业设计项目案例定制

以下毕业设计是与深度学习自然语言处理&#xff08;NLP&#xff09;相关的毕业设计项目案例&#xff0c;涵盖文本分类、生成式模型、语义理解、机器翻译、对话系统、情感分析等多个领域&#xff1a; 实现案例截图&#xff1a; 基于深度学习的文本分类系统基于BERT的情感分析系…

RabbitMQ 界面管理说明

1.RabbitMQ界面访问端口和后端代码连接端口不一样 界面端口是15672 http://localhost:15672/ 后端端口是 5672 默认账户密码登录 guest 2.总览图 3.RabbitMq数据存储位置 4.队列 4.客户端消费者连接状态 5.队列运行状态 6.整体运行状态

【SpringCloud】环境和工程搭建

环境和工程搭建 1. 案例介绍1.1 需求1.2 服务拆分服务拆分原则服务拆分⽰例 1. 案例介绍 1.1 需求 实现⼀个电商平台(不真实实现, 仅为演⽰) ⼀个电商平台包含的内容⾮常多, 以京东为例, 仅从⾸⻚上就可以看到巨多的功能 我们该如何实现呢? 如果把这些功能全部写在⼀个服务…

基于大数据技术的足球数据分析与可视化系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…

java计算机毕设课设—博网即时通讯软件(附源码、文章、相关截图、部署视频)

这是什么系统&#xff1f; 资源获取方式在最下方 java计算机毕设课设—博网即时通讯软件(附源码、文章、相关截图、部署视频) 博网即时通讯软件是一款功能丰富的实时通讯平台&#xff0c;旨在提升用户的交流效率与体验。在服务器端&#xff0c;该软件支持运行监控功能&#…

Java中的Junit、类加载时机与机制、反射、注解及枚举

目录 Java中的Junit、类加载时机与机制、反射、注解及枚举 Junit Junit介绍与使用 Junit注意事项 Junit其他注解 类加载时机与机制 类加载时机 类加载器介绍 获取类加载器对象 双亲委派机制和缓存机制 反射 获取类对象 获取类对象的构造方法 使用反射获取的构造方法创建对象 获…

无环SLAM系统集成后端回环检测模块(loop):SC-A-LOAM以及FAST_LIO_SLAM

最近在研究SLAM目标检测相关知识&#xff0c;看到一篇论文&#xff0c;集成了SC-A-LOAM作为后端回环检测模块&#xff0c;在学习了论文相关内容后决定看一下代码知识&#xff0c;随后将其移植&#xff0c;学习过程中发现我找的论文已经集成了回环检测模块&#xff0c;但是我的另…

Postgresql源码(136)syscache/relcache 缓存及失效机制

相关 《Postgresql源码&#xff08;45&#xff09;SysCache内存结构与搜索流程分析》 0 总结速查 syscache&#xff1a;缓存系统表的行。通用数据结构&#xff0c;可以缓存一切数据&#xff08;hash dlist&#xff09;。可以分别缓存单行和多行查询。 syscache使用CatCache数…

Hadoop框架及应用场景说明

Hadoop是一个开源的分布式系统基础架构。由多个组件组成&#xff0c;组件之间协同工作&#xff0c;进行大规模数据集的存储和处理。 本文将探讨Hadoop的架构以及应用场景。 一Hadoop框架 Hadoop的核心组件包含&#xff1a; 1. Hadoop分布式文件系统&#xff08;HDFS&#xff…

windows10使用bat脚本安装前后端环境之msyql5.7安装配置并重置用户密码

首先需要搞清楚msyql在本地是怎么安装配置、然后在根据如下步骤编写bat脚本&#xff1a; 思路 1.下载mysql5.7 zip格式安装包 2.新增data文件夹与my.ini配置文件 3.初始化数据库 4.安装mysql windows服务 5.启动并修改root密码&#xff08;新增用户初始化授予权限&#xff09…

浅拷贝深拷贝

&#x1f4cb;目录 &#x1f4da;引入&#x1f4da;浅拷贝&#x1f4d6;定义&#x1f4d6;实现方式&#x1f4d6;特点 &#x1f4da;深拷贝&#x1f4d6; 定义&#x1f4d6;实现方式&#x1f4d6;特点 &#x1f4da;拓展&#x1f4d6;Object类✈️toString()方法✈️equals()方…

预防工作场所的违规政策

违规政策是指未经管理层制定或批准的工作场所政策。 它们也可能直接违反公司政策。如果管理不善&#xff0c;这些政策可能会对您的业务产生负面影响。 最常见的流氓政策来源是 试图绕过现有政策框架的员工&#xff0c;或 经理们未经高层领导批准&#xff0c;擅自制定自己的…