C语言读取CSV到结构体数组仅保留最后一行数据的问题
你遇到的核心问题是所有Student结构体的nume和prenume都指向了同一个内存地址——也就是main里定义的myLine数组地址。因为strtok返回的是传入字符串内部的指针,而myLine是固定大小的数组,每次循环fgets都会覆盖它的内容。循环内部打印时,myLine里还是当前行的数据,所以看起来正常;但循环结束后,myLine里只剩最后一行的内容,所有结构体的字符串指针都指向这里,自然就只显示最后一行的数据了。
除此之外,你的代码还有几个其他问题,我一起帮你梳理并修正:
1. 字符串指针未分配独立内存
Student结构体里的nume和prenume是char*类型,但你直接把strtok返回的指针赋值给它们,没有为每个字符串分配独立的内存空间。当myLine被覆盖后,这些指针指向的内容就变成了最新的行数据。
解决方法:用strdup(或者malloc+strcpy)为每个字符串复制一份独立的内存,比如:
myStudents[index].nume = strdup(myWord);
注意:strdup需要包含string.h,而且使用后要记得在程序结束时释放内存,避免内存泄漏。
2. fileNrLines函数的逻辑错误
你的fileNrLines一开始就执行了lines++,这会导致统计的行数比实际多1(比如空文件会返回1)。正确的统计方式应该是先初始化lines=0,然后在循环里遇到'\n'时才递增,最后还要考虑文件最后一行没有换行符的情况。
修正后的fileNrLines:
int fileNrLines() { FILE *fp = fopen("students.csv", "r"); if (!fp) { // 要检查文件是否打开成功 perror("Failed to open file"); return 0; } char ch; int lines = 0; int prev_char = '\n'; // 处理最后一行没有换行符的情况 while ((ch = fgetc(fp)) != EOF) { if (ch == '\n') { lines++; } prev_char = ch; } // 如果最后一行没有换行符,额外加1 if (prev_char != '\n' && lines > 0) { lines++; } else if (prev_char != '\n') { // 文件只有一行且无换行 lines = 1; } fclose(fp); return lines; }
3. printStudents的参数错误
你调用printStudents(myStudents, &lines);,但函数定义是void printStudents(Student *myStudents, int size),第二个参数应该是int类型,不是指针。正确调用是printStudents(myStudents, lines);。
4. strtok的嵌套循环逻辑冗余
每行CSV固定有4个字段,不需要嵌套while (myWord!=NULL)循环——这个循环会重复处理字段,导致同一行的字段被多次赋值(甚至可能越界)。直接按顺序取4个strtok的结果即可。
5. 数值转换错误
你用atoll转换浮点数,atoll是用来转长整型的,应该用atof来转换浮点数。
修正后的完整代码
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct Student { char* nume; char* prenume; int cod; float nota; } Student; int fileNrLines() { FILE *fp = fopen("students.csv", "r"); if (!fp) { perror("Failed to open file"); return 0; } char ch; int lines = 0; int prev_char = '\n'; while ((ch = fgetc(fp)) != EOF) { if (ch == '\n') { lines++; } prev_char = ch; } if (prev_char != '\n' && lines > 0) { lines++; } else if (prev_char != '\n') { lines = 1; } fclose(fp); return lines; } void printStudents(Student *myStudents, int size) { printf("| %20s | %20s | %3s | %5s |\n", "Nume", "Prenume", "Cod", "Nota"); printf("-------------------------------------------------------------------------\n"); for (int i = 0; i < size; ++i) { printf("| %20s | %20s | %3d | %5.2f |\n", myStudents[i].nume, myStudents[i].prenume, myStudents[i].cod, myStudents[i].nota); } } // 释放Student数组的内存 void freeStudents(Student *myStudents, int size) { for (int i = 0; i < size; ++i) { free(myStudents[i].nume); free(myStudents[i].prenume); } free(myStudents); } int main() { // main的返回类型应该是int,不是void int lines = fileNrLines(); if (lines == 0) { printf("No data in file or file error.\n"); return 1; } Student *myStudents = malloc(lines * sizeof(Student)); if (!myStudents) { perror("Failed to allocate memory"); return 1; } FILE *fp = fopen("students.csv", "r"); if (!fp) { perror("Failed to open file"); freeStudents(myStudents, lines); return 1; } char myLine[50]; int index = 0; while (fgets(myLine, sizeof(myLine), fp) != NULL && index < lines) { // 去掉换行符(如果存在) size_t len = strlen(myLine); if (len > 0 && myLine[len-1] == '\n') { myLine[len-1] = '\0'; } char* myWord = strtok(myLine, ","); if (myWord == NULL) break; myStudents[index].nume = strdup(myWord); myWord = strtok(NULL, ","); if (myWord == NULL) break; myStudents[index].prenume = strdup(myWord); myWord = strtok(NULL, ","); if (myWord == NULL) break; myStudents[index].cod = atoi(myWord); myWord = strtok(NULL, ","); if (myWord == NULL) break; myStudents[index].nota = atof(myWord); index++; } fclose(fp); printStudents(myStudents, index); freeStudents(myStudents, index); return 0; }
关键修正点总结
- 为每个字符串字段分配独立内存,避免共用
myLine的地址 - 修复行数统计的逻辑错误,处理无换行符的最后一行
- 修正
printStudents的参数传递错误 - 简化
strtok的使用逻辑,避免冗余循环 - 用
atof替代atoll进行浮点数转换 - 添加内存释放逻辑,避免内存泄漏
- 规范
main函数的返回类型,添加文件和内存分配的错误检查
内容的提问来源于stack exchange,提问作者Mareș Ștefan

