When One main Stops Working

Fourteen chapters ago a whole program fit in ten lines, and putting everything in main was the right call. Somewhere around a hundred lines it stops being the right call, and by three hundred it is a trap. Here is the shape of a grade-report program written the way first drafts always are — this is the wrong way to structure it, shown abridged:

int main(void)
{
    /* 40 lines: declare every variable the whole program will ever use */
    int marks[MAX_STUDENTS][MAX_SUBJECTS];
    char names[MAX_STUDENTS][NAME_LEN];
    int totals[MAX_STUDENTS];
    /* ...twenty more... */

    /* 60 lines: read and validate every student record */
    /* 50 lines: compute totals, averages, and grades */
    /* 45 lines: find toppers and failure lists */
    /* 70 lines: print the report, page by page */
    /* 30 lines: write everything to grades.dat */
    return 0;
}

Every variable is visible to every line, so nothing tells you which of those twenty arrays the printing phase actually touches — you must read all three hundred lines to know. A bug in grading cannot be tested without also running input and printing. And when a change to the report format breaks the topper search, the only tool you have is reading the whole thing again. The program is not wrong; it is unreadable, and unreadable programs grow bugs faster than you can fix them.

Top-Down Design: Name the Functions First

The cure is to design from the top down. Before writing any body, ask: what are the steps of this program? A grade reporter reads records, computes grades, prints a report, saves the file. Those step names become function names, and main becomes a table of contents that reads like the program's outline:

int main(void)
{
    struct student students[MAX_STUDENTS];
    int count;

    count = readStudents(students, MAX_STUDENTS);
    computeGrades(students, count);
    printReport(students, count);
    return saveReport(students, count);
}

Ten lines now tell you the entire story, and each name is a promise you fill in below, one function at a time — each testable on its own, exactly the way chapter 9 built programs from functions. If a step is itself complicated, repeat the trick inside it: printReport might call printHeader, printOneStudent, printSummary. You never face three hundred lines at once; you face a handful of ten-to-thirty-line problems.

One Job per Function

The discipline that keeps this working is: one function, one job, stated by its name. Two reliable signals tell you a function has taken on a second job. First, the honest name for it contains "and" — readAndValidateAndStore is three functions wearing one set of braces. Second, the body needs blank lines and comments to separate its phases: the moment you write /* now compute the average */ halfway down, the code below that comment wants to be a function called computeAverage. A function you can name cleanly with one verb phrase is usually the right size; a function you cannot name without "and" is usually two.

Prototypes on Top, Definitions Below

Chapter 9 introduced prototypes; in a larger program they become the file's public outline. Put every prototype at the top of the file, in the order the functions matter, then define the functions below in the same reading order — main first, helpers after, so a reader meets the story before the details. The prototype block is the first thing anyone sees, and read together, it should describe the program:

void printMenu(void);
int readInt(const char *prompt, int *out);
int addNumber(int numbers[], int *count);
void printStats(const int numbers[], int count);

That is four lines of documentation the compiler checks: every call is verified against its full parameter list (never write empty parentheses — int readInt(); means "unspecified parameters" and turns off checking, as chapter 9 warned). In a multi-file program, the same outline would move into a header behind an include guard; the sandbox compiles one file, so here the top of the file plays that role.

Names Do Half the Work

Naming is not decoration; it is the design made visible. The course's conventions, applied consistently: functions are verb phrases (readStudents, computeGrades, printStats) because they do things; data names are nouns (numbers, count, capacity) because they are things; both in the same camelCase everywhere, with #define constants in capitals (CAPACITY, MAX_STUDENTS). The name states what the function does; the body states how. If a reader must open the body to learn what a function does, the name failed — and if a comment must restate the name, the comment is filler.

Pass Data In, Return Results Out

The giant main had one honest advantage: every phase could see every variable. Do not recreate that with file-scope globals — a global is writable from anywhere, so the question "what changes count?" is answerable only by reading the entire program again. Instead keep data flow explicit: each function receives what it needs as parameters and hands results back through return values or pointer parameters (chapter 11). Then every function's dependencies are printed in its prototype, and you can test it alone.

The same discipline turns repeated code into reusable tools. The guarded-scanf check this course has used since chapter 5 belongs in a function you write once and call everywhere input happens:

int readInt(const char *prompt, int *out)
{
    printf("%s", prompt);
    if (scanf("%d", out) != 1) {
        printf("invalid input\n");
        return 0;
    }
    return 1;
}

The result travels out through the pointer parameter; the return value reports success, and callers must check it — the same convention as scanf, fopen, and malloc.

A Small Program Built This Way

Here is the whole method in one program: a menu-driven number logger with a fixed-capacity array. Read the prototype block, then main, and you know everything before reading a single helper:

Run with the input 1 12 1 7 1 41 2 3 (add 12, 7, and 41, print stats, quit), it produces:

1) add number  2) print stats  3) quit
choice: value: stored 12 (1 of 100 used)
1) add number  2) print stats  3) quit
choice: value: stored 7 (2 of 100 used)
1) add number  2) print stats  3) quit
choice: value: stored 41 (3 of 100 used)
1) add number  2) print stats  3) quit
choice: count 3, min 7, max 41, mean 20.00
1) add number  2) print stats  3) quit
choice: goodbye

Notice where everything lives. main owns the data — numbers and count are locals, not globals — and lends them out: addNumber gets &count because it must change it, printStats gets a const array and a plain count because it must not. Every readInt and addNumber result is checked. CAPACITY appears in one #define, so growing the array is a one-line change. And each helper is small enough to test in your head. This is the same structure at 90 lines that will carry a 300-line program: nothing about it needs to change, you just add more well-named functions to the outline.

From Here to the Capstone

You now have the habits: outline first, one job per function, prototypes as the table of contents, explicit data flow, names that carry the design. Before applying them at full scale, one detour is needed — the next lesson squares this course's careful C with the older dialect that exam papers and old textbooks still speak, so you can read both without being fooled. Then the capstone builds a complete records manager — structs, dynamic memory, files, everything — using exactly the structure from this lesson, just with more entries in the outline.

Key Takeaways

  • A single giant main fails at scale because every variable is visible to every line: nothing can be read, tested, or changed in isolation.
  • Design top-down: name the program's steps first, make each step a function, and write main as a table of contents that reads like the program's outline.
  • One function, one job — if its honest name needs "and", or its body needs comments to separate phases, split it.
  • Put every prototype (with full parameter lists) at the top of the file as the program's public outline, then define functions below in reading order.
  • Functions get verb-phrase names, data gets noun names, all in consistent camelCase, with #define constants in capitals; the name states what, the body states how.
  • Keep data flow explicit — pass parameters and return results instead of using globals — so each function's dependencies are visible in its prototype, and wrap recurring work like guarded scanf input in one reusable, checked helper.