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:

  1. Load any saved contacts when it starts
  2. Let you add new contacts
  3. Let you search for contacts by name
  4. Let you see all your contacts
  5. Let you delete contacts
  6. 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 list
  • add_contact() - Add a new contact
  • find_contact() - Search for a contact
  • delete_contact() - Remove a contact
  • list_contacts() - Print all contacts

Working with files:

  • save_contacts() - Save to a file
  • load_contacts() - Load from a file

User interface:

  • print_menu() - Show the menu
  • get_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.c

Then run it:

./contacts

On 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:

  1. Shows the menu
  2. Gets the user’s choice
  3. Calls the right function based on their choice
  4. Repeats until they choose to quit

Why We Pass &list

handle_add(&list);  // Pass the ADDRESS

We 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 contacts

This 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 contacts

Ideas to Make It Better

Try adding these features:

  1. Edit contacts - Let users change a contact’s name or phone
  2. Sort contacts - Show contacts in alphabetical order
  3. Search by phone - Find out who a phone number belongs to
  4. Categories - Group contacts into Family, Friends, Work
  5. Export to text - Save contacts as a human-readable text file
  6. 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:

ConceptWhere We Used It
VariablesEverywhere!
ArraysThe contacts array, character arrays for strings
StructsContact and ContactList
Functionsadd_contact, delete_contact, save_contacts, etc.
PointersPassing ContactList *list to functions
LoopsThe main menu loop, searching through contacts
ConditionalsMenu choices, validation
FilesSaving and loading contacts

Lessons from Real Programming

Building this taught some important lessons:

  1. Plan before you code - We figured out our structs and functions before writing anything
  2. Break problems into pieces - Each function does one thing
  3. Handle errors - We check if files open, if inputs are valid, if the list is full
  4. Make it user-friendly - Confirm before deleting, show helpful messages
  5. 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:

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.