Practical Project: Building a Contact Manager
You’ve learned the pieces. Now let’s build something real.
We’re going to create a contact manager - a program that stores names and phone numbers, lets you search and delete contacts, and saves everything to a file. When you close the program and open it again, your contacts are still there.
This uses everything from the entire tutorial: variables, loops, functions, structs, pointers, arrays, and files.
What We’re Building
Our contact manager will:
- Load any saved contacts when it starts
- Let you add new contacts
- Let you search for contacts by name
- Let you see all your contacts
- Let you delete contacts
- Save everything when you quit
Here’s what it looks like when running:
Loaded 4 contact(s).
=== Contact Manager ===
1. Add contact
2. Search contacts
3. List all contacts
4. Delete contact
5. Save and quit
Choice: 3
# Name Phone
--------------------------------------------
1 Jenny 867-5309
2 Alice Smith 555-1234
3 Bob Jones 555-5678
4 Mom 555-9999
Total: 4 contact(s)Step 1: Plan the Data
Before writing code, let’s figure out what data we need.
A contact has:
- A name (text)
- A phone number (text)
We’ll use a struct:
#define MAX_NAME 50
#define MAX_PHONE 20
#define MAX_CONTACTS 100
typedef struct {
char name[MAX_NAME];
char phone[MAX_PHONE];
} Contact;We’ll store all contacts in an array, along with a count of how many we have:
typedef struct {
Contact contacts[MAX_CONTACTS];
int count;
} ContactList;Step 2: Plan the Functions
Let’s break the program into small, focused functions:
Working with contacts:
init_list()- Set up an empty contact listadd_contact()- Add a new contactfind_contact()- Search for a contactdelete_contact()- Remove a contactlist_contacts()- Print all contacts
Working with files:
save_contacts()- Save to a fileload_contacts()- Load from a file
User interface:
print_menu()- Show the menuget_input()- Get text from the user
Step 3: The Complete Program
Here’s the full code. Don’t worry if it looks like a lot - we’ll walk through the important parts:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#define MAX_NAME 50
#define MAX_PHONE 20
#define MAX_CONTACTS 100
#define FILENAME "contacts.dat"
// ============================================================
// Data Structures
// ============================================================
typedef struct {
char name[MAX_NAME];
char phone[MAX_PHONE];
} Contact;
typedef struct {
Contact contacts[MAX_CONTACTS];
int count;
} ContactList;
// ============================================================
// Helper Functions
// ============================================================
// Remove the newline that fgets leaves at the end
void trim_newline(char *str) {
int len = strlen(str);
if (len > 0 && str[len - 1] == '\n') {
str[len - 1] = '\0';
}
}
// Get input from the user with a prompt
void get_input(char *buffer, int size, const char *prompt) {
printf("%s", prompt);
if (fgets(buffer, size, stdin) != NULL) {
trim_newline(buffer);
}
}
// Check if one string contains another (ignoring uppercase/lowercase)
int str_contains(const char *haystack, const char *needle) {
// Make lowercase copies for comparison
char h_lower[MAX_NAME];
char n_lower[MAX_NAME];
for (int i = 0; haystack[i]; i++) {
h_lower[i] = tolower(haystack[i]);
h_lower[i + 1] = '\0';
}
for (int i = 0; needle[i]; i++) {
n_lower[i] = tolower(needle[i]);
n_lower[i + 1] = '\0';
}
return strstr(h_lower, n_lower) != NULL;
}
// ============================================================
// Contact Operations
// ============================================================
void init_list(ContactList *list) {
list->count = 0;
}
int add_contact(ContactList *list, const char *name, const char *phone) {
if (list->count >= MAX_CONTACTS) {
return 0; // List is full
}
// Get a pointer to the next empty slot
Contact *c = &list->contacts[list->count];
// Copy the name and phone (safely, with size limits)
strncpy(c->name, name, MAX_NAME - 1);
c->name[MAX_NAME - 1] = '\0'; // Make sure it ends with null
strncpy(c->phone, phone, MAX_PHONE - 1);
c->phone[MAX_PHONE - 1] = '\0';
list->count++;
return 1; // Success
}
int delete_contact(ContactList *list, int index) {
if (index < 0 || index >= list->count) {
return 0; // Invalid index
}
// Shift all contacts after this one down by one
for (int i = index; i < list->count - 1; i++) {
list->contacts[i] = list->contacts[i + 1];
}
list->count--;
return 1;
}
void list_contacts(const ContactList *list) {
if (list->count == 0) {
printf("\nNo contacts.\n");
return;
}
printf("\n%-4s %-30s %s\n", "#", "Name", "Phone");
printf("--------------------------------------------\n");
for (int i = 0; i < list->count; i++) {
printf("%-4d %-30s %s\n",
i + 1,
list->contacts[i].name,
list->contacts[i].phone);
}
printf("\nTotal: %d contact(s)\n", list->count);
}
// ============================================================
// File Operations
// ============================================================
int save_contacts(const ContactList *list, const char *filename) {
FILE *file = fopen(filename, "wb"); // Write binary
if (file == NULL) {
return 0;
}
// Write the count first, then all the contacts
fwrite(&list->count, sizeof(int), 1, file);
fwrite(list->contacts, sizeof(Contact), list->count, file);
fclose(file);
return 1;
}
int load_contacts(ContactList *list, const char *filename) {
FILE *file = fopen(filename, "rb"); // Read binary
if (file == NULL) {
return 0; // File doesn't exist yet - that's OK
}
fread(&list->count, sizeof(int), 1, file);
if (list->count > MAX_CONTACTS) {
list->count = MAX_CONTACTS; // Safety check
}
fread(list->contacts, sizeof(Contact), list->count, file);
fclose(file);
return 1;
}
// ============================================================
// User Interface
// ============================================================
void print_menu(void) {
printf("\n");
printf("=== Contact Manager ===\n");
printf("1. Add contact\n");
printf("2. Search contacts\n");
printf("3. List all contacts\n");
printf("4. Delete contact\n");
printf("5. Save and quit\n");
printf("\n");
}
void handle_add(ContactList *list) {
char name[MAX_NAME];
char phone[MAX_PHONE];
get_input(name, MAX_NAME, "Enter name: ");
if (strlen(name) == 0) {
printf("Name cannot be empty.\n");
return;
}
get_input(phone, MAX_PHONE, "Enter phone: ");
if (strlen(phone) == 0) {
printf("Phone cannot be empty.\n");
return;
}
if (add_contact(list, name, phone)) {
printf("Contact added!\n");
} else {
printf("Contact list is full!\n");
}
}
void handle_search(const ContactList *list) {
char search[MAX_NAME];
get_input(search, MAX_NAME, "Search for: ");
printf("\nSearch results:\n");
int found = 0;
for (int i = 0; i < list->count; i++) {
if (str_contains(list->contacts[i].name, search)) {
printf(" %s - %s\n",
list->contacts[i].name,
list->contacts[i].phone);
found++;
}
}
if (found == 0) {
printf(" No matches found.\n");
} else {
printf("\nFound %d contact(s).\n", found);
}
}
void handle_delete(ContactList *list) {
if (list->count == 0) {
printf("No contacts to delete.\n");
return;
}
list_contacts(list);
char input[10];
get_input(input, sizeof(input), "Enter number to delete (0 to cancel): ");
int num = atoi(input); // Convert text to number
if (num == 0) {
printf("Cancelled.\n");
return;
}
if (num < 1 || num > list->count) {
printf("Invalid number.\n");
return;
}
// Confirm deletion
printf("Delete '%s'? (y/n): ", list->contacts[num - 1].name);
char confirm[10];
get_input(confirm, sizeof(confirm), "");
if (confirm[0] == 'y' || confirm[0] == 'Y') {
delete_contact(list, num - 1);
printf("Contact deleted.\n");
} else {
printf("Cancelled.\n");
}
}
// ============================================================
// Main Program
// ============================================================
int main(void) {
ContactList list;
init_list(&list);
// Try to load existing contacts
if (load_contacts(&list, FILENAME)) {
printf("Loaded %d contact(s).\n", list.count);
}
char choice[10];
int running = 1;
while (running) {
print_menu();
get_input(choice, sizeof(choice), "Choice: ");
switch (choice[0]) {
case '1':
handle_add(&list);
break;
case '2':
handle_search(&list);
break;
case '3':
list_contacts(&list);
break;
case '4':
handle_delete(&list);
break;
case '5':
if (save_contacts(&list, FILENAME)) {
printf("Contacts saved.\n");
} else {
printf("Error saving contacts!\n");
}
running = 0;
break;
default:
printf("Invalid choice. Enter 1-5.\n");
}
}
printf("Goodbye!\n");
return 0;
}Step 4: Compile and Run
Save this as contacts.c and compile it:
gcc -Wall -o contacts contacts.cThen run it:
./contactsOn Windows, run contacts.exe instead.
Understanding the Code
Let’s look at some key parts:
The Main Loop
while (running) {
print_menu();
get_input(choice, sizeof(choice), "Choice: ");
switch (choice[0]) {
case '1':
handle_add(&list);
break;
// ... more cases ...
}
}This is the heart of the program. It:
- Shows the menu
- Gets the user’s choice
- Calls the right function based on their choice
- Repeats until they choose to quit
Why We Pass &list
handle_add(&list); // Pass the ADDRESSWe pass the address of list (using &) so the function can modify the actual contact list, not a copy. Remember from Part 10 - without the &, the function would only change its own copy.
Saving and Loading
We use binary files to save contacts:
fwrite(&list->count, sizeof(int), 1, file); // Save the count
fwrite(list->contacts, sizeof(Contact), count, file); // Save all contactsThis writes the data exactly as it exists in memory. When we load, we read it back the same way:
fread(&list->count, sizeof(int), 1, file); // Read the count
fread(list->contacts, sizeof(Contact), count, file); // Read all contactsIdeas to Make It Better
Try adding these features:
- Edit contacts - Let users change a contact’s name or phone
- Sort contacts - Show contacts in alphabetical order
- Search by phone - Find out who a phone number belongs to
- Categories - Group contacts into Family, Friends, Work
- Export to text - Save contacts as a human-readable text file
- Import from text - Load contacts from a text file
Each of these uses things you’ve already learned - just combined in new ways.
What You Built
This ~250-line program uses:
| Concept | Where We Used It |
|---|---|
| Variables | Everywhere! |
| Arrays | The contacts array, character arrays for strings |
| Structs | Contact and ContactList |
| Functions | add_contact, delete_contact, save_contacts, etc. |
| Pointers | Passing ContactList *list to functions |
| Loops | The main menu loop, searching through contacts |
| Conditionals | Menu choices, validation |
| Files | Saving and loading contacts |
Lessons from Real Programming
Building this taught some important lessons:
- Plan before you code - We figured out our structs and functions before writing anything
- Break problems into pieces - Each function does one thing
- Handle errors - We check if files open, if inputs are valid, if the list is full
- Make it user-friendly - Confirm before deleting, show helpful messages
- Keep it simple - Fixed-size arrays are fine for learning (real apps might use dynamic allocation)
What’s Next?
You now have a solid foundation in C. Here are some directions you could go:
- Data structures - Build linked lists, trees, hash tables
- Algorithms - Learn sorting, searching, and problem-solving techniques
- Systems programming - Write a shell, understand how operating systems work
- Game programming - Make simple games using what you know
- Read other people’s code - Look at open source C projects on GitHub
Final Thoughts
C isn’t easy, but it’s simple. There’s not much language to learn - just a few concepts that combine in powerful ways.
The pointers that seemed scary in Parts 7 and 8 are now tools you use naturally. The memory management that seemed tedious is now second nature.
You’ve learned to think like the computer thinks. That knowledge transfers to every other language you’ll ever use. You can look at Python, JavaScript, or Rust and understand what’s happening underneath.
The tech industry is full of people who build layers on top of layers without understanding what’s below. They’re productive, but they’re also stuck. When something breaks in a way they didn’t expect, they’re helpless.
You won’t be.
Planting Seeds
What you’ve learned here is a seed. Not just for your own career, but for something larger.
The world has enough software that extracts - that harvests attention, manufactures addiction, concentrates wealth, and leaves people worse off than it found them. We don’t need more of that. We have plenty.
What we need is software that cultivates. Tools that help people grow food, share knowledge, build community, and solve real problems. Programs that work with people instead of against them. Systems designed to make their users more capable, not more dependent.
Solarpunk isn’t just an aesthetic. It’s a choice about what kind of future we’re building. Every line of code you write is a vote for one future or another.
You now understand how computers actually work. That puts you in a small group of people who can build things that matter - tools that run efficiently on modest hardware, software that respects its users, systems that can be understood and maintained by communities rather than controlled by corporations.
That understanding is valuable. Guard it. Build on it. And when you find someone who wants to learn what you know, teach them. Plant more seeds.
The future isn’t something that happens to us. It’s something we build, one function at a time.
Build something that helps.
Enjoyed This?
If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.