20、C语言文件输入输出操作详解

C语言文件输入输出操作详解

1. 流刷新

在C语言中,流可能是全缓冲或部分缓冲的,这意味着你以为已经写入的数据可能还未真正传输到主机环境。当程序突然终止时,这可能会引发问题。 fflush 函数可以将指定流中未写入的数据传输到主机环境并写入文件,其原型如下:

int fflush(FILE *stream);

如果流上的最后一个操作是输入,那么该函数的行为是未定义的。如果 stream 是一个空指针, fflush 函数会对所有流执行刷新操作。在调用 fflush 之前,要确保文件指针不为空。

2. 设置文件位置

随机访问文件(如磁盘文件,但不包括终端)会维护一个与流相关联的文件位置指示器,该指示器描述了流当前在文件中的读写位置。打开文件时,除非以追加模式打开,否则指示器会位于文件开头。你可以将指示器定位到文件的任意位置进行读写操作。

ftell 函数用于获取文件位置指示器的当前值, fseek 函数用于设置文件位置指示器。这两个函数使用 long int 类型来表示文件中的偏移量(位置),因此只能处理可以用 long int 表示的偏移量。以下是使用 ftell fseek 函数的示例代码:

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

long int get_file_size(FILE *fp) {
  if (fseek(fp, 0, SEEK_END) != 0) {
    err(EXIT_FAILURE, "Seek to end-of-file failed");
  }
  long int fpi = ftell(fp);
  if (fpi == -1L) {
    err(EXIT_FAILURE, "ftell failed");
  }
  return fpi;
}

int main() {
  FILE *fp = fopen("fred.txt", "rb");
  if (fp  == nullptr) {
    err(EXIT_FAILURE, "Cannot open fred.txt file");
  }
  printf("file size: %ld\n", get_file_size(fp));
  if (fclose(fp) == EOF) {
    err(EXIT_FAILURE, "Failed to close file");
  }
  return EXIT_SUCCESS;
}

该程序打开 fred.txt 文件,调用 get_file_size 函数获取文件大小。 get_file_size 函数使用 fseek 将文件位置指示器设置到文件末尾,然后使用 ftell 获取当前位置的值。最后,程序关闭文件。

fseek 函数对文本文件和二进制文件有不同的约束。对于文本文件,偏移量必须为零或之前由 ftell 返回的值;而对于二进制文件,可以使用计算得到的偏移量。为确保代码健壮,要检查文件I/O操作的错误。 fopen 失败时返回空指针, fseek 无法满足请求时返回非零值, ftell 失败时返回 -1L 并在 errno 中存储实现定义的值, fclose 检测到错误时返回 EOF

fgetpos fsetpos 函数使用 fpos_t 类型来表示偏移量,该类型可以表示任意大的偏移量,因此可以用于处理任意大的文件。以下是使用 fgetpos fsetpos 函数的示例代码:

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

int main() {
  FILE *fp = fopen("fred.txt", "w+");
  if (fp == nullptr) {
    err(EXIT_FAILURE, "Cannot open fred.txt file");
  }
  fpos_t pos;
  if (fgetpos(fp, &pos) != 0) {
    err(EXIT_FAILURE, "get position");
  }
  if (fputs("abcdefghijklmnopqrstuvwxyz", fp) == EOF) {
      fputs("Cannot write to fred.txt file\n", stderr);
  }
  if (fsetpos(fp, &pos) != 0) {
    err(EXIT_FAILURE, "set position");
  }
  long int fpi = ftell(fp);
  if (fpi == -1L) {
    err(EXIT_FAILURE, "ftell");
  }
  printf("file position = %ld\n", fpi);
  if (fputs("0123456789", fp) == EOF) {
    fputs("Cannot write to fred.txt file\n", stderr);
  }
  if (fclose(fp) == EOF) {
    err(EXIT_FAILURE, "Failed to close file\n");
  }
  return EXIT_SUCCESS;
}

该程序打开 fred.txt 文件进行写入,使用 fgetpos 获取当前文件位置并存储在 pos 中,写入一些文本后,使用 fsetpos 将文件位置指示器恢复到之前存储的位置,最后使用 ftell 获取并打印文件位置。

