I've created an app to manage students' data which also supports file handling. The Student structure looks like this:
typedef struct Student { //dynamically allocated structure
char *first_name;
char *last_name;
float grade;
}Student;
Considering C is a low-level programming language, I wanted to take full advantage on its dynamic memory allocation features. That is, both the first name and last name of each student will be allocated dynamically. The main data structure I've used to store the students is a vector. By now, the app has the following features:
The file handling part is that interests me the most. I tried to carefully read the C documentation and make my functions less error-prone in terms of memory management and I/O.
The following functions are designed to handle CSV files:
void Read_From_CSV(const char *file_name, Student *p_destination)
{
if (!strstr(file_name, ".csv"))
{
printf("This method only works for CSV files. \n");
return;
}
FILE *p_file = fopen(file_name, "r");
if (!p_file)
{
perror("File not found or corrupted file. \n");
exit(EXIT_FAILURE);
}
unsigned char buffer_lname[30], buffer_fname[30], buffer[256];
unsigned int count_students = 0, successfully_read = 0;
while (fscanf(p_file,"%s",buffer)==1)
{
successfully_read = sscanf(buffer, "%[^,],%[^,],%f", buffer_lname, buffer_fname, &p_destination[count_students].grade);
if (successfully_read != 3)
{
printf("Error reading from CSV.\n");
exit(EXIT_FAILURE);
}
p_destination[count_students].last_name = (char*)malloc(strlen(buffer_lname) + 1);
if (!p_destination[count_students].last_name)
{
printf("Couldn't allocate memory. \n");
exit(EXIT_FAILURE);
}
strcpy(p_destination[count_students].last_name, buffer_lname);
p_destination[count_students].first_name = (char*)malloc(strlen(buffer_fname) + 1);
if (!p_destination[count_students].first_name)
{
printf("Couldn't allocate memory. \n");
exit(EXIT_FAILURE);
}
strcpy(p_destination[count_students].first_name, buffer_fname);
++count_students;
}
fclose(p_file);
}
void Write_To_CSV(const char *file_name, Student *p_source, const unsigned int number_of_students)
{
if (!strstr(file_name, ".csv"))
{
printf("This method only works for CSV files. \n");
return;
}
FILE *p_file = fopen(file_name, "w");
if (!p_file)
{
perror("An error has occured. \n");
exit(EXIT_FAILURE);
}
unsigned int index = 0;
int successfully_written = 0;
while (index < number_of_students)
{
successfully_written = fprintf(p_file, "%s,", p_source[index].last_name);
if (successfully_written != strlen(p_source[index].last_name) + 1)
{
printf("An error has occured. \n");
exit(EXIT_FAILURE);
}
successfully_written = fprintf(p_file, "%s,", p_source[index].first_name);
if (successfully_written != strlen(p_source[index].first_name) + 1)
{
printf("An error has occured. \n");
exit(EXIT_FAILURE);
}
successfully_written = fprintf(p_file, "%.2f", p_source[index].grade);
if (successfully_written != 4 && successfully_written != 5)
{
printf("Error occured during grade writing. \n");
exit(EXIT_FAILURE);
}
fprintf(p_file, "\n");
++index;
}
fclose(p_file);
}
And these two are designed for binary files:
void Read_From_Binary(const char *file_name, Student *p_destination, const unsigned int number_of_students)
{
if (!strstr(file_name, ".bin"))
{
printf("This method only works for binary files. \n");
return;
}
FILE *p_file = fopen(file_name, "rb");
if (!p_file)
{
perror("Error opening file. \n");
exit(EXIT_FAILURE);
}
size_t successfully_read;
unsigned int width = 0;
for (unsigned int index = 0; index < number_of_students; ++index)
{
successfully_read = fread(&width, sizeof(width), 1, p_file);
if (successfully_read != 1)
{
printf("Error reading from the file. \n");
exit(EXIT_FAILURE);
}
p_destination[index].last_name = (char*)malloc(width);
if (!p_destination[index].last_name)
{
printf("Could not allocate memory. \n");
exit(EXIT_FAILURE);
}
successfully_read = fread(p_destination[index].last_name, width, 1, p_file);
if (successfully_read != 1)
{
printf("Error reading from the file. \n");
exit(EXIT_FAILURE);
}
successfully_read = fread(&width, sizeof(width), 1, p_file);
if (successfully_read != 1)
{
printf("Error reading from the file. \n");
exit(EXIT_FAILURE);
}
p_destination[index].first_name = (char*)malloc(width);
if (!p_destination[index].first_name)
{
printf("Could not allocate memory. \n");
exit(EXIT_FAILURE);
}
successfully_read = fread(p_destination[index].first_name, width, 1, p_file);
if (successfully_read != 1)
{
printf("Error reading from the file. \n");
exit(EXIT_FAILURE);
}
successfully_read = fread(&p_destination[index].grade, sizeof(float), 1, p_file);
if (successfully_read != 1)
{
printf("Error reading from the file. \n");
exit(EXIT_FAILURE);
}
}
fclose(p_file);
}
void Write_To_Binary(const char *file_name, Student *p_source, const unsigned int number_of_students)
{
if (!strstr(file_name, ".bin"))
{
printf("This method only works for binary files. \n");
return;
}
FILE *p_file = fopen(file_name, "wb");
if (!p_file)
{
perror("Error opening the file. \n");
exit(EXIT_FAILURE);
}
size_t successfully_written = 0;
unsigned int width = 0;
for (unsigned int index = 0; index < number_of_students; ++index)
{
width = strlen(p_source[index].last_name) + 1;
successfully_written = fwrite(&width, sizeof(width), 1, p_file);
if (successfully_written != 1)
{
printf("Error writing to the file. \n");
exit(EXIT_FAILURE);
}
successfully_written = fwrite(p_source[index].last_name, width, 1, p_file);
if (successfully_written != 1)
{
printf("Error writing to the file. \n");
exit(EXIT_FAILURE);
}
width = strlen(p_source[index].first_name) + 1;
successfully_written = fwrite(&width, sizeof(width), 1, p_file);
if (successfully_written != 1)
{
printf("Error writing to the file. \n");
exit(EXIT_FAILURE);
}
successfully_written = fwrite(p_source[index].first_name, width, 1, p_file);
if (successfully_written != 1)
{
printf("Error writing to the file. \n");
exit(EXIT_FAILURE);
}
successfully_written = fwrite(&p_source[index].grade, sizeof(float), 1, p_file);
if (successfully_written != 1)
{
printf("Error writing to the file. \n");
exit(EXIT_FAILURE);
}
}
fclose(p_file);
}
UPDATE
As I've been suggested, I'll add a driver code that exemplifies the usage of the earlier defined functions.
Suppose that we have a Students.csv file which contains the following registrations:
Wayne,Bruce,8.50
Kent,Clark,6.60
Dent,Harvey,5.50
Stark,Daniel,7
The driver code will import data from the CSV file. Then it will print the data on the console, write another CSV file and a binary file. The students array is cleared and then we import data from the binary file that was earlier created. Please consider that the structure and the functions for file handling are already defined.
#include <stdio.h>
#include <stdlib.h>
void Print_Students(const Student *p_first, const Student *p_last)
{
unsigned int index = 0;
for (; p_first < p_last; ++p_first)
{
printf("Student %d\n", index);
printf("Last name: %s \n", p_first->last_name);
printf("First name: %s \n", p_first->first_name);
printf("Grade: %.2f \n", p_first->grade);
++index;
}
printf("\n");
}
void main()
{
Student *students = (Student*)malloc(4 * sizeof(Student));
if (!students)
{
printf("Couldn't allocate memory.\n");
exit(EXIT_FAILURE);
}
Read_From_CSV("Students.csv", students);
printf("Reading data from CSV. \n");
Print_Students(students, students + 4);
Write_To_CSV("Students1.csv", students, 4);
Write_To_Binary("Students1.bin", students, 4);
free(students);
students = (Student*)malloc(4 * sizeof(Student));
if (!students)
{
printf("Couldn't allocate memory.\n");
exit(EXIT_FAILURE);
}
Read_From_Binary("Students1.bin", students, 4);
printf("Reading data from binary file. \n");
Print_Students(students, students + 4);
free(students);
}
My main questions are the following:
- Are the variables named properly?
- When working with files, did I correctly check the return values from functions like fscanf, fwrite, fread etc.?
- Is there a better way to extract data from a CSV file?
- Is the memory allocation part done properly?
Should I update my original post and add the driver code?
I think that the way to go. \$\endgroup\$