Skip to content

Your first C program

Every C program begins at the same place: a function called main. Understanding what main does โ€” and what the operating system does before and after it โ€” unlocks the structure of every program you will ever read. CargoForge-C is a real-world example: its src/main.c is deliberately small so you can see the skeleton without distraction.

The mental model ๐Ÿง 

A C program is a building with exactly one front door. The operating system is the only visitor, and it always comes in through main โ€” never through any other function directly. On the way in it hands main two things: a count of the words you typed (argc) and the list of those words (argv). main does its job, and on the way out hands back a single number โ€” 0 for "all good," anything else for "something went wrong."

CargoForge's src/main.c is deliberately a receptionist, not a worker. It reads the first word after the program name โ€” optimize, validate, or serve โ€” and walks you to the right department. The heavy lifting lives in other files; main just routes. That is why it stays small enough to read in one sitting.

Hold this picture: one door in, one number out. Every C program you ever open, from a three-line "hello world" to an operating-system kernel, has this exact skeleton underneath.

A C program has exactly one entry point: main() The operating system calls main with argc and argv. CargoForge's main reads the first argument and dispatches to one subcommand โ€” optimize, validate, or serve โ€” then returns a single exit code to the OS. $ cargoforge optimize ship.cfg argc = 3 argv = { "cargoforge", "optimize", "ship.cfg" } OS calls main(argc, argv) optimize validate serve return 0 โ†’ exit code (0 = success)

What this actually means (plain English)

No jargon โ€” here's what the ideas in this lesson actually mean, and why they matter.

  • Function = "a named, reusable block of code that does one job" โ€” you can hand it inputs (its parameters), and it can hand back a result (its return value). main is one function; CargoForge-C is built almost entirely out of small functions like parse_ship_config and perform_analysis calling each other.
  • Array / pointer / struct (all used below, all covered properly much later) = for now: an array is a numbered row of same-type values sitting back-to-back in memory (Lesson 12); a pointer is a variable that stores another variable's address rather than a value itself (Lesson 9); a struct is a labelled bundle of related fields treated as one unit (Lesson 5). This lesson uses all three in passing โ€” a quick "you'll get the full picture soon" is enough for now.
  • main = "the one front door every C program must have" โ€” the OS calls it when you run the program; in CargoForge-C it is a 21-line skeleton that immediately hands control to parse_cli_args and dispatch_subcommand rather than doing any real work itself.
  • argc / argv = "the words you typed on the command line, counted and collected" โ€” when you run cargoforge optimize sample_ship.cfg sample_cargo.txt, the OS sets argc = 4 and argv[2] / argv[3] to the two file paths that every downstream handler depends on.
  • CLIContext = "a single struct that carries everything main learned from the command line" โ€” init_cli_context fills it with safe defaults, parse_cli_args populates it, and dispatch_subcommand reads it to route to cmd_optimize (which in turn calls parse_ship_config, parse_cargo_list, place_cargo_3d, and perform_analysis).
  • exit code = "a number the program hands back to the shell to say whether it succeeded" โ€” CargoForge-C returns EXIT_SUCCESS (0) on success, EXIT_INVALID_ARGS on bad arguments, and whatever dispatch_subcommand returns for runtime errors; shell scripts, make, and CI pipelines read this number to decide whether to continue.
  • free_cli_context before every return = "clean up your mess no matter which door you leave through" โ€” main calls it in both the early-exit branch and the normal-exit branch; skipping it in either place would leak whatever memory parse_cli_args allocated inside ctx.
  • printf with format specifiers = "a fill-in-the-blank template for text" โ€” CargoForge-C uses it inside print_loading_plan to emit lines like Draft: 5.23 m; %.2f means "a decimal number rounded to two places," and \n ends the line cleanly.

Why it matters: if you misread argc/argv indexing you silently pass the wrong file to parse_ship_config or parse_cargo_list and get a corrupt loading plan with no error message; if you skip free_cli_context in the early-exit path you introduce a memory leak that only shows up under Valgrind โ€” two bugs that look like stability-analysis bugs but live entirely in main.


The entry point

When you run any program on Linux or macOS, the operating system loads the binary, sets up memory, and then calls one specific function: main. Your C code must define exactly one main. Everything else is optional.

Here is the complete src/main.c for CargoForge-C:

/* main.c โ€” Entry point for CargoForge-C */
#include "cargoforge.h"
#include "cli.h"
#include <stdlib.h>

