C语言循环内多条fscanf读文件如何正确判断EOF避免段错误
问题根因
你遇到的段错误、EOF判断失效是几个典型C文件操作错误叠加导致的,那个返回值3221226356对应Windows平台的STATUS_STACK_BUFFER_OVERRUN错误(0xC0000409),本质是内存越界写入:
fscanf返回值校验不全:你只在两个读取节点判断EOF,但fscanf字段匹配失败时返回0而非EOF,此时文件指针位置无定义,后续ftell/fseek的回退逻辑完全失效,指针逐记录错位后会把读入的野值写入非分配内存区域,前3条记录因为错位幅度小刚好没触发内存保护,到第4条访问非法地址直接崩溃。feof使用逻辑错误:feof是读操作触发EOF后的状态标记位,不能用来预判文件尾,放在读操作前判断永远不会在正确的时机触发终止。- 可选字段读取逻辑脆弱:靠固定偏移量回退指针的逻辑完全没考虑换行符、多余空白符的影响,
fscanf默认跳过空白符的特性会让你记录的指针位置和实际读入的字节数不匹配,回退量算错后越读错位越严重。 - 动态内存无校验:
realloc/calloc分配失败返回NULL时没有做拦截,直接写入野指针也会触发段错误。 - 字符串读取逻辑有漏洞:直接用
fscanf读枢纽名称遇到带空格的名称会自动截断,后续字段会全部错位。
正确实现方案
核心实现原则:
- 所有
fscanf/fgets调用都必须检查返回值,只有返回值和预期读取的字段/字节数匹配时,才认定读取成功;仅当读操作返回EOF时触发循环终止,禁止提前用feof做预判。 - 放弃靠硬算偏移量
fseek回退的脆弱逻辑,读取可选'E'字段时先记录当前文件指针位置,整行读取内容后判断是不是'E',如果不是再回退到记录的位置,避免指针错位。 - 包含空格、可变长度的字段(比如枢纽名称)一律用
fgets整行读取,处理完首尾空白后再存储,避免被空白符截断。 - 所有动态内存操作用临时指针接收返回值,校验非NULL后再赋值给原指针,避免分配失败导致内存泄漏、野指针写入。
可直接运行的参考代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #define MAX_HUB_NAME_LEN 128 #define INIT_RECORD_CAP 8 #define LINE_BUF_SIZE 256 // 卡车信息结构体 typedef struct { int year; int month; int day; int pkg_count; char hub_name[MAX_HUB_NAME_LEN]; int is_oversize; // 1为超限货物,0为普通货物 } TruckRecord; // 去除字符串首尾空白字符的工具函数 char* str_trim(char* s) { while (isspace((unsigned char)*s)) s++; if (*s == 0) return s; char* end = s + strlen(s) - 1; while (end > s && isspace((unsigned char)*end)) end--; end[1] = '\0'; return s; } int main() { FILE* fp = fopen("info.txt", "r"); if (!fp) { perror("文件打开失败"); return EXIT_FAILURE; } TruckRecord* records = calloc(INIT_RECORD_CAP, sizeof(TruckRecord)); if (!records) { perror("初始内存分配失败"); fclose(fp); return EXIT_FAILURE; } int record_cnt = 0; int record_cap = INIT_RECORD_CAP; char line_buf[LINE_BUF_SIZE]; while (1) { // 动态扩容 if (record_cnt >= record_cap) { record_cap *= 2; TruckRecord* tmp = realloc(records, record_cap * sizeof(TruckRecord)); if (!tmp) { perror("内存扩容失败"); free(records); fclose(fp); return EXIT_FAILURE; } records = tmp; } TruckRecord* cur = &records[record_cnt]; // 读取日期行 if (fscanf(fp, "%d - %d - %d", &cur->year, &cur->month, &cur->day) != 3) { if (feof(fp)) break; // 格式错误跳过当前坏行 fgets(line_buf, LINE_BUF_SIZE, fp); continue; } // 吃掉日期行剩余的换行符 fgets(line_buf, LINE_BUF_SIZE, fp); // 读取包裹数量 if (fscanf(fp, "%d", &cur->pkg_count) != 1) { if (feof(fp)) break; fgets(line_buf, LINE_BUF_SIZE, fp); continue; } fgets(line_buf, LINE_BUF_SIZE, fp); // 读取枢纽名称(整行读支持带空格的名称) if (!fgets(line_buf, LINE_BUF_SIZE, fp)) break; strcpy(cur->hub_name, str_trim(line_buf)); // 判断是否有超限标记'E' cur->is_oversize = 0; long last_pos = ftell(fp); if (fgets(line_buf, LINE_BUF_SIZE, fp)) { str_trim(line_buf); if (strcmp(line_buf, "E") == 0) { cur->is_oversize = 1; } else { // 不是超限标记,回退指针到读行前的位置,供下一轮读取日期 fseek(fp, last_pos, SEEK_SET); } } else { // 读到文件尾直接终止 break; } record_cnt++; } // 测试输出读取结果 for (int i = 0; i < record_cnt; i++) { printf("记录%d: %d-%d-%d | 包裹数:%d | 出发枢纽:%s | 超限:%s\n", i+1, records[i].year, records[i].month, records[i].day, records[i].pkg_count, records[i].hub_name, records[i].is_oversize ? "是" : "否"); } free(records); fclose(fp); return EXIT_SUCCESS; }
验证说明
测试用info.txt内容:
2024 - 5 - 12 128 上海青浦枢纽 E 2024 - 5 - 13 96 北京通州枢纽 2024 - 5 - 14 210 广州白云枢纽 E 2024 - 5 - 15 74 成都双流枢纽
运行预期输出:
记录1: 2024-5-12 | 包裹数:128 | 出发枢纽:上海青浦枢纽 | 超限:是 记录2: 2024-5-13 | 包裹数:96 | 出发枢纽:北京通州枢纽 | 超限:否 记录3: 2024-5-14 | 包裹数:210 | 出发枢纽:广州白云枢纽 | 超限:是 记录4: 2024-5-15 | 包裹数:74 | 出发枢纽:成都双流枢纽 | 超限:否
该实现不会出现指针错位、内存越界问题,支持任意条数的记录读取,对带空格的枢纽名称、行尾多余空白符都有兼容能力。
内容的提问来源于stack exchange,提问作者Luca
相关产品推荐
相关产品推荐

