C语言fscanf读文件到struct触发Access Violation错误
问题背景
编写基础C语言程序实现文本文件与结构体的信息读写功能:结构体全量数据写入文本文件的函数可正常运行,但将文件中同格式信息读取回空结构体时,持续触发Access Violation(访问冲突)错误,程序崩溃返回码为0xC0000005。
原始实现代码
#include <stdio.h> #include <stdlib.h> #define MAXENTRIES 100 struct entry{ unsigned int recordNum; char *toolName; unsigned int quantity; double cost; }; struct entry dataBase[MAXENTRIES]; int currentEntries = 0; void initDataBase(void){ dataBase[0].recordNum = 3; dataBase[0].toolName = "Electric Sander"; dataBase[0].quantity = 7; dataBase[0].cost = 57.98; dataBase[1].recordNum = 17; dataBase[1].toolName = "Hammer"; dataBase[1].quantity = 76; dataBase[1].cost = 11.99; dataBase[2].recordNum = 24; dataBase[2].toolName = "Jig Saw"; dataBase[2].quantity = 21; dataBase[2].cost = 11.00; dataBase[3].recordNum = 39; dataBase[3].toolName = "Lawn Mower"; dataBase[3].quantity = 3; dataBase[3].cost = 79.50; dataBase[4].recordNum = 56; dataBase[4].toolName = "Power Saw"; dataBase[4].quantity = 18; dataBase[4].cost = 99.99; dataBase[5].recordNum = 68; dataBase[5].toolName = "Screwdriver"; dataBase[5].quantity = 106; dataBase[5].cost = 6.99; dataBase[6].recordNum = 77; dataBase[6].toolName = "Sledge Hammer"; dataBase[6].quantity = 11; dataBase[6].cost = 21.50; dataBase[7].recordNum = 83; dataBase[7].toolName = "Wrench"; dataBase[7].quantity = 34; dataBase[7].cost = 7.50; currentEntries = 8; } void printEntry(int i){ printf("\nRecordNum : %d", dataBase[i].recordNum); printf("\nTool Name : %s", dataBase[i].toolName); printf("\nQuantity : %d", dataBase[i].quantity); printf("\nCost : %.2f\n\n", dataBase[i].cost); } void fprintDataBase(void){ FILE *dbPtr; if((dbPtr = fopen("hardware.dat", "w")) == NULL){ puts("File could not be opened"); } else{ for(int i = 0; i < currentEntries; i++){ fprintf(dbPtr, "%d %s %d %.2f\n", dataBase[i].recordNum, dataBase[i].toolName, dataBase[i].quantity, dataBase[i].cost); } printf("\nDatabase recorded!\n"); fclose(dbPtr); } } void fscanDataBase(void){ FILE *dbPtr; if((dbPtr = fopen("hardware.dat", "r")) == NULL){ puts("File could not be opened"); } else{ currentEntries = 1; fscanf(dbPtr, "%d", &dataBase[0].recordNum); //Access Violation Error HERE while(!feof(dbPtr)){ fscanf(dbPtr, "%d%29s%d%lf", &dataBase[currentEntries].recordNum, dataBase[currentEntries].toolName, &dataBase[currentEntries].quantity, &dataBase[currentEntries].cost); currentEntries++; } printf("\nDatabase scanned in!\n"); fclose(dbPtr); } } int main() { // initDataBase(); // fprintDataBase(); fscanDataBase(); printEntry(0); return 0; }
复现规律
- 调用
initDataBase()完成初始化后执行fprintDataBase(),可正常生成包含全部所需数据的hardware.dat文件 - 注释掉初始化、写文件的两行调用,单独执行
fscanDataBase()读取文件就触发访问冲突崩溃 - 崩溃点标注为
fscanDataBase()中读取第一条记录编号的fscanf行
已排除的故障可能
- 数组越界:
initDataBase()运行时可正常访问dataBase[0]元素 - 结构体作用域问题:
dataBase为全局变量,不存在作用域失效情况 - 基础语法问题:
initDataBase()中测试用scanf写入dataBase[0]字段可正常执行
故障根因
- 核心触发原因:野指针写入
结构体的toolName是char*类型指针,没有预先分配可写的有效内存:
- 执行
initDataBase()时,代码直接把toolName指针指向了只读字符串常量的地址,此时指针是有效的,所以写文件操作正常 - 跳过初始化直接读文件时,全局数组的指针成员默认初始值为
NULL(无效内存地址),后续fscanf尝试把读取到的工具名写入NULL指向的内存段,直接触发内存访问违规。哪怕代码里先读recordNum不触发,执行到读toolName的逻辑时一样会崩溃。
- 附带逻辑缺陷
%s格式符读取字符串时会在第一个空白字符处截断,Electric Sander这类带空格的工具名无法被完整读取,还会打乱后续字段的读取偏移while(!feof(dbPtr))的循环写法存在逻辑bug,会在文件读取结束后多执行一次读操作,可能产生冗余无效记录甚至触发数组越界- 文件打开失败的分支没有加return,会继续执行后续文件操作逻辑
修复方案
- 新增依赖头文件,修改结构体定义,将字符指针替换为固定长度字符数组,从根源解决指针内存无效问题:
#include <string.h> #include <ctype.h> #define MAXENTRIES 100 #define NAMELEN 30 // 定义工具名最大长度 struct entry{ unsigned int recordNum; char toolName[NAMELEN]; // 替换原char*,数组自带预分配的可写存储空间 unsigned int quantity; double cost; };
- 重写文件读取逻辑,修正格式匹配、循环判断的bug:
void fscanDataBase(void){ FILE *dbPtr; if((dbPtr = fopen("hardware.dat", "r")) == NULL){ puts("File could not be opened"); return; // 打开失败直接退出,不执行后续逻辑 } currentEntries = 0; // 以fscanf返回值作为循环判断:成功读满4个字段才处理,避免feof的多读问题 while(fscanf(dbPtr, "%u %29[^0-9] %u %lf", &dataBase[currentEntries].recordNum, dataBase[currentEntries].toolName, &dataBase[currentEntries].quantity, &dataBase[currentEntries].cost) == 4){ // 清除工具名末尾因为格式匹配产生的多余空格 char *tail = dataBase[currentEntries].toolName + strlen(dataBase[currentEntries].toolName) - 1; while(tail >= dataBase[currentEntries].toolName && isspace(*tail)){ *tail = '\0'; tail--; } currentEntries++; if(currentEntries >= MAXENTRIES) break; // 防止超过数组最大容量 } printf("\nDatabase scanned in! total %d entries\n", currentEntries); fclose(dbPtr); }
- 同步调整初始化函数的赋值逻辑,因为
toolName改成了数组,不能直接给指针赋值,要用strcpy拷贝字符串,其余条目按相同规则替换即可:
void initDataBase(void){ dataBase[0].recordNum = 3; strcpy(dataBase[0].toolName, "Electric Sander"); dataBase[0].quantity = 7; dataBase[0].cost = 57.98; dataBase[1].recordNum = 17; strcpy(dataBase[1].toolName, "Hammer"); dataBase[1].quantity = 76; dataBase[1].cost = 11.99; // 剩余初始化条目省略 currentEntries = 8; }
内容的提问来源于stack exchange,提问作者Owen Gibson
相关产品推荐
相关产品推荐