int main(int argc, char *argv[]) {
    CLIContext ctx;
    init_cli_context(&ctx);

    int result = parse_cli_args(argc, argv, &ctx);
    if (result <= 0) {
        free_cli_context(&ctx);
        return (result == 0) ? EXIT_SUCCESS : EXIT_INVALID_ARGS;
    }

    int exit_code = dispatch_subcommand(&ctx);
    free_cli_context(&ctx);
    return exit_code;
}

Twenty-one lines total, including the comment and blank lines. That is intentional. main should do almost nothing itself; it exists to hand off to the rest of the program. You will see this pattern in professional C throughout your career.


The signature: int main(int argc, char *argv[])

Return type: int

main returns an integer. The operating system receives this integer when the program exits. By convention:

  • 0 means success.
  • Any non-zero value means something went wrong.

Shell scripts, CI pipelines, and the make build system all rely on this convention. When make test fails, it is because a test binary returned non-zero.

argc โ€” argument count

argc is the number of words typed on the command line, including the program name itself. When you run:

cargoforge optimize examples/sample_ship.cfg examples/sample_cargo.txt

the OS sets argc = 4:

Index Value
0 cargoforge
1 optimize
2 examples/sample_ship.cfg
3 examples/sample_cargo.txt

argv โ€” argument vector

argv is an array of strings (character pointers), one per word โ€” an array is just a numbered row of values back-to-back in memory (Lesson 12 covers this properly), and a pointer stores an address rather than a value directly (Lesson 9). The type char *argv[] means "an array of pointers to characters." You will read this as "argv is an array of strings" โ€” you do not need the full mechanics of either concept yet to follow this lesson.

argv[0] is always the program name. argv[argc] is always a NULL pointer โ€” a sentinel that marks the end of the array.

Note

int argc and char *argv[] are parameters โ€” the named inputs a function declares it needs, listed inside its parentheses โ€” not global variables (a global variable would be visible to every function in the program; a parameter belongs only to the one function that declared it). The OS fills them before calling main. You cannot change what was typed, but you can read every word.


EXIT_SUCCESS and EXIT_INVALID_ARGS

The standard header <stdlib.h> defines EXIT_SUCCESS (0) and EXIT_FAILURE (1). CargoForge-C defines its own EXIT_INVALID_ARGS in cargoforge.h for the case where the user typed bad arguments โ€” a more informative exit code than a generic 1.

The expression on line 15:

return (result == 0) ? EXIT_SUCCESS : EXIT_INVALID_ARGS;

is a ternary operator: if result == 0, return EXIT_SUCCESS; otherwise return EXIT_INVALID_ARGS. It is a compact if-else that fits on one line when the logic is simple.


What main actually does

Step 1 โ€” Initialise the context

CLIContext ctx;
init_cli_context(&ctx);

CLIContext is a struct (defined in cli.h) that holds the parsed subcommand, file paths, output format, and flags for the current run โ€” a struct just bundles several related fields under one name instead of juggling them as separate loose variables (Lesson 5 covers this properly). Declaring CLIContext ctx creates the struct on the stack โ€” one of two places C stores data in memory; for now just know it is automatically cleaned up the moment main returns (Lesson 10 explains exactly what the stack is and why that matters). init_cli_context fills it with safe defaults โ€” zeroes, NULLs, and the human-readable output format.

The &ctx syntax means "the address of ctx." init_cli_context receives a pointer โ€” a variable that stores an address instead of a value (Lesson 9) โ€” so it can modify the struct in place. You will see this pattern constantly in C because functions cannot return large structs cheaply โ€” they operate through pointers instead.

Step 2 โ€” Parse the command line

int result = parse_cli_args(argc, argv, &ctx);
if (result <= 0) {
    free_cli_context(&ctx);
    return (result == 0) ? EXIT_SUCCESS : EXIT_INVALID_ARGS;
}

parse_cli_args reads argv, identifies the subcommand (optimize, validate, info, serve, version, help), stores file paths, and sets flags in ctx. It returns:

  • positive โ€” parsed successfully, proceed.
  • 0 โ€” a help or version request was handled; exit cleanly.
  • negative โ€” invalid arguments; exit with an error code.

The early return here is a guard clause โ€” a pattern where you handle a problem case immediately and exit, rather than wrapping the rest of the function in a big "if everything's fine" block. Get the error cases out of the way immediately so the rest of the function reads top-to-bottom without nesting.

Tip

free_cli_context(&ctx) appears in both exit branches. Releasing resources before every return โ€” not just the last one โ€” is a discipline you must build from day one. CargoForge-C enforces it here precisely because forgetting it causes memory leaks.

