Why this matters
Every C program you write begins the same way, and every program you run reports back a number when it finishes. That number is how scripts, build systems and other programs know whether your program succeeded. make stops a build when a compiler exits with a nonzero status; a shell script using set -e aborts; CI systems mark a job red. A program that prints "error" but exits with 0 is lying to all of them.
The smallest complete program
#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -g -O0 -o prog main.c
$ ./prog
Hello, world!
Line by line:
#include <stdio.h>pastes in the declarations of the standard input/output functions, includingprintf. The angle brackets mean "look in the system include directories".int main(void)begins the definition of a function namedmain.intis its return type: main hands back an integer.(void)is its parameter list: it takes nothing. (Later you will seeint main(int argc, char *argv[]), which receives command-line arguments.){and}delimit the function's body, a block of statements.printf("Hello, world!\n");is a statement: a call toprintfwith one argument, a string literal in double quotes.\nis an escape sequence meaning "newline". Without it the shell prompt would appear on the same line as your output. Every statement ends with a semicolon.return 0;endsmainand gives back 0.
C does not care about indentation or line breaks; they are for humans. This is equally valid and equally horrible:
#include <stdio.h>
int main(void){printf("Hello, world!\n");return 0;}
The #include line is the exception: preprocessor directives must be on their own line.
Who calls main?
You never call main yourself. When the kernel starts your program, it jumps to a small piece of startup code from the C library (on Linux, a function usually called _start). That code sets up the environment, gathers command-line arguments, and then calls your main. When main returns, the startup code takes the return value and passes it to exit, which flushes output buffers, closes files, and asks the kernel to end the process with that status.
kernel starts process
|
v
_start (C library)
| calls
v
main() <-- your code
| returns 0
v
exit(0) flush output, tell kernel "done, status 0"
|
v
shell receives status 0 ($? == 0)
Exit status
In the shell, $? holds the exit status of the last command:
$ ./prog
Hello, world!
$ echo $?
0
Change the program to return 7;, rebuild, and:
$ ./prog
Hello, world!
$ echo $?
7
Conventions:
0means success. This is the only universal rule.- Any nonzero value means failure. Small numbers like 1 and 2 are typical.
- Only the lowest 8 bits are delivered, so statuses range from 0 to 255;
return 256;looks like success to the shell.
<stdlib.h> defines two portable names, EXIT_SUCCESS (0) and EXIT_FAILURE (1 on Linux). They make intent obvious to a reader.
Shells use the status directly:
$ ./prog && echo "it worked"
Hello, world!
it worked
&& runs the second command only if the first exited with 0.
Ending the program from anywhere: exit()
return leaves the current function. In main that ends the program, but inside another function it only goes back to the caller. To end the program from deep inside, call exit:
#include <stdio.h>
#include <stdlib.h>
void fail(void)
{
printf("fatal: giving up\n");
exit(EXIT_FAILURE); /* never returns */
}
int main(void)
{
printf("step 1\n");
fail();
printf("step 2\n"); /* never reached */
return 0;
}
$ ./prog
step 1
fatal: giving up
$ echo $?
1
exit runs the same cleanup that returning from main does: it flushes buffered output and closes open files. (There is also _exit, which skips that cleanup; you will meet it in the systems module.)
Functions in one paragraph
You have now defined two functions, main and fail. A function definition has a return type, a name, a parameter list in parentheses, and a body in braces. void as a return type means "returns nothing"; void in the parentheses means "takes nothing". A function must be declared before it is called, which is why fail is written above main (or declared above it with a prototype, as you saw in the previous lesson). Functions get a whole module later; for now this is enough to read the examples.
Common mistakes
- Forgetting the semicolon: The compiler points at the next token,
return, because that is where it realized the statement had not ended. Look at the end of the previous line.main.c:5:5: error: expected ';' before 'return' 5 | return 0; | ^~~~~~ - Writing
void main(). Some old books do this. It is not standard C for hosted programs;mainmust returnint. gcc warns:return type of 'main' is not 'int'. - Misspelling main (for example
int mian(void)). Every function compiles, but the linker cannot findmain: Note who reports it:_start, the startup code, wanted to callmain./usr/bin/ld: /usr/lib/x86_64-linux-gnu/Scrt1.o: in function `_start': (.text+0x1b): undefined reference to `main' - Printing an error but returning 0. Scripts will think the program succeeded. Make the status match what happened.
- Using
exitwithout#include <stdlib.h>. GCC 14 rejects this withimplicit declaration of function 'exit'(and helpfully suggests the header).
Recap
- Every C program has exactly one
int main(void)(or theargc/argvform), called by the C library's startup code. - The value
mainreturns becomes the process's exit status; 0 is success, nonzero is failure, and only 0 to 255 survive. - Falling off the end of
mainreturns 0, but sayreturn 0;anyway. exit(status)ends the program from any function, after flushing output.EXIT_SUCCESSandEXIT_FAILUREcome from<stdlib.h>.
Going further: run man 3 exit in the sandbox terminal and read the description. Then try ./prog; echo $? with a few different return values, including 256 and -1.