Problem Details:
I'm developing a C contact management system where each contact is stored as:
struct Contact {
char name[31];
char number[11];
char email[30];
};
Current Behavior: When saving contacts with emails, the file looks totally correct :
John Doe
0555555555
john@test.com
Alfred
0666666666
alfred@gmail.com
After quitting the program , loading and re-saving the contacts into the same file (without modifying), the file becomes corrupted (NULL is just a used when the user chooses not to fill an email so I don't have to show it when he wants to see his contact) :
John Doe
0555555555
NULL
john@test.com
Alfred
0666666666
What SHOULD be displayed :
John Doe
0555555555
john@test.com
Alfred
0666666666
alfred@gmail.com
So what I see that is happening is :
1)The first contact's email gets replaced with "NULL"
2)The original email ("john@test.com") shifts into the next contact's data position
3)Subsequent contacts get misaligned in a cascading effect .
This is what my saving to ( and Loading from )file function looks like :
void LoadContactsFromFile() {
FILE* file = fopen("contacts.txt", "r");
if (file == NULL) {
printf("No saved contacts found. Starting fresh.\n");
return;
}
char name[31], email[30], number[11];
while (fgets(name, sizeof(name), file) &&
fgets(number, sizeof(number), file) &&
fgets(email, sizeof(email), file)) {
int c = strcspn(email, "\n");
name[strcspn(name, "\n")] = '\0';
number[strcspn(number, "\n")] = '\0';
email[strcspn(email, "\n")] = '\0';
printf("c = %d\n" , c);
printf("Read: name='%s' | number='%s' | email='%s'\n", name, number, email); //TESTING IF THE PROBLEM IS IN HERE
struct Contact* temp = realloc(List, (SizeList + 1) * sizeof(struct Contact));
if (!temp) {
fprintf(stderr, "ERROR: Failed to allocate memory while loading contacts\n");
fclose(file);
return;
}
List = temp;
strcpy(List[SizeList].name, name);
strcpy(List[SizeList].number, number);
if (strcmp(email, "NULL") == 0){
List[SizeList].email[0] = '\0'; // store as empty string
}else{
strcpy(List[SizeList].email, email);
}
SizeList++;
}
fclose(file);
printf("Successfully loaded %d contact(s) from file.\n", SizeList);
}
void SaveContactsToFile(){
FILE* file = fopen("contacts.txt" , "w");
if(!file){fprintf(stderr , "ERROR : Couldn't create file");}
for(int i = 0 ; i<SizeList ; i++){
const char* emailtosave = (List[i].email[0] == '\0') ? "NULL" : List[i].email;
fprintf(file, "%s\n%s\n%s\n", List[i].name, List[i].number, emailtosave);
}
fclose(file);
}
Question: Why does my contact data shift when loading and resaving, and how can I maintain consistent file structure when handling optional email fields?
Why does my contact data shift when loading and resaving, and how can I maintain consistent file structure when handling optional email fields?
Your use of fgets()
is flawed.
fgets()
transfers bytes from the file to the specified array until it has transferred a newline, or it has filled the buffer one byte short of the specified capacity, or it reaches the end of the file. Then it adds a string terminator. Whatever point it reaches in the file is where the next read will start, barring a repositioning in between. In particular, if fgets()
terminates on account of filling the destination, without transferring a newline, then the next read will continue on the same line.
You are writing a phone number that, together with its terminator, completely fills the Contact.number
array -- 10 bytes of data plus 1 terminator. The corresponding line in the file contains 10 bytes of data plus 1 newline. On readback, fgets()
will exhaust the destination buffer (less one byte reserved for a terminator) after transferring the digits only, leaving the newline still to be read. Not taking that into account, the program goes straight to reading the email with another fgets()
, which transfers the newline. The program replaces the newline with a string terminator, then converts the resulting empty string to the string "NULL".
The absolute simplest fix for the current program would be to make the intermediate arrays in LoadContactsFromFile()
one byte longer each, so that they can accommodate the expected maximum number of characters, plus a string terminator, plus a newline. But that's short-sighted, as it would be subject to much the same problem for malformed inputs, for which a robust program must be prepared.
The right way to perform line-oriented input with fgets()
is to determine for each call whether the end of the line was reached, as judged by a newline having been read or the end of the file having been reached. If not, then the program must handle the situation in an appropriate manner, such as by discarding the rest of the line, ingesting the rest of the line (via one or more additional calls), or rejecting the input as erroneous. In your program, you can determine whether a newline was read by examining the return values of the strcspn()
calls. number[strcspn(number, "\n")] = '\0'
and the like is just a bit too clever when you care whether there was a newline or not.