Types, control flow, and functions¶
C is a typed language: every variable has a fixed type declared at compile time, and the type determines how memory is used and what operations are legal. Understanding why CargoForge-C chooses float over double for physics, and how functions isolate that physics into reusable units, is the first step to reading the codebase fluently.
The mental model ๐ง ¶
A type is a labelled container that fixes both the size of the bits inside and what they mean. Declare float weight and the compiler reserves exactly 4 bytes and treats them as a decimal number; declare int count and it reserves space for a whole number. The compiler is a strict clerk โ it won't let you post a sentence into a slot built for a number.
A function is a machine with a typed intake slot and a typed output chute. float calculate_gm(Ship *s) is a promise: feed me one ship, I hand you back exactly one float. CargoForge keeps each piece of physics in its own machine โ and marks the helpers static so they stay sealed inside analysis.c, invisible to the rest of the program. Control flow (if, for, while) is just the wiring that decides which machines run, and how often.
The catch the clerk won't save you from: types pin size and meaning, not truth. Store a negative number in an unsigned and it silently wraps to a huge positive one. The type system catches typos, not wrong physics โ that is what the tests in Lesson 15 are for.
What this actually means (plain English)¶
No jargon โ here's what the ideas in this lesson actually mean, and why they matter.
- Type = "a label the compiler uses to know how big a variable is and what you can do with it" โ in CargoForge-C this is why every draught, weight, and angle is
float(4 bytes, enough for six digits) rather than the largerdouble, and why theShipandCargostructs stay compact and cache-friendly. floatvsdouble= "single-precision vs double-precision decimal numbers โ about 6โ7 digits of accuracy vs 15โ16" โ real sea-going instruments (draught gauges, load cells) don't resolve beyond a few centimetres or half a tonne, sofloatmatches the sensor, and thefsuffix on every constant like1.025finanalysis.ctells the compiler to keep it that way.memsetzero-init = "wiping a struct's memory to all zeros before filling it in" โperform_analysisdoesmemset(&r, 0, sizeof(r))on theAnalysisResultbefore computing anything, because C does not automatically zero local variables and reading uninitialised memory is undefined behaviour.staticon a function = "this function is private to this one.cfile โ nothing else can call it" โgz_at_angle,integrate_gz, andfind_gz_maxare all markedstaticinanalysis.cso they stay hidden implementation details; onlyperform_analysisandprint_loading_planare visible to the rest of the program.- Pass by value = "the function gets its own copy of whatever you hand it" โ when
perform_analysiscallsgz_at_angle(gm_effective, r.bm, 30.0f), the function works on local copies of those numbers and cannot accidentally change the caller's variables; to write a result back, a pointer (likefloat *max_gzinfind_gz_max) is passed instead. - Trapezoidal rule in
integrate_gz= "slicing the GZ stability curve into 100 thin trapezoids and summing their areas to approximate the area under the curve" โ theforloop inintegrate_gzdoes exactly this, and theif (gz < 0) gz = 0guards inside it ensure a negative righting lever never artificially inflates the stability area.
Why it matters: if you mix up float and double (missing the f suffix) the compiler silently promotes constants to double and back, wasting cycles in the physics hot path; if you read an uninitialised field in an AnalysisResult you get garbage GM or KB values that could pass safety checks with nonsense numbers.
Primitive types¶
C has a small set of numeric types that map directly to CPU registers โ the tiny, extremely fast storage slots built into the processor chip itself, where actual arithmetic happens (as opposed to the much larger but slower main memory a variable normally lives in).
| Type | Typical size | Range / precision | Use in CargoForge-C |
|---|---|---|---|
int |
4 bytes | โ2.1 ร 10โน to +2.1 ร 10โน | loop counters, item counts, flag values |
float |
4 bytes | โ 6โ7 significant digits | all physics quantities (metres, kg, degrees) |
double |
8 bytes | โ 15โ16 significant digits | trigonometry return values, rarely used directly |
char |
1 byte | 0โ255 | characters; arrays of char form strings |
Why float for physics, not double?¶
Stability calculations involve measurements made at sea: draughts measured to the nearest centimetre, weights accurate to perhaps half a tonne. Six significant digits of precision is more than enough โ a real draught gauge does not resolve below a millimetre. Using float instead of double:
- Halves the memory footprint of the
CargoandShipstructs (dozens offloatfields vsdouble). - Keeps struct sizes predictable and cache-friendly.
- Matches the precision of the physical sensors that produced the input data.
analysis.c mixes both: the physics stays in float, but the <math.h> functions (sinf, cosf, tanf, atanf) are the single-precision variants explicitly, so no hidden conversion to double and back happens in the hot path.
The f suffix
In C, a bare literal like 3.14 is a double. Write 3.14f to get a float literal. You will see this throughout analysis.c:
f, the compiler silently promotes the constant to double for any arithmetic that touches it, then truncates it back โ wasted work and a subtle precision trap.
Declaring variables¶
The pattern in C is always: type, then name, then optional initialiser.
C does not zero-initialise local variables automatically. Reading an uninitialised variable is undefined behaviour. CargoForge-C handles this by initialising every local it uses before reading it โ and by using memset to zero entire structs before filling them in:
Control flow¶
if / else if / else¶
The body of an if can be a single statement or a block delimited by { }. CargoForge-C uses blocks consistently.
From print_loading_plan in analysis.c:
const char *gm_str;
float gm_display = a.gm_corrected;
if (gm_display < 0.3f)
gm_str = "CRITICAL - Too tender";
else if (gm_display > 3.0f)
gm_str = "WARNING - Too stiff";
else if (gm_display >= 0.5f && gm_display <= 2.5f)
gm_str = "Optimal";
else
gm_str = "Acceptable";
The conditions are evaluated top-to-bottom; only the first matching branch runs. The && operator is logical AND โ both sub-conditions must be true for the branch to be taken.
for loops¶
The initialiser runs once. Before each iteration the condition is checked; if false, the loop stops. The update runs at the end of each iteration.
From integrate_gz in analysis.c:
int steps = 100;
float step = (end_deg - start_deg) / steps;
float area = 0.0f;
for (int i = 0; i < steps; i++) {
float a1 = start_deg + i * step;
float a2 = a1 + step;
float gz1 = gz_at_angle(gm, bm, a1);
float gz2 = gz_at_angle(gm, bm, a2);
if (gz1 < 0) gz1 = 0;
if (gz2 < 0) gz2 = 0;
float da = step * (float)M_PI / 180.0f;
area += (gz1 + gz2) * 0.5f * da;
}
This loop implements the trapezoidal rule: the area under a curve is approximated by summing 100 thin trapezoids, each of width da radians. The if guards clamp negative GZ values to zero โ a ship cannot have a negative restoring moment that contributes to stability area. The (float) in front of M_PI is a cast: an explicit instruction to treat that value as a float rather than its natural (double-precision) type, keeping the whole expression in single precision on purpose.
Loop variable scope
Declaring int i inside the for initialiser (for (int i = 0; ...)) is valid C99 and restricts i to the loop body. This is the preferred style in CargoForge-C โ it prevents accidentally reusing a stale loop counter.
while and continue / break¶
CargoForge-C mostly uses for loops with explicit bounds. The continue keyword skips the rest of the current iteration; break exits the loop entirely. You will see continue used as an early-exit guard:
/* from perform_analysis, analysis.c:104 */
for (int i = 0; i < ship->cargo_count; i++) {
const Cargo *c = &ship->cargo[i];
if (c->pos_x < 0) continue; /* skip unplaced cargo */
/* ... accumulate moments ... */
}
The sentinel pos_x < 0 marks cargo that has not been placed in a hold yet. continue cleanly skips those items without nesting the whole body inside an if.
Declaring and calling functions¶
Anatomy of a function¶
- Return type: what value the function hands back to its caller.
voidmeans "nothing". - Parameters: local copies of the data passed in (more on this below).
return: exits the function immediately and delivers the value.
A real example: gz_at_angle¶
The wall-sided formula for the righting lever at angle \(\theta\) is:
Here is the complete implementation from analysis.c:
static float gz_at_angle(float gm, float bm, float theta_deg) {
float theta = theta_deg * (float)M_PI / 180.0f;
float tan_theta = tanf(theta);
return sinf(theta) * (gm + bm * tan_theta * tan_theta / 2.0f);
}
Walk through it:
staticโ the function is private toanalysis.c. Nothing outside this file can call it. This is good hygiene: it keeps the internal physics helpers from leaking into the public API.float gz_at_angle(float gm, float bm, float theta_deg)โ the function takes three floats and returns a float.- Degrees to radians:
<math.h>trigonometric functions work in radians, so the first step converts \(\theta\). tanf,sinf: single-precision versions of \(\tan\) and \(\sin\). Usingtan(double-precision) would silently promote everything todoubleand back.return: the computed value is returned to whoever calledgz_at_angle.
Calling it:
The caller passes two local variables (gm_effective, r.bm) and a literal (30.0f). The function receives them as its own local copies named gm, bm, and theta_deg.
void functions: find_gz_max¶
Some functions do not return a value โ they communicate results by writing through pointer parameters. find_gz_max in analysis.c scans 1โ80ยฐ in 1ยฐ steps to find where GZ peaks:
static void find_gz_max(float gm, float bm, float *max_gz, float *max_angle) {
*max_gz = 0.0f;
*max_angle = 0.0f;
for (float a = 1.0f; a <= 80.0f; a += 1.0f) {
float gz = gz_at_angle(gm, bm, a);
if (gz > *max_gz) {
*max_gz = gz;
*max_angle = a;
}
}
}
The * in float *max_gz means "pointer to float". Writing *max_gz = value stores the value at the memory address the caller provided. This is the standard C idiom for returning more than one result from a function.
Called from perform_analysis:
The & operator takes the address of r.gz_max โ it hands find_gz_max the location in memory where the result should be written. Pointers are covered in detail in a later lesson; for now, read float * as "out-parameter" and &variable as "the address of this variable."
Pass by value¶
C passes arguments by value: the function receives a copy, not the original.
float gm = 1.5f;
float result = gz_at_angle(gm, bm, 30.0f);
/* gm is still 1.5f here โ gz_at_angle cannot change it */
Inside gz_at_angle, the parameter named gm is a separate local variable that starts with the value 1.5f. The function can modify its own gm without affecting the caller's gm. This isolation makes functions easy to reason about โ no side-effects through the parameter list unless a pointer is explicitly passed.
Pass by value with structs
Passing a large struct by value copies every byte of it. perform_analysis takes a const Ship *ship โ a pointer to the Ship โ rather than a Ship ship copy, because copying a Ship (with its cargo array pointer and all other fields) would be wasteful and semantically wrong. You will see const Type * used throughout when "I need to read this, not copy it."
The static keyword on functions¶
gz_at_angle, integrate_gz, and find_gz_max are all declared static. This does not affect their behaviour โ it restricts their visibility to the current translation unit (the .c file). The three functions are implementation details of analysis.c; the only public interface is perform_analysis and print_loading_plan (declared in cargoforge.h). Using static on helpers prevents name collisions across files and communicates clearly that the function is internal.
Recap¶
- C's primitive numeric types are
int,float, anddouble. CargoForge-C usesfloatfor all physics quantities โ adequate precision, half the memory ofdouble. - Literal floating-point constants need an
fsuffix (1.025f) to stayfloat; without it they aredouble. forloops follow the patternfor (init; condition; update). Declaring the loop variable inside the initialiser (int i) keeps it scoped to the loop body.continueskips the rest of the current iteration;breakexits the loop. Both are used as early-exit guards in the cargo accumulation loops.- A function is declared with its return type, name, and parameter list.
staticrestricts it to the current file. - Arguments are passed by value โ functions receive copies. To write back to the caller, pass a pointer (covered in a later lesson).
gz_at_angleinanalysis.cis a clean example: pure function, no side-effects, three floats in, one float out, directly encoding the wall-sided stability formula.
Check yourself¶
Why float instead of double for weights, dimensions, and hydrostatic values?
Real sea-going instruments โ draught gauges, load cells โ don't resolve beyond a few centimetres or half a tonne. float's ~6-7 digit precision already matches the sensor, and the smaller size keeps the Ship and Cargo structs compact and cache-friendly.
Why does perform_analysis call memset(&r, 0, sizeof(r)) before computing anything?
C does not automatically zero local variables. Reading an uninitialised field would be undefined behaviour, so the AnalysisResult is explicitly zeroed first, before any accumulation begins.
What does marking a function static inside a .c file actually restrict?
It confines that function to its own compilation unit โ no other .c file can call it, even if they know its name. That is how helpers like gz_at_angle stay private implementation details instead of part of the program's public surface.