Step 3 โ€” Dispatch to the real work

int exit_code = dispatch_subcommand(&ctx);
free_cli_context(&ctx);
return exit_code;

dispatch_subcommand reads ctx.subcommand and calls the appropriate handler: optimize, validate, info, serve, and so on. Each handler does the heavy lifting โ€” parsing ship configs, placing cargo, running the stability analysis โ€” and returns an exit code. main passes that code straight back to the OS.


Building and running

From the repository root:

make

The Makefile compiles every .c file in src/ with these flags:

-O3 -Wall -Wextra -std=c99 -D_POSIX_C_SOURCE=200809L -Iinclude

-Wall -Wextra enable nearly all compiler warnings. Treat warnings as errors from the start: a warning in C is often a real bug.

Once built, run the optimizer on the bundled example files:

./cargoforge optimize examples/sample_ship.cfg examples/sample_cargo.txt

CargoForge-C will:

  1. Parse the ship configuration from sample_ship.cfg (length, width, deadweight, lightship weight, and so on).
  2. Parse the cargo manifest from sample_cargo.txt (item IDs, weights, dimensions, types).
  3. Place cargo into the three hard-coded holds (ForwardHold, AftHold, Deck) using the 3-D bin-packing algorithm.
  4. Run the stability analysis โ€” draft, KG, GM, GZ curve, IMO criteria.
  5. Print a human-readable loading plan to standard output.

All of that work is triggered by the two file-path arguments that main received through argv[2] and argv[3].


printf โ€” printing to the terminal

CargoForge-C uses printf throughout its output routines (for example, inside print_loading_plan in analysis.c). The basic form is:

printf("Draft: %.2f m\n", draft);
  • The first argument is a format string: literal text mixed with conversion specifiers like %d (integer), %f (float), %s (string), %.2f (float to 2 decimal places).
  • Subsequent arguments supply the values for each specifier, in order.
  • \n is a newline character.

printf writes to standard output (stdout), which by default appears in the terminal. The OS connects stdout to the terminal before main is called; you do not have to set it up. Redirect it with > file.txt on the command line and the output goes to a file instead โ€” your C code does not change.

Warning

A missing newline at the end of the last line of output can cause the shell prompt to appear on the same line as your output. Always end the final line of a block of output with \n.


Tracing the full call chain

When you run cargoforge optimize ..., execution flows like this:

OS loader
  โ””โ”€ main(argc=4, argv=[...])
       โ”œโ”€ init_cli_context(&ctx)
       โ”œโ”€ parse_cli_args(argc, argv, &ctx)   // identifies "optimize"
       โ””โ”€ dispatch_subcommand(&ctx)
            โ””โ”€ cmd_optimize(&ctx)            // in cli.c
                 โ”œโ”€ parse_ship_config(...)   // parser.c
                 โ”œโ”€ parse_cargo_list(...)    // parser.c
                 โ”œโ”€ place_cargo_3d(...)      // placement_3d.c
                 โ”œโ”€ perform_analysis(...)    // analysis.c
                 โ””โ”€ output_results(...)      // cli.c โ†’ json_output.c or print_loading_plan

main.c stays untouched no matter which subcommand is added. New subcommands go into cli.c; new analysis modules go into their own .c files. This separation is not accidental โ€” it is the standard pattern for keeping a C program navigable as it grows.


Recap

  • Every C program has exactly one main function; the OS calls it and receives its return value as the program's exit code.
  • argc is the count of command-line words; argv is the array of those words as strings, with argv[0] being the program name.
  • EXIT_SUCCESS (0) signals success to the shell; any non-zero value signals failure. CargoForge-C defines EXIT_INVALID_ARGS for bad argument errors.
  • main in CargoForge-C does three things only: initialise a context struct, parse the CLI arguments, and dispatch to the right subcommand handler.
  • printf writes formatted text to standard output using conversion specifiers (%d, %f, %s, etc.) and escape sequences (\n).
  • Free every resource before every return, not just the last one โ€” both exit paths in main call free_cli_context.

Check yourself

What two things does the operating system hand to main() when a program starts, and what does main() hand back?

It hands in argc (a count of the words typed) and argv (the words themselves, as an array of strings); main hands back a single integer exit code โ€” 0 for success, anything else for failure.

Why is src/main.c deliberately small, mostly just reading argv[1] and dispatching?

main is a receptionist, not a worker โ€” it routes to the right subcommand function and lets the real physics, parsing, and placement logic live in other files. That is what keeps it readable in one sitting.

Next: Types, control flow, and functions.