在读写流时,若要在写入后读取或在读取后写入,需要调用 fflush 函数写入未写入的数据或调用文件定位函数(如 fseek fsetpos rewind )。 rewind 函数将文件位置指示器设置到文件开头,其等价于调用 fseek 并清除流的错误指示器:

void rewind(FILE *stream);
// 等价于
fseek(stream, 0L, SEEK_SET);
clearerr(stream);

由于无法确定 rewind 是否失败,建议使用 fseek 以便检查错误。另外,不要尝试在以追加模式打开的文件中使用文件位置,因为许多系统在追加时不会修改当前文件位置指示器,或者在写入时会强制将其重置到文件末尾。

3. 删除和重命名文件

C标准库提供了 remove 函数用于删除文件, rename 函数用于移动或重命名文件,其原型如下:

int remove(const char *filename);
int rename(const char *old, const char *new);

在POSIX中,文件删除函数是 unlink ,目录删除函数是 rmdir

int unlink(const char *path);
int rmdir(const char *path);

POSIX也使用 rename 进行重命名。C标准和POSIX的一个明显区别是,C标准没有目录的概念,而POSIX有。因此,C标准中没有定义处理目录的具体语义。

unlink 函数的语义比 remove 函数更明确,因为它特定于POSIX文件系统。在POSIX和Windows中,一个文件可以有多个链接,包括硬链接和打开的文件描述符。 unlink 函数总是会删除文件的目录项,但只有当没有更多链接或打开的文件描述符引用该文件时,才会删除文件。即使文件被删除,其内容可能仍会保留在永久存储中。 rmdir 函数只能删除空目录。

在POSIX中,当 remove 函数的参数不是目录时,其行为与 unlink 函数相同;当参数是目录时,其行为与 rmdir 函数相同。在其他操作系统上, remove 函数的行为可能不同。由于文件系统是与其他并发运行的程序共享的,文件条目可能会消失或被替换,这可能导致安全漏洞和意外的数据丢失。POSIX提供了一些函数,可以通过打开的文件描述符或句柄来删除和重命名文件,以防止这些问题。

4. 使用临时文件

临时文件常用于进程间通信或临时将信息存储到磁盘以释放随机访问内存(RAM)。例如,一个进程可以写入临时文件,另一个进程可以从中读取。这些文件通常使用C标准库的 tmpfile tmpnam 函数或POSIX的 mkstemp 函数在临时目录中创建。

临时目录可以是全局的或用户特定的。在Unix和Linux中, TMPDIR 环境变量用于指定全局临时目录的位置,通常是 /tmp /var/tmp 。运行Wayland或X11窗口系统的系统通常使用 $XDG_RUNTIME_DIR 环境变量定义用户特定的临时目录,通常设置为 /run/user/$uid 。在Windows中,用户特定的临时目录可以在用户配置文件的 AppData 部分找到,通常是 C:\Users\User Name\AppData\Local\Temp 。Windows的全局临时目录由 TMP TEMP 环境变量指定, C:\Windows\Temp 是Windows用于存储临时文件的系统文件夹。

出于安全考虑,每个用户最好有自己的临时目录,因为使用全局临时目录经常会导致安全漏洞。创建临时文件最安全的函数是POSIX的 mkstemp 函数。由于安全地访问共享目录中的文件可能很困难或不可能,建议不使用现有的函数,而是使用套接字、共享内存或其他专门为此目的设计的机制进行进程间通信。

5. 读取格式化文本流

fscanf 函数用于读取格式化输入,它是 fprintf 函数的输入对应版本,其原型如下:

int fscanf(FILE * restrict stream, const char * restrict format, ...);

fscanf 函数在格式字符串的控制下,从 stream 指向的流中读取输入,格式字符串告诉函数期望的参数数量、类型以及如何进行转换和赋值。后续参数是接收转换后输入的对象的指针。如果格式字符串的参数不足,结果是未定义的。如果提供的参数多于转换说明符,多余的参数会被计算但会被忽略。

以下是一个使用 fscanf 函数读取 signals.txt 文件并打印每行的示例程序:

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

#define TO_STR_HELPER(x) #x
#define TO_STR(x) TO_STR_HELPER(x)
#define DESC_MAX_LEN 99

