LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
C ProgrammingBeginner~1.5 hours

Contact Book

Create a contact management system with add, delete, search, and update functionality using file handling.

StructuresFile I/OArrays

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.

What You'll Build
  • 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 on
typedef 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.

Sample Run of addContact()

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/fread
typedef 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

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.