C / GETTING STARTED
main, return values and exit codes
Return meaningful exit codes from main, read them back with echo $?, and predict the status the shell reports for values like -1 or 300.
What you will learn
- Signal success or failure from main using return 0 or a small nonzero exit code
- Read a program's exit status with echo $? and branch on it using && and ||
- Predict truncation: only the low 8 bits of the returned value reach the shell
- Use exit() to end the program from inside a helper, and atexit() for cleanup
Understanding main, return values and exit codes
The startup code bundled into your executable is what actually begins running; it prepares the environment and then calls main like any other function. Because it is a call, main can hand a value back, and that value goes straight to exit(), which ends the process using it as the exit status. That is why the standard forms are int main(void) and int main(int argc, char *argv[]) and never void main(): the host environment expects exactly one int in return.
By convention 0 means the program did what it was asked and any nonzero value means it did not. The asymmetry is deliberate: there is only one way to succeed, so one value can stand for it, while the remaining values are free to say which failure occurred. <stdlib.h> provides EXIT_SUCCESS and EXIT_FAILURE for the two common cases, and the shell stores the number in $?, which is what if, && and || actually test. Nothing prints the status for you, and that separation from stdout is exactly why make and CI can trust it while ignoring your log lines.
The channel is narrow. On POSIX systems the parent receives the status through wait(), which carries only the low 8 bits, so return 300; shows up as 44, return -1; shows up as 255, and return 256; collapses to 0 and turns a failure into an apparent success. Returning from main and calling exit() are equivalent when you are in main, since both flush stdio and run atexit handlers, but exit() also works from deep inside a call chain where a return statement would only reach the immediate caller.
<stdio.h>
<stdlib.h>
int main(void)
{
int total = 6;
int parts = 0;
if (parts <= 0) {
fprintf(stderr, "error: parts must be at least 1, got %d\n", parts);
return EXIT_FAILURE; /* $? becomes 1 */
}
printf("%d parts of %d each\n", parts, total / parts);
return EXIT_SUCCESS; /* $? becomes 0 */
}
The int that main returns is not program output but the process exit status: the one small number your program hands back to whatever started it.
Worked examples
Ending the program from a helper with exit()
Shows that exit() terminates the whole process from inside a called function while still running cleanup registered with atexit().
<stdio.h>
<stdlib.h>
static void report(void)
{
printf("shutting down\n");
}
static void open_input(int available)
{
if (!available) {
fprintf(stderr, "input not available\n");
exit(2);
}
printf("input ready\n");
}
int main(void)
{
atexit(report);
open_input(0);
printf("never printed\n");
return 0;
}
Example explained
Line 1atexit(report) records a function to run when the program terminates normally, whichever path gets there.
Line 2exit(2) ends the process from inside open_input, so control never comes back to main and the printf after the call cannot run.
Line 3The 2 in exit(2) becomes the exit status, exactly as if main had executed return 2;.
Line 4Flushing stdio and running atexit handlers happen for exit() and for returning from main, but _Exit() and abort() skip both.
Why the status you return is not the status you see
Demonstrates that only the low 8 bits of the value returned from main survive to the shell.
<stdio.h>
int main(void)
{
int status = 300;
printf("main returns %d\n", status);
printf("the shell will report %d\n", status & 0xFF);
return status;
}
Example explained
Line 1return status; makes the startup code call exit(300) on your behalf.
Line 2The waiting shell receives only the low 8 bits, so 300 arrives as 300 & 0xFF, which is 44.
Line 3The second printf applies the same mask, so the program prints in advance what echo $? will show.
Line 4The same rule turns return -1; into a status of 255, because -1 has every low bit set.
Falling off the end of main
Shows the C99 rule that reaching the closing brace of main is equivalent to returning 0.
<stdio.h>
int main(void)
{
printf("reached the end of main\n");
}
Example explained
Line 1There is no return statement, yet the process exits with status 0: since C99, reaching the closing brace of main behaves like return 0;.
Line 2Compiled as C89 the value handed to the host environment is indeterminate, so such code can report a random failure to a script.
Line 3The shortcut applies only to main; falling off the end of any other non-void function and then using its value is undefined behavior.
Line 4Writing return 0; explicitly costs one line and makes the intended status visible to a reader.
Important notes
$? holds the status of the most recent command only, so read it before running anything else; cmd.exe uses %ERRORLEVEL% and PowerShell uses $LASTEXITCODE.
Shells already assign meanings to 126 (not executable), 127 (not found) and 128+n (killed by signal n), so keep your own failure codes between 1 and 125.
Common mistakes
Printing an error message and then returning 0 anyway: make, CI and shell && all read the status, not your text, so they treat the run as a success and keep going.
Writing return -1; and expecting to see -1: the shell reports 255, and return 256; reports 0, which makes a hard failure look like success.
Declaring void main() or leaving the return type off: it is not standard C, gcc warns about the return type of main, and the program loses its only way to report failure.
Try it yourself
Change, predict, then run
Write a program that registers an atexit handler printing done, prints the value of 130 & 0xFF, and then does return 130; from main. Move the termination into a helper as exit(130) and confirm the printed prediction and the handler output are unchanged.
Open the C workspaceCheck your understanding
A program writes an error to stderr and then executes return 512; from main. What does echo $? print afterwards on Linux, and how does ./prog && next behave?
- 0, so && runs the next command and the failure goes unnoticed
- 512, so && stops and the failure is reported
- 255, so && stops and the failure is reported
- 1, because the shell normalizes every nonzero return to 1
Show answer
The value reaches the shell through wait(), which exposes only the low 8 bits, and 512 & 0xFF is 0, so the shell records a successful run and && continues even though the program failed. 255 is tempting because that is where a negative code such as -1 lands, but 512 is an exact multiple of 256 and collapses to 0; and no shell rewrites nonzero statuses to 1.