int main() {
  int status = EXIT_SUCCESS;
  FILE *in;
  struct sigrecord {
    int signum;
    char signame[10];
    char sigdesc[DESC_MAX_LEN + 1];
  } rec;

  if ((in = fopen("signals.txt", "r")) == nullptr) {
    err(EXIT_FAILURE, "Cannot open signals.txt file");
  }

  while (true) {
    int n = fscanf(in, "%d%9s%*[ \t]%" TO_STR(DESC_MAX_LEN) "[^\n]",
      &rec.signum, rec.signame, rec.sigdesc
    );
    if (n == 3) {
      printf(
        "Signal\n  number = %d\n  name = %s\n  description = %s\n\n",
        rec.signum, rec.signame, rec.sigdesc
      );
    }
    else if (ferror(in)) {
      perror("Error indicated");
      status = EXIT_FAILURE;
      break;
    }
    else if (n == EOF) {
      // normal end-of-file
      break;
    }
    else if (feof(in)) {
      fputs("Premature end-of-file detected\n", stderr);
      status = EXIT_FAILURE;
      break;
    }
    else {
      fputs("Failed to match signum, signame, or sigdesc\n\n", stderr);
      int c;
      while ((c = getc(in)) != '\n' && c != EOF);
      status = EXIT_FAILURE;
    }
  }

  if (fclose(in) == EOF) {
    err(EXIT_FAILURE, "Failed to close file\n");
  }
  return status;
}

该程序定义了一个 sigrecord 结构体来存储文件中每行的信号信息。 fscanf 函数在一个无限循环中读取文件的每行输入,根据返回值进行不同的处理。如果返回值为3,表示成功读取三个输入项,打印信号描述;如果设置了错误指示器,打印错误信息并退出;如果返回 EOF ,表示已成功处理所有输入,退出循环;如果返回值不是期望的输入项数量且不是 EOF ,则将其视为非致命错误,打印错误信息并跳过该行。

fscanf 函数使用的格式字符串 "%d%9s%*[ \t]%99[^\n]" 包含四个转换说明符:
- %d :匹配第一个可选带符号的十进制整数,对应文件中的信号编号。
- %9s :匹配输入流中的下一个非空白字符序列,对应信号名称,长度修饰符防止输入超过九个字符,并在匹配字符后写入空字符。
- %*[ \t] :消耗信号ID和描述之间的任何空格或水平制表符,并使用赋值抑制说明符 * 抑制这些字符。
- %99[^\n] :匹配文件中的信号描述字段,读取所有字符直到遇到 \n EOF ,并将其存储在 rec.sigdesc 中,最大字符串长度为99字符,以避免缓冲区溢出。

以下是这个读取格式化文本流过程的流程图:

graph TD;
    A[开始] --> B[打开文件];
    B --> C{文件打开成功?};
    C -- 是 --> D[进入循环读取];
    C -- 否 --> E[输出错误信息并退出];
    D --> F[fscanf读取一行];
    F --> G{返回值n是否为3?};
    G -- 是 --> H[打印信号描述];
    G -- 否 --> I{是否设置错误指示器?};
    I -- 是 --> J[输出错误信息并退出循环];
    I -- 否 --> K{n是否为EOF?};
    K -- 是 --> L[退出循环];
    K -- 否 --> M{是否到达文件末尾?};
    M -- 是 --> N[输出错误信息并退出循环];
    M -- 否 --> O[输出匹配失败信息并跳过该行];
    H --> D;
    J --> P[关闭文件];
    L --> P;
    N --> P;
    O --> D;
    P --> Q[结束];
6. 读写二进制流

C标准库的 fread fwrite 函数可以操作文本流和二进制流。 fwrite 函数的原型如下:

size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb,
  FILE * restrict stream);

该函数将 ptr 指向的数组中最多 nmemb 个大小为 size 字节的元素写入 stream 流。 fwrite 函数的行为就像将每个对象转换为 unsigned char 数组,然后按顺序调用 fputc 函数写入数组中的每个字符。流的文件位置指示器会根据成功写入的字符数前进。

以下是使用 fwrite 函数将信号记录写入 signals.bin 文件的示例代码:

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

typedef struct sigrecord {
  int signum;
  char signame[10];
  char sigdesc[100];
} rec;

