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.
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).
mainis one function; CargoForge-C is built almost entirely out of small functions likeparse_ship_configandperform_analysiscalling 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 toparse_cli_argsanddispatch_subcommandrather than doing any real work itself.argc/argv= "the words you typed on the command line, counted and collected" โ when you runcargoforge optimize sample_ship.cfg sample_cargo.txt, the OS setsargc = 4andargv[2]/argv[3]to the two file paths that every downstream handler depends on.CLIContext= "a single struct that carries everythingmainlearned from the command line" โinit_cli_contextfills it with safe defaults,parse_cli_argspopulates it, anddispatch_subcommandreads it to route tocmd_optimize(which in turn callsparse_ship_config,parse_cargo_list,place_cargo_3d, andperform_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_ARGSon bad arguments, and whateverdispatch_subcommandreturns for runtime errors; shell scripts,make, and CI pipelines read this number to decide whether to continue. free_cli_contextbefore everyreturn= "clean up your mess no matter which door you leave through" โmaincalls it in both the early-exit branch and the normal-exit branch; skipping it in either place would leak whatever memoryparse_cli_argsallocated insidectx.printfwith format specifiers = "a fill-in-the-blank template for text" โ CargoForge-C uses it insideprint_loading_planto emit lines likeDraft: 5.23 m;%.2fmeans "a decimal number rounded to two places," and\nends 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:
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:
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 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¶
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:
The Makefile compiles every .c file in src/ with these flags:
-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-C will:
- Parse the ship configuration from
sample_ship.cfg(length, width, deadweight, lightship weight, and so on). - Parse the cargo manifest from
sample_cargo.txt(item IDs, weights, dimensions, types). - Place cargo into the three hard-coded holds (ForwardHold, AftHold, Deck) using the 3-D bin-packing algorithm.
- Run the stability analysis โ draft, KG, GM, GZ curve, IMO criteria.
- 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:
- 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.
\nis 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
mainfunction; the OS calls it and receives its return value as the program's exit code. argcis the count of command-line words;argvis the array of those words as strings, withargv[0]being the program name.EXIT_SUCCESS(0) signals success to the shell; any non-zero value signals failure. CargoForge-C definesEXIT_INVALID_ARGSfor bad argument errors.mainin CargoForge-C does three things only: initialise a context struct, parse the CLI arguments, and dispatch to the right subcommand handler.printfwrites 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 inmaincallfree_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.