Overview
A contact book is one of the most natural first "real" projects in C because it forces you to combine four skills you have learned separately into one working program: structures to model a contact, arrays to hold many of them in memory, string handling to search and compare names, and file I/O to make the data survive after the program exits. Nothing you type into a contact book should disappear the moment you close the terminal, and that persistence requirement is exactly what pushes this project beyond a toy exercise.
By the end of this tutorial you will have a menu-driven console application that stores contacts on disk in a binary file, loads them back in on startup, and lets the user add, list, search, update, and delete entries. Every operation you build reinforces how structs, arrays of structs, and `fread`/`fwrite` work together in a single, coherent program.
- A `Contact` struct holding name, phone number, and email.
- An "Add Contact" feature that appends a new entry to a binary data file.
- A "List Contacts" feature that reads every saved contact and prints them in a formatted table.
- A "Search Contact" feature that finds a contact by name.
- Update and delete features that rewrite the file with the modified contact list.
- A menu loop that keeps the program running until the user chooses to exit.
Prerequisites
- Structures — defining a `struct`, creating variables of that type, and accessing members with the dot operator.
- Arrays — declaring fixed-size arrays and looping over them with `for`.
- Strings in C — using `char` arrays, and functions like `strcpy` and `strcmp` from `<string.h>`.
- File handling — opening a file with `fopen`, checking for `NULL`, and closing it with `fclose`.
- Functions — passing structs and arrays to functions, and returning values from them.
Project Structure
The whole program lives in a single file, `contact_book.c`, which is typical for small C console projects. Internally, we will keep the design simple: every contact is a `struct Contact` with three fields (name, phone, email). Rather than keeping contacts only in memory, we persist them in a binary file named `contacts.dat` using `fwrite` to write raw `struct Contact` records and `fread` to read them back. Binary I/O is a good fit here because a `struct` can be written and read as one fixed-size block, with no manual text parsing required.
Each menu operation follows the same general pattern: open `contacts.dat` in the appropriate mode (`"ab"` to append, `"rb"` to read, or `"wb"` to rewrite), perform the operation, and close the file immediately afterward. Update and delete are slightly different because C files do not support removing a record from the middle directly — instead, we read every contact into a temporary array, apply the change in memory, and rewrite the whole file.
Step 1: Define the Contact Structure
Everything starts with the data model. We define a `struct Contact` with fixed-size character arrays for the name, phone number, and email, and we introduce a file name constant so it is defined in exactly one place. Fixed-size fields are important for binary file I/O — since we will write the struct directly to disk with `fwrite`, every record must be exactly the same number of bytes.
#include <stdio.h> // Gives us printf/scanf and the FILE type for console and file I/O#include <string.h> // Gives us strcpy/strcmp, needed later to copy and compare contact names#include <stdlib.h> // Standard utility library, included by convention for a self-contained C file
#define FILENAME "contacts.dat" // One named constant for the data file so every function agrees on it#define NAME_LEN 50 // Max stored length for a name, including the null terminator#define PHONE_LEN 15 // Max stored length for a phone number#define EMAIL_LEN 50 // Max stored length for an email address
// Fixed-size char arrays make every Contact the same number of bytes, which fwrite/fread rely ontypedef struct { char name[NAME_LEN]; // Contact's name, stored as a fixed-size buffer char phone[PHONE_LEN]; // Contact's phone number, stored as a fixed-size buffer char email[EMAIL_LEN]; // Contact's email, stored as a fixed-size buffer} Contact;Using `typedef struct { ... } Contact;` lets us write `Contact` instead of `struct Contact` everywhere else in the program, which keeps function signatures shorter and more readable.
Step 2: Add a New Contact
Adding a contact means reading three strings from the user, filling in a `Contact` variable, and appending it to `contacts.dat`. We open the file in `"ab"` (append binary) mode, which creates the file if it does not exist yet and always writes new data after whatever is already there.
void addContact(void) { Contact c; // Local record we'll fill in and then write to disk FILE *fp = fopen(FILENAME, "ab"); // Open in append-binary mode so writing never overwrites earlier contacts if (fp == NULL) { // fopen returns NULL if the file can't be created/opened printf("Error: could not open file for writing.\n"); return; // Bail out early rather than working with a missing file }
printf("Enter name: "); scanf(" %49[^\n]", c.name); // Read up to 49 chars up to newline so names with spaces work printf("Enter phone: "); scanf(" %14[^\n]", c.phone); // Cap at 14 chars to match PHONE_LEN's 15-byte buffer printf("Enter email: "); scanf(" %49[^\n]", c.email); // Same bounded read, guards against buffer overflow
fwrite(&c, sizeof(Contact), 1, fp); // Write exactly one Contact-sized block, taken from &c fclose(fp); // Flush to disk and release the file handle immediately
printf("Contact \"%s\" saved successfully!\n", c.name);}The `%49[^\n]` format specifier reads up to 49 characters until it hits a newline, which safely captures names with spaces (unlike plain `%s`, which stops at the first space) while never overflowing the 50-byte `name` array. `fwrite(&c, sizeof(Contact), 1, fp)` writes exactly one block of `sizeof(Contact)` bytes, taken from the address of `c`, into the file.
Click Run to see what this code prints.
Step 3: List All Contacts
Listing contacts means opening the file in `"rb"` (read binary) mode and reading one `Contact` record at a time in a loop until `fread` reports that there is nothing left to read. `fread` returns the number of complete records it managed to read, so checking that the return value equals `1` is how we detect end-of-file.
void listContacts(void) { Contact c; // Reused as scratch space for each record read from disk int count = 0; // Tracks how many contacts we've printed, doubles as the row number FILE *fp = fopen(FILENAME, "rb"); // Open read-only in binary mode since we're only reading records if (fp == NULL) { printf("No contacts found yet.\n"); return; // No file yet means no contacts have been added }
printf("\n%-4s %-20s %-15s %-25s\n", "No.", "Name", "Phone", "Email"); // Column headers printf("---------------------------------------------------------------\n"); // Visual separator under headers
while (fread(&c, sizeof(Contact), 1, fp) == 1) { // fread returns 1 while a full record was read; 0 means end of file count++; printf("%-4d %-20s %-15s %-25s\n", count, c.name, c.phone, c.email); // Left-aligned columns for a table look }
if (count == 0) { // Loop ran zero times, so the file existed but was empty printf("No contacts saved yet.\n"); }
fclose(fp); // Release the file handle now that we're done reading}The `%-20s` style format specifiers left-align each field inside a fixed-width column, which gives the output a clean, table-like appearance without needing any external formatting library.
Step 4: Search for a Contact
Searching reuses the same read loop as listing, but instead of printing every contact, it compares each contact's name against the search term using `strcmp` and stops as soon as it finds a match. `strcmp` returns `0` when two strings are exactly equal, which is the condition we check for.
void searchContact(void) { char target[NAME_LEN]; // Holds the name the user is searching for Contact c; // Scratch record reused for each entry read from the file int found = 0; // Flag so we can tell after the loop whether a match turned up
printf("Enter name to search: "); scanf(" %49[^\n]", target); // Same bounded read pattern used for adding contacts
FILE *fp = fopen(FILENAME, "rb"); // Read-only binary mode, same as listing if (fp == NULL) { printf("No contacts found yet.\n"); return; }
while (fread(&c, sizeof(Contact), 1, fp) == 1) { // Walk every saved record until one matches or file ends if (strcmp(c.name, target) == 0) { // strcmp returns 0 only when the strings are identical printf("\nContact found:\n"); printf("Name : %s\n", c.name); printf("Phone: %s\n", c.phone); printf("Email: %s\n", c.email); found = 1; break; // No need to keep scanning once we've found the contact } }
fclose(fp);
if (!found) { // Only reached if the loop finished without ever setting found printf("No contact named \"%s\" was found.\n", target); }}Step 5: Update a Contact
C's standard file functions cannot edit one record in the middle of a file in place without disturbing the rest, so the safest general approach is: read all contacts into memory, find and modify the matching one, then rewrite the entire file from that in-memory array using `"wb"` mode (which truncates the existing file before writing). This is simple to reason about and works well for contact-book-sized data sets.
#define MAX_CONTACTS 100 // Upper bound on how many contacts we can hold in memory at once
int loadAllContacts(Contact list[]) { FILE *fp = fopen(FILENAME, "rb"); // Read-only binary mode, we're only loading existing data int count = 0; // Tracks how many records were actually read into list[] if (fp == NULL) return 0; // No file yet means there's nothing to load
while (count < MAX_CONTACTS && fread(&list[count], sizeof(Contact), 1, fp) == 1) { count++; // Stops at MAX_CONTACTS so we never write past the array bounds } fclose(fp); return count; // Caller needs this to know how many of list[]'s slots are valid}
void saveAllContacts(Contact list[], int count) { FILE *fp = fopen(FILENAME, "wb"); // "wb" truncates the file first, so this fully replaces its contents if (fp == NULL) { printf("Error: could not open file for writing.\n"); return; } fwrite(list, sizeof(Contact), count, fp); // Writes count contiguous Contact records in one call fclose(fp);}
void updateContact(void) { Contact list[MAX_CONTACTS]; // Working copy of every contact, loaded from disk char target[NAME_LEN]; // Name of the contact the user wants to update int count = loadAllContacts(list); // Pull the whole file into memory before making changes int i, found = 0;
printf("Enter name to update: "); scanf(" %49[^\n]", target);
for (i = 0; i < count; i++) { if (strcmp(list[i].name, target) == 0) { // Locate the matching contact by name printf("Enter new phone: "); scanf(" %14[^\n]", list[i].phone); // Overwrite the phone field in memory only, for now printf("Enter new email: "); scanf(" %49[^\n]", list[i].email); // Overwrite the email field the same way found = 1; break; // No need to keep scanning after the match is updated } }
if (found) { saveAllContacts(list, count); // Persist the whole in-memory array, including the one edited record printf("Contact \"%s\" updated successfully!\n", target); } else { printf("No contact named \"%s\" was found.\n", target); }}Notice that `loadAllContacts` and `saveAllContacts` are small, reusable helper functions — both the update and delete features below depend on this same "load into an array, modify, save back" pattern, so writing it once avoids duplicating file-handling code.
Step 6: Delete a Contact
Deleting follows the same load-modify-save pattern as updating, except instead of changing a field we skip copying the matching contact into a second array, effectively removing it before the file is rewritten.
void deleteContact(void) { Contact list[MAX_CONTACTS]; // Every contact currently on disk, loaded into memory Contact remaining[MAX_CONTACTS]; // Second array we'll build up, skipping the deleted contact char target[NAME_LEN]; // Name of the contact to remove int count = loadAllContacts(list); int i, remainingCount = 0, found = 0;
printf("Enter name to delete: "); scanf(" %49[^\n]", target);
for (i = 0; i < count; i++) { if (strcmp(list[i].name, target) == 0) { found = 1; continue; // Skip copying this one into remaining[], which deletes it } remaining[remainingCount++] = list[i]; // Keep every other contact, compacting the array as we go }
if (found) { saveAllContacts(remaining, remainingCount); // Rewrite the file with the shorter, filtered list printf("Contact \"%s\" deleted successfully!\n", target); } else { printf("No contact named \"%s\" was found.\n", target); }}Step 7: Build the Menu Loop
Finally, `main` ties every feature together behind a numbered menu that keeps looping until the user chooses to exit. Reading the menu choice with `scanf("%d", &choice)` and using a `switch` statement is a standard, beginner-friendly way to drive a console application like this.
int main(void) { int choice; // Holds the menu option the user typed
do { // do-while so the menu shows at least once before checking the exit condition printf("\n===== CONTACT BOOK =====\n"); printf("1. Add Contact\n"); printf("2. List Contacts\n"); printf("3. Search Contact\n"); printf("4. Update Contact\n"); printf("5. Delete Contact\n"); printf("6. Exit\n"); printf("Enter your choice: "); scanf("%d", &choice);
switch (choice) { // Dispatch to the matching feature based on the number typed case 1: addContact(); break; case 2: listContacts(); break; case 3: searchContact(); break; case 4: updateContact(); break; case 5: deleteContact(); break; case 6: printf("Goodbye!\n"); break; default: printf("Invalid choice, try again.\n"); // Catches any number outside 1-6 } } while (choice != 6); // Keep looping until the user explicitly picks Exit
return 0; // Successful program termination}Complete Code
Here is the full program with every function assembled in the correct order, ready to compile with `gcc contact_book.c -o contact_book`.
#include <stdio.h> // printf/scanf and the FILE type for console and file I/O#include <string.h> // strcpy/strcmp for copying and comparing contact names#include <stdlib.h> // Standard utility library, included by convention
#define FILENAME "contacts.dat" // Single named constant for the data file, used by every function#define NAME_LEN 50 // Max stored length for a name#define PHONE_LEN 15 // Max stored length for a phone number#define EMAIL_LEN 50 // Max stored length for an email address#define MAX_CONTACTS 100 // Upper bound on contacts held in memory at once for update/delete
// Fixed-size fields let a Contact be written/read as one raw block via fwrite/freadtypedef struct { char name[NAME_LEN]; // Contact's name char phone[PHONE_LEN]; // Contact's phone number char email[EMAIL_LEN]; // Contact's email} Contact;
void addContact(void) { Contact c; // Local record we'll fill in and then write to disk FILE *fp = fopen(FILENAME, "ab"); // Open in append-binary mode so writing never overwrites earlier contacts if (fp == NULL) { // fopen returns NULL if the file can't be created/opened printf("Error: could not open file for writing.\n"); return; // Bail out early rather than working with a missing file }
printf("Enter name: "); scanf(" %49[^\n]", c.name); // Read up to 49 chars up to newline so names with spaces work printf("Enter phone: "); scanf(" %14[^\n]", c.phone); // Cap at 14 chars to match PHONE_LEN's 15-byte buffer printf("Enter email: "); scanf(" %49[^\n]", c.email); // Same bounded read, guards against buffer overflow
fwrite(&c, sizeof(Contact), 1, fp); // Write exactly one Contact-sized block, taken from &c fclose(fp); // Flush to disk and release the file handle immediately
printf("Contact \"%s\" saved successfully!\n", c.name);}
void listContacts(void) { Contact c; // Reused as scratch space for each record read from disk int count = 0; // Tracks how many contacts we've printed, doubles as the row number FILE *fp = fopen(FILENAME, "rb"); // Open read-only in binary mode since we're only reading records if (fp == NULL) { printf("No contacts found yet.\n"); return; // No file yet means no contacts have been added }
printf("\n%-4s %-20s %-15s %-25s\n", "No.", "Name", "Phone", "Email"); // Column headers printf("---------------------------------------------------------------\n"); // Visual separator under headers
while (fread(&c, sizeof(Contact), 1, fp) == 1) { // fread returns 1 while a full record was read; 0 means end of file count++; printf("%-4d %-20s %-15s %-25s\n", count, c.name, c.phone, c.email); // Left-aligned columns for a table look }
if (count == 0) { // Loop ran zero times, so the file existed but was empty printf("No contacts saved yet.\n"); }
fclose(fp); // Release the file handle now that we're done reading}
void searchContact(void) { char target[NAME_LEN]; // Holds the name the user is searching for Contact c; // Scratch record reused for each entry read from the file int found = 0; // Flag so we can tell after the loop whether a match turned up
printf("Enter name to search: "); scanf(" %49[^\n]", target); // Same bounded read pattern used for adding contacts
FILE *fp = fopen(FILENAME, "rb"); // Read-only binary mode, same as listing if (fp == NULL) { printf("No contacts found yet.\n"); return; }
while (fread(&c, sizeof(Contact), 1, fp) == 1) { // Walk every saved record until one matches or file ends if (strcmp(c.name, target) == 0) { // strcmp returns 0 only when the strings are identical printf("\nContact found:\n"); printf("Name : %s\n", c.name); printf("Phone: %s\n", c.phone); printf("Email: %s\n", c.email); found = 1; break; // No need to keep scanning once we've found the contact } }
fclose(fp);
if (!found) { // Only reached if the loop finished without ever setting found printf("No contact named \"%s\" was found.\n", target); }}
int loadAllContacts(Contact list[]) { FILE *fp = fopen(FILENAME, "rb"); // Read-only, we're just loading what's already saved int count = 0; // Number of records successfully read if (fp == NULL) return 0; // No file yet, so nothing to load
while (count < MAX_CONTACTS && fread(&list[count], sizeof(Contact), 1, fp) == 1) { count++; // Bounded by MAX_CONTACTS to avoid overrunning list[] } fclose(fp); return count; // Tells the caller how many entries in list[] are valid}
void saveAllContacts(Contact list[], int count) { FILE *fp = fopen(FILENAME, "wb"); // "wb" truncates the file so this fully rewrites it if (fp == NULL) { printf("Error: could not open file for writing.\n"); return; } fwrite(list, sizeof(Contact), count, fp); // Write all count records back in one contiguous block fclose(fp);}
void updateContact(void) { Contact list[MAX_CONTACTS]; // Working copy of every saved contact char target[NAME_LEN]; // Name to search for int count = loadAllContacts(list); // Load everything before editing anything int i, found = 0;
printf("Enter name to update: "); scanf(" %49[^\n]", target);
for (i = 0; i < count; i++) { if (strcmp(list[i].name, target) == 0) { // Found the contact to edit printf("Enter new phone: "); scanf(" %14[^\n]", list[i].phone); // Update in memory first printf("Enter new email: "); scanf(" %49[^\n]", list[i].email); found = 1; break; } }
if (found) { saveAllContacts(list, count); // Write the whole updated array back to disk printf("Contact \"%s\" updated successfully!\n", target); } else { printf("No contact named \"%s\" was found.\n", target); }}
void deleteContact(void) { Contact list[MAX_CONTACTS]; // Every contact currently on disk, loaded into memory Contact remaining[MAX_CONTACTS]; // Second array we'll build up, skipping the deleted contact char target[NAME_LEN]; // Name of the contact to remove int count = loadAllContacts(list); int i, remainingCount = 0, found = 0;
printf("Enter name to delete: "); scanf(" %49[^\n]", target);
for (i = 0; i < count; i++) { if (strcmp(list[i].name, target) == 0) { found = 1; continue; // Skip copying this one into remaining[], which deletes it } remaining[remainingCount++] = list[i]; // Keep every other contact, compacting the array as we go }
if (found) { saveAllContacts(remaining, remainingCount); // Rewrite the file with the shorter, filtered list printf("Contact \"%s\" deleted successfully!\n", target); } else { printf("No contact named \"%s\" was found.\n", target); }}
int main(void) { int choice; // Holds the menu option the user typed
do { // do-while so the menu shows at least once before checking the exit condition printf("\n===== CONTACT BOOK =====\n"); printf("1. Add Contact\n"); printf("2. List Contacts\n"); printf("3. Search Contact\n"); printf("4. Update Contact\n"); printf("5. Delete Contact\n"); printf("6. Exit\n"); printf("Enter your choice: "); scanf("%d", &choice);
switch (choice) { // Dispatch to the matching feature based on the number typed case 1: addContact(); break; case 2: listContacts(); break; case 3: searchContact(); break; case 4: updateContact(); break; case 5: deleteContact(); break; case 6: printf("Goodbye!\n"); break; default: printf("Invalid choice, try again.\n"); // Catches any number outside 1-6 } } while (choice != 6); // Keep looping until the user explicitly picks Exit
return 0; // Successful program termination}Sample Run
Click Run to see what this code prints.
Extend This Project
- Prevent duplicate contacts by checking whether the entered name already exists before calling `addContact`.
- Sort the contact list alphabetically before printing, using `qsort` with a custom comparator on the `name` field.
- Add a "search by partial name" mode using `strstr` instead of requiring an exact `strcmp` match.
- Encrypt the phone and email fields with a simple XOR cipher before writing them to disk, and decrypt on read.
- Replace the fixed `MAX_CONTACTS` array with dynamic memory (`malloc`/`realloc`) so the contact book has no hard upper limit.
Summary
You built a complete, persistent Contact Book in C, combining a struct-based data model, array-based in-memory processing, and binary file I/O into one cohesive application. The load-modify-save pattern you used for updating and deleting is a technique you will reach for constantly in future C projects any time you need to change one record inside a flat file.