int main() {
  int status = EXIT_SUCCESS;
  FILE *fp;

  if ((fp = fopen("signals.bin", "wb")) == nullptr) {
    fputs("Cannot open signals.bin file\n", stderr);
    return EXIT_FAILURE;
  }

  rec sigrec30 = { 30, "USR1", "user-defined signal 1" };
  rec sigrec31 = {
    .signum = 31, .signame = "USR2", .sigdesc = "user-defined signal 2"
  };

  size_t size = sizeof(rec);

  if (fwrite(&sigrec30, size, 1, fp) != 1) {
    fputs("Cannot write sigrec30 to signals.bin file\n", stderr);
    status = EXIT_FAILURE;
    goto close_files;
  }

  if (fwrite(&sigrec31, size, 1, fp) != 1) {
    fputs("Cannot write sigrec31 to signals.bin file\n", stderr);
    status = EXIT_FAILURE;
  }

close_files:
  if (fclose(fp) == EOF) {
    fputs("Failed to close file\n", stderr);
    status = EXIT_FAILURE;
  }
  return status;
}

该程序打开 signals.bin 文件进行写入,声明并初始化两个 rec 结构体,然后使用 fwrite 函数将结构体写入文件。在写入过程中,检查 fwrite 函数的返回值,确保写入了正确数量的元素。

以下是使用 fread 函数从 signals.bin 文件中读取数据的示例代码:

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

typedef struct rec {
  int signum;
  char signame[10];
  char sigdesc[100];
} rec;

int main() {
  int status = EXIT_SUCCESS;
  FILE *fp;
  rec sigrec;

  if ((fp = fopen("signals.bin", "rb")) == nullptr) {
    fputs("Cannot open signals.bin file\n", stderr);
    return EXIT_FAILURE;
  }

  // read the second signal
  if (fseek(fp, sizeof(rec), SEEK_SET)  != 0) {
    fputs("fseek in signals.bin file failed\n", stderr);
    status = EXIT_FAILURE;
    goto close_files;
  }

  if (fread(&sigrec, sizeof(rec), 1, fp) != 1) {
    fputs("Cannot read from signals.bin file\n", stderr);
    status = EXIT_FAILURE;
    goto close_files;
  }

  printf(
    "Signal\n  number = %d\n  name = %s\n  description = %s\n\n",
    sigrec.signum, sigrec.signame, sigrec.sigdesc
  );

close_files:
  if (fclose(fp) == EOF) {
    fputs("Failed to close file\n", stderr);
    status = EXIT_FAILURE;
  }
  return status;
}

该程序打开 signals.bin 文件进行读取,使用 fseek 函数将文件位置指示器移动到第二个信号的位置,然后使用 fread 函数读取数据并打印。同样,在读取过程中,检查 fread 函数的返回值,确保读取了正确数量的元素。

以下是读写二进制流的操作步骤总结:
| 操作 | 步骤 |
| ---- | ---- |
| 写入二进制文件 | 1. 打开文件(以 wb 模式)。
2. 定义并初始化结构体。
3. 调用 fwrite 函数写入数据。
4. 检查 fwrite 返回值。
5. 关闭文件。 |
| 读取二进制文件 | 1. 打开文件(以 rb 模式)。
2. 使用 fseek 定位文件位置(可选)。
3. 调用 fread 函数读取数据。
4. 检查 fread 返回值。
5. 处理读取的数据。
6. 关闭文件。 |

综上所述,C语言提供了丰富的文件输入输出操作函数,在使用过程中要注意错误处理和不同函数的使用规则,以确保程序的健壮性和安全性。

C语言文件输入输出操作详解(续)

7. 关键函数总结

