Binary Input and Output in C
To work with binary files, the C standard library provides two functions: fwrite and fread.
These two functions allow a program to read and write blocks of binary data efficiently in a single pass. They are particularly useful when working with complex data structures or large amounts of data, as they allow avoiding the need to convert data to text format.
- The
fwrite()andfread()functions are used to write and read blocks of binary data in C language. - These functions are particularly useful for working with complex data structures or large amounts of data.
fwrite()writes a block of data from memory to an output stream, whilefread()reads a block of data from an input stream to memory.- It is important to understand the difference between the
sizeandcountparameters in both functions to correctly manage the number of bytes read or written. - These functions can also be used to save and read user-defined data structures, but care must be taken with pointer-type fields within structures.
Binary Output Function: fwrite
The fwrite() function is designed to copy an array from memory to an output stream, such as a file opened in binary mode.
Its signature is as follows:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
Its first argument, ptr, is a pointer to the memory block or array you want to write. The second argument, size, specifies the size in bytes of each element to be written, while the third argument, count, indicates the number of elements to write. Finally, the fourth argument, stream, is a pointer to the file opened in binary mode where the data will be written.
Suppose, for example, we have a static array of integers that we want to write to a binary file. Here's how we could do it:
int array[] = {1, 2, 3, 4, 5};
size_t element_size = sizeof(int);
size_t num_elements = sizeof(array) / sizeof(array[0]);
fwrite(array, element_size, num_elements, binary_file);
In this example, fwrite() writes the entire array of integers to the binary file specified by binary_file. The function returns the number of elements actually written, which should equal num_elements if the operation was successful. Note that the number of elements returned is not the number of bytes written, but the number of elements specified in the third argument of the function.
Obviously, we are not obliged to write the entire array. We can choose to write only part of it by specifying a lower value for count. For example, to write only the first three elements of the array, we could do this:
fwrite(array, element_size, 3, binary_file);
In case of an error during the write operation, fwrite() returns a value less than count, indicating the number of elements actually written before the error.
Binary Input Function: fread
The fread() function is designed to read a block of binary data from an input stream, such as a file opened in binary mode, and copy it to a memory area. Its parameters are similar to those of fwrite(). Its signature is as follows:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
The first argument, ptr, is a pointer to the memory area where the read data will be stored. The second argument, size, specifies the size in bytes of each element to be read, while the third argument, count, indicates the number of elements to read. Finally, the fourth argument, stream, is a pointer to the file opened in binary mode from which the data will be read.
Suppose we want to perform the inverse operation compared to the previous example, reading data from a binary file into an array of integers. Here's how we could do it:
int array[5];
size_t element_size = sizeof(int);
size_t num_elements_read = fread(array, element_size, 5, binary_file);
In this example, fread() reads up to 5 integers from the binary file specified by binary_file and stores them in the array array. The function returns the number of elements actually read, which could be less than 5 if the end of the file is reached before reading all the requested elements.
In this last case, it is important to check the value returned by fread() to ensure that the read operation was successful and that the number of elements read is as expected. Otherwise, we can use the feof() function to check if the end of the file has been reached, or ferror() to check if a read error has occurred.
Observations on the use of fwrite and fread
A first important observation concerns the two parameters size and count. These two parameters should not be confused, especially when you want to read from a file.
In fact, let's consider for a moment the following call to fread():
fread(array, 1, 100, file);
In this case, we are asking fread to read 100 elements of size 1 byte each, that is, to read 100 bytes from the file. For this reason, in case of success, the function will return the number 100, which represents the number of elements read (each of 1 byte). In any case, it will return a number between 0 and 100, depending on how many elements were actually read.
If, instead, we reverse the two parameters, as in the following example:
fread(array, 100, 1, file);
We are asking fread to read 1 element of size 100 bytes. Therefore, in case of success, the function will return the number 1, which represents the number of elements read.
In both cases the function reads 100 bytes from the file, but the check we must perform on the value returned by the function changes depending on how we set the size and count parameters.
Saving and Reading Data Structures
Above, we stated that fwrite and fread are used to read and write blocks of binary data, or arrays, efficiently.
In reality, these functions can also be used to save and read complex data structures defined by the user through the struct keyword. We can, in fact, use fwrite to write an entire structure to a binary file, and fread to read the structure from the file and store it in a struct type variable. In this way, we can easily save and retrieve structured data without having to convert it to text format.
For example, suppose we have the following data structure representing a person:
struct Person {
char name[50];
int age;
};
We can write an instance of this structure to a binary file using fwrite like this:
struct Person p = {"John Dow", 30};
fwrite(&p, sizeof(struct Person), 1, binary_file);
Similarly, we can read the structure from the binary file using fread:
struct Person p;
fread(&p, sizeof(struct Person), 1, binary_file);
There is only one thing we must be careful about when saving and reading data structures with fwrite and fread: the presence of pointer-type fields within the structure. In the example structure above, there are no pointers, so we can save and read the structure without problems.
However, suppose we modify the Person structure so that the name is no longer a static string (a static array of characters), but a pointer to character:
struct Person {
char *name;
int age;
};
To create a structure of this type, we must dynamically allocate memory for the name field:
struct Person p;
p.name = malloc(50 * sizeof(char));
strcpy(p.name, "John Doe");
p.age = 30;
But at this point, if we try to save the structure p to a binary file using fwrite, as in the following example:
fwrite(&p, sizeof(struct Person), 1, binary_file);
We are not saving the content of the string "John Doe", but only the value of the pointer name, which represents a memory address. When we read the structure from the binary file, the name field will contain a memory address that is no longer valid, since the memory it pointed to may have been freed or overwritten.
Therefore, in these cases, we must adopt a different approach to save and read data. In the next lessons, we will see how to correctly handle the reading and writing of complex data structures containing pointers, using techniques such as data serialization and deserialization.
Example: Phone Directory
Let's try to put together what we have seen so far with a complete example of a program that manages a phone directory by saving and reading contacts from a binary file.
This program allows performing the following operations:
- Add a new contact to the directory.
- Display all contacts present in the directory.
- Save contacts to a binary file.
- Load contacts from a binary file.
First, let's define the data structure to represent a directory contact:
struct Contact {
char name[50];
char phone_number[15];
};
The program will store contacts in memory using a static array of Contact structures. This array can contain a maximum of 100 contacts. In a separate variable, we will keep track of the number of contacts currently present in the directory.
When the program saves contacts to a binary file, it will use the fwrite function to write two things:
- The number of contacts present in the directory.
- The array of contacts.
Similarly, when the program loads contacts from a binary file, it will use the fread function to first read the number of contacts and then the array of contacts.
Let's first implement the function to save contacts to a binary file:
/*
* Function to save the directory to a binary file
* Parameters:
* - file_name: the name of the file to save the directory to
* - directory: the array of contacts to save
* - num_contacts: the number of contacts present in the directory
* Returns:
* - 0 on success, -1 on error
*/
int save_directory(const char *file_name,
struct Contact directory[],
size_t num_contacts) {
FILE *file = fopen(file_name, "wb");
if (file == NULL) {
return -1; // Error opening file
}
// First writes the number of contacts
fwrite(&num_contacts, sizeof(size_t), 1, file);
// Writes the array of contacts
fwrite(directory, sizeof(struct Contact), num_contacts, file);
fclose(file);
// Success
return 0;
}
Now, let's implement the function to load contacts from a binary file. This function will first read the number of contacts and then the array of contacts. To avoid overflow problems, the function will check that the number of contacts read does not exceed the maximum size of the contacts array.
/*
* Function to load the directory from a binary file
* Parameters:
* - file_name: the name of the file to load the directory from
* - directory: the array to store the loaded contacts
* - max_contacts: the maximum size of the contacts array
* Returns:
* - the number of contacts loaded on success, -1 on error
*/
int load_directory(const char *file_name,
struct Contact directory[],
size_t max_contacts) {
FILE *file = fopen(file_name, "rb");
if (file == NULL) {
return -1; // Error opening file
}
size_t num_contacts;
// Reads the number of contacts
fread(&num_contacts, sizeof(size_t), 1, file);
// Checks that the number of contacts does not exceed the maximum size
if (num_contacts > max_contacts) {
fclose(file);
return -1; // Error: number of contacts too large
}
// Reads the array of contacts
fread(directory, sizeof(struct Contact), num_contacts, file);
fclose(file);
// Returns the number of contacts loaded
return num_contacts;
}
With these two functions, we can now implement the rest of the program to manage the phone directory, allowing the user to add contacts, display them, save them to a binary file, and load them from a binary file.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_CONTACTS 100
struct Contact {
char name[50];
char phone_number[15];
};
int save_directory(const char *file_name,
struct Contact directory[],
size_t num_contacts) {
FILE *file = fopen(file_name, "wb");
if (file == NULL) {
return -1; // Error opening file
}
fwrite(&num_contacts, sizeof(size_t), 1, file);
fwrite(directory, sizeof(struct Contact), num_contacts, file);
fclose(file);
return 0;
}
int load_directory(const char *file_name,
struct Contact directory[],
size_t max_contacts) {
FILE *file = fopen(file_name, "rb");
if (file == NULL) {
return -1; // Error opening file
}
size_t num_contacts;
fread(&num_contacts, sizeof(size_t), 1, file);
if (num_contacts > max_contacts) {
fclose(file);
return -1; // Error: number of contacts too large
}
fread(directory, sizeof(struct Contact), num_contacts, file);
fclose(file);
return num_contacts;
}
int main() {
struct Contact directory[MAX_CONTACTS];
size_t num_contacts = 0;
int choice;
do {
printf("Phone Directory\n");
printf("1. Add Contact\n");
printf("2. Display Contacts\n");
printf("3. Save Directory to File\n");
printf("4. Load Directory from File\n");
printf("5. Exit\n");
printf("Choice: ");
scanf("%d", &choice);
// Consumes the newline character
getchar();
if (choice == 1) {
if (num_contacts < MAX_CONTACTS) {
printf("Name: ");
fgets(directory[num_contacts].name,
sizeof(directory[num_contacts].name),
stdin);
// Removes newline
directory[num_contacts].name[strcspn(directory[num_contacts].name, "\n")] = 0;
printf("Phone Number: ");
fgets(directory[num_contacts].phone_number,
sizeof(directory[num_contacts].phone_number),
stdin);
// Removes newline
directory[num_contacts].phone_number[strcspn(directory[num_contacts].phone_number, "\n")] = 0;
num_contacts++;
} else {
printf("Directory full!\n");
}
} else if (choice == 2) {
for (size_t i = 0; i < num_contacts; i++) {
printf("%s - %s\n", directory[i].name, directory[i].phone_number);
}
} else if (choice == 3) {
if (save_directory("directory.bin", directory, num_contacts) == 0) {
printf("Directory saved successfully.\n");
} else {
printf("Error saving directory.\n");
}
} else if (choice == 4) {
int loaded_contacts = load_directory("directory.bin", directory, MAX_CONTACTS);
if (loaded_contacts >= 0) {
num_contacts = loaded_contacts;
printf("Directory loaded successfully.\n");
} else {
printf("Error loading directory.\n");
}
}
printf("\n");
} while (choice != 5);
return 0;
}
If we try to compile and run this program, we can add contacts to the directory, display them, save them to a binary file called directory.bin, and load them again from the file.
For example, if we add two contacts:
Phone Directory
1. Add Contact
2. Display Contacts
3. Save Directory to File
4. Load Directory from File
5. Exit
Choice: 1
Name: John Doe
Phone Number: 0123456789
Phone Directory
1. Add Contact
2. Display Contacts
3. Save Directory to File
4. Load Directory from File
5. Exit
Choice: 1
Name: Louis Green
Phone Number: 0987654321
Phone Directory
1. Add Contact
2. Display Contacts
3. Save Directory to File
4. Load Directory from File
5. Exit
Choice: 2
John Doe - 0123456789
Louis Green - 0987654321
Phone Directory
1. Add Contact
2. Display Contacts
3. Save Directory to File
4. Load Directory from File
5. Exit
Choice: 3
Directory saved successfully.
Phone Directory
1. Add Contact
2. Display Contacts
3. Save Directory to File
4. Load Directory from File
5. Exit
Choice: 5
In this way, the program will create a binary file directory.bin containing the saved contacts. We can then run the program again, choose the option to load the directory from the binary file, and display the previously saved contacts:
Phone Directory
1. Add Contact
2. Display Contacts
3. Save Directory to File
4. Load Directory from File
5. Exit
Choice: 4
Directory loaded successfully.
Phone Directory
1. Add Contact
2. Display Contacts
3. Save Directory to File
4. Load Directory from File
5. Exit
Choice: 2
John Doe - 0123456789
Louis Green - 0987654321