Skip to content

C语言文件读写中的文件指针定位问题

问题描述

许多C语言开发者在进行文件操作时会遇到这样的困惑:成功创建文件并写入内容后,立即尝试读取文件内容,却发现无法读取到任何数据。以下是一个典型的错误示例代码:

c
#include <stdio.h>

#define SIZE 256
int main() {
  FILE *fptr = fopen("mem.txt","w+"); // 创建/覆盖可读写文件流
  char buffer[SIZE + 1]; // 额外存储空字符
  buffer[SIZE] = '\0';

  if(!fptr) { // 文件打开失败时fptr为NULL
    printf("Failed creation");
    return -1;
  }

  printf("Creation successful\n");
  fprintf(fptr, "Hello, World!"); // 写入文件
  
  fread(buffer, sizeof(buffer), 1, fptr); // 立即尝试读取
  printf("%s", buffer);

  fclose(fptr);
  return 0;
}

期望输出:

Creation successful
Hello, World!

实际输出:

Creation successful

问题原因

文件指针定位问题

关键问题

在C语言标准中,输出操作后不能直接进行输入操作,除非先调用文件定位函数或刷新缓冲区。

根据C标准(7.23.5.3p7):

输出后不能直接进行输入,除非调用fflush函数或文件定位函数(fseekfsetposrewind);输入后也不能直接进行输出,除非调用文件定位函数或输入操作遇到文件结束标志。

写入操作后,文件指针位于文件末尾,此时直接调用fread会从文件末尾开始读取,自然无法获取已写入的内容。

fread使用问题

c
// fread原型:
size_t fread(void * restrict ptr, size_t size, size_t nmemb,
    FILE * restrict stream);

// 错误用法:
fread(buffer, sizeof(buffer), 1, fptr);

这种用法存在问题:

  1. 请求读取1个sizeof(buffer)大小的元素,很可能返回0
  2. 如果读取的部分元素不完整,其表示形式是不确定的
  3. 读取的数据不保证以空字符结尾,不能直接作为字符串使用

解决方案

方案1:使用文件定位函数(推荐)

在写入和读取操作之间添加文件定位函数:

c
fprintf(fptr, "Hello, World!"); // 写入文件
rewind(fptr); // 将文件指针重置到文件开头
// 或者使用:fseek(fptr, 0, SEEK_SET);
fread(buffer, sizeof(buffer), 1, fptr); // 读取文件

方案2:正确的fread使用方法

c
size_t length = fread(buffer, 1, sizeof(buffer) - 1, fptr);
buffer[length] = '\0';
printf("%s", buffer);
c
size_t length = fread(buffer, 1, sizeof(buffer), fptr);
printf("%.*s", (int) length, buffer);

方案3:使用fgets替代fread

对于文本文件,使用fgets可以简化操作,因为它会自动添加空字符:

c
if (fgets(buffer, sizeof buffer, fptr)) {
  printf("%s", buffer);
}

方案4:使用fwrite进行输出

如果使用fread读取,可以使用fwrite输出,这样可以正确处理文件中的空字符:

c
size_t length = fread(buffer, 1, sizeof(buffer), fptr);
fwrite(buffer, 1, length, stdout);

方案5:使用POSIX系统调用(非便携)

在POSIX系统(Linux、macOS等)中,可以使用preadpwrite系统调用:

c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    const int fd = open("mem.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    
    const char text[] = "Hello, World!";
    pwrite(fd, text, sizeof(text) - 1, 0); // 写入文件
    
    char buffer[sizeof(text)];
    ssize_t readSize = pread(fd, buffer, sizeof(buffer) - 1, 0); // 读取文件
    
    buffer[readSize] = 0;
    puts(buffer);
    
    close(fd);
}

注意

系统调用没有进程级缓冲,每次文件访问都会导致系统调用,性能可能较低。

最佳实践总结

  1. 始终检查文件操作返回值:确保读写操作成功执行
  2. 正确处理文件指针定位:在读写操作切换时使用rewindfseekfflush
  3. 安全处理字符串:确保读取的数据正确以空字符结尾,或使用安全的输出方法
  4. 考虑使用更简单的函数:对于文本文件,fgetsfputs可能比freadfwrite更合适
  5. 错误处理:使用perror输出详细的错误信息,帮助调试

完整修正代码

c
#include <stdio.h>

#define SIZE 256
int main() {
  FILE *fptr = fopen("mem.txt","w+");
  char buffer[SIZE];
  
  if(!fptr) {
    printf("Failed creation");
    return -1;
  }

  printf("Creation successful\n");
  fprintf(fptr, "Hello, World!");
  
  rewind(fptr); // 重置文件指针到开头
  
  size_t length = fread(buffer, 1, sizeof(buffer) - 1, fptr);
  buffer[length] = '\0';
  printf("%s", buffer);

  fclose(fptr);
  return 0;
}

遵循这些准则,您可以避免常见的文件操作陷阱,编写出健壮可靠的C语言文件I/O代码。