为了更清晰地理解和使用这些文件操作函数,下面对一些关键函数进行总结:
| 函数名 | 功能 | 原型 | 注意事项 |
| ---- | ---- | ---- | ---- |
| fflush | 将指定流中未写入的数据传输到主机环境并写入文件 | int fflush(FILE *stream); | 若流上最后操作是输入,行为未定义; stream 为空指针时对所有流操作 |
| ftell | 获取文件位置指示器的当前值 | long int ftell(FILE *stream); | 失败时返回 -1L 并在 errno 中存储实现定义的值 |
| fseek | 设置文件位置指示器 | int fseek(FILE *stream, long int offset, int whence); | 对文本和二进制文件约束不同,无法满足请求时返回非零值 |
| fgetpos | 获取文件位置信息 | int fgetpos(FILE *stream, fpos_t *pos); | 使用 fpos_t 类型可处理大文件 |
| fsetpos | 设置文件位置信息 | int fsetpos(FILE *stream, const fpos_t *pos); | 结合 fgetpos 可恢复文件位置和解析状态 |
| rewind | 将文件位置指示器设置到文件开头 | void rewind(FILE *stream); | 等价于 fseek clearerr ,无法判断是否失败,建议用 fseek |
| remove | 删除文件 | int remove(const char *filename); | 在POSIX中参数非目录时行为同 unlink ,是目录时同 rmdir |
| rename | 移动或重命名文件 | int rename(const char *old, const char *new); | - |
| fscanf | 读取格式化输入 | int fscanf(FILE * restrict stream, const char * restrict format, ...); | 注意格式字符串和参数匹配 |
| fwrite | 写入数据到流 | size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream); | 检查返回值确保写入正确数量元素 |
| fread | 从流中读取数据 | size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream); | 检查返回值确保读取正确数量元素 |

8. 不同场景下的使用建议
  • 临时文件使用场景
    • 当进行进程间通信或临时存储数据时,优先考虑每个用户有自己的临时目录。如果使用全局临时目录,尽量使用POSIX的 mkstemp 函数创建临时文件,以提高安全性。若安全访问共享目录困难,可采用套接字、共享内存等机制替代。
  • 文件定位操作场景
    • 在需要频繁定位文件位置时,对于大文件推荐使用 fgetpos fsetpos 函数,因为它们使用的 fpos_t 类型能处理任意大的偏移量。而对于一般文件, ftell fseek 函数足以满足需求,但要注意文本文件和二进制文件的不同约束。
  • 读写操作场景
    • 读写格式化文本流时,使用 fscanf 函数,要仔细设计格式字符串,避免缓冲区溢出和参数不匹配问题。读写二进制流时,使用 fread fwrite 函数,确保检查返回值,以保证数据的正确读写。
9. 错误处理的重要性

在文件输入输出操作中,错误处理至关重要。不同的函数以不同的方式报告错误,例如:
- fopen 失败时返回空指针,因此在使用返回的文件指针前,要检查是否为空。
- fseek 无法满足请求时返回非零值,可根据返回值判断操作是否成功。
- ftell 失败时返回 -1L ,可通过检查返回值和 errno 来处理错误。
- fclose 检测到错误时返回 EOF ,关闭文件后要检查返回值。

以下是一个通用的错误处理示例流程:

graph TD;
    A[执行文件操作] --> B{操作成功?};
    B -- 是 --> C[继续后续操作];
    B -- 否 --> D[检查错误类型];
    D --> E{是否为文件打开错误?};
    E -- 是 --> F[输出文件打开错误信息并退出程序];
    E -- 否 --> G{是否为文件定位错误?};
    G -- 是 --> H[输出文件定位错误信息并处理];
    G -- 否 --> I{是否为文件读写错误?};
    I -- 是 --> J[输出文件读写错误信息并处理];
    I -- 否 --> K[输出其他错误信息并处理];
    F --> L[结束];
    H --> C;
    J --> C;
    K --> C;
    C --> M[完成操作];
    M --> N[关闭文件];
    N --> O{文件关闭成功?};
    O -- 是 --> P[结束];
    O -- 否 --> Q[输出文件关闭错误信息并结束];
10. 总结与展望

C语言的文件输入输出操作提供了丰富的功能和灵活的控制,但也要求开发者仔细处理各种细节和错误。通过合理使用这些函数和遵循正确的操作步骤,可以编写出高效、健壮和安全的文件处理程序。

在未来的开发中,随着系统和应用的不断发展,对文件操作的性能和安全性要求会越来越高。开发者可以进一步研究和应用更高级的文件系统接口和技术,如异步I/O、文件锁机制等,以提升程序的性能和并发处理能力。同时,持续关注错误处理和安全问题,避免因文件操作不当导致的数据丢失和安全漏洞。

总之,掌握C语言的文件输入输出操作是成为一名优秀开发者的重要基础,希望本文能帮助读者更好地理解和运用这些知识。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值