The main() Function in C: Role, Signatures, argc/argv & Exit Codes Explained
Every C program you write has one thing in common: it starts at main(). But most
beginner tutorials only show you what main() looks like, not why it
exists, what the OS actually does when it calls it, or what happens to your program after it
returns. This guide covers all of that — from the OS startup sequence to argc/argv in real
command-line tools to the exit code conventions used by every shell and CI system on the planet.
The Role of main(): Why It Exists
When the operating system runs a program, it needs a defined place to start. Different languages
solve this differently — Python executes the script file top-to-bottom, Java looks for
public static void main(String[] args), JavaScript runs from the first line.
C's solution is main() — a function with a specific name that the OS runtime
is designed to call as the first function of your program. It is not special because of any
language magic; it is special because the C runtime startup code (part of the C standard
library) is hardcoded to call a function named main after finishing its own
initialization.
This is why every C program must have exactly one main(): it is the agreed
contract between the C runtime and your code. If you don't define it, the linker reports
"undefined reference to main". If you define it twice, the linker reports "multiple definition
of main". There is exactly one entry point, no exceptions.
main()
follows all the same rules as any other C function. It can call other functions, declare local
variables, use loops and conditionals, and return a value. There is nothing syntactically
special about it beyond its name and required return type.
What Happens Before main() Runs
The path from "the OS launches your program" to "your first line of main() executes" involves several steps most developers never think about. Understanding this sequence explains some otherwise-mysterious C behaviors — like why global variables are already initialized when main() starts.
int x = 5;) are stored in the data segment with their values embedded in
the executable. By the time the first line of main() runs, all global variables are
fully initialized.
The Two Valid Signatures for main()
The C standard (C99, C11, C17, C23) defines exactly two valid forms of main().
Any other form is non-standard, even if some compilers accept it.
int main(void) or int main()Use when the program does not need command-line arguments.
void is more
explicit that no parameters are accepted; () is equally valid in C.
int main(int argc, char *argv[])Use when the program reads arguments from the command line. The parameter names
argc and argv are conventional — the compiler accepts any names.
/* Signature 1 — no command-line arguments needed */
int main(void) {
return 0;
}
/* Signature 2 — receives command-line arguments */
int main(int argc, char *argv[]) {
return 0;
}
/* Also valid — alternative argv declaration syntax */
int main(int argc, char **argv) {
return 0;
}
void main() is not standard C. Some compilers accept it as an
extension, but it violates the C standard and produces undefined behavior — the OS receives
no defined exit code. Always use int main(). GCC will warn about
void main() with -Wall.
argc and argv: Command-Line Arguments
When a user runs a C program from the terminal with arguments, those arguments are delivered
to main() through argc and argv. This is how every
command-line tool you've ever used — gcc, ls, git —
receives its options and file paths.
What argc and argv contain
argc (argument count) is the total number of strings in argv.
It is always at least 1 because argv[0] is always the program's name (or path).
argv (argument vector) is an array of char* pointers, each
pointing to a null-terminated string. argv[argc] is always a NULL pointer —
a guaranteed sentinel you can use to iterate without knowing argc.
Running: ./calc 10 + 5
#include <stdio.h>
#include <stdlib.h> // atoi()
int main(int argc, char *argv[]) {
if (argc != 4) {
fprintf(stderr, "Usage: %s <num> <op> <num>\n", argv[0]);
return 1; // non-zero = failure
}
int a = atoi(argv[1]); // convert string "10" → int 10
char op = argv[2][0]; // first char of argv[2] ("+", "-", etc.)
int b = atoi(argv[3]);
int result;
switch (op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
default:
fprintf(stderr, "Unknown operator: %c\n", op);
return 1;
}
printf("%d %c %d = %d\n", a, op, b, result);
return 0;
}
./calc 10 + 5
10 + 5 = 15
./calc 20 - 8
20 - 8 = 12
./calc
Usage: ./calc <num> <op> <num>
argv[1] when you run ./prog 42 is the string
"42", not the integer 42. Use atoi() to convert to
int, atof() or strtod() for double.
strtol() and strtod() are preferred over atoi() because
they detect invalid input and overflow.
Iterating over all arguments
/* Style 1: index-based */
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
/* Style 2: pointer-based (uses the NULL sentinel) */
for (char **arg = argv; *arg != NULL; arg++) {
printf("%s\n", *arg);
}
Return Values and Exit Codes
The integer main() returns becomes the program's exit code —
a signal sent to the OS when your program finishes. This is one of the most important
interfaces between a C program and the outside world.
| Exit code | Meaning | Real-world use |
|---|---|---|
| 0 | Success | Program completed normally. Shell scripts and CI pass. |
| 1 | General error | Most programs return 1 for unspecified failures. |
| 2 | Misuse of shell command / invalid usage | Used by bash builtins; also common for "wrong arguments" errors. |
| 126 | Command found but not executable | OS-generated; not typically returned by programs. |
| 127 | Command not found | OS-generated by shells. |
| 128+n | Fatal signal n | E.g., exit code 139 = killed by signal 11 (SIGSEGV / segfault). |
| EXIT_SUCCESS | Macro for 0 | From <stdlib.h> — readable alternative to literal 0. |
| EXIT_FAILURE | Macro for 1 | From <stdlib.h> — readable alternative to literal 1. |
# Check last program's exit code
./myprogram
echo $? # Linux/macOS: prints 0 if success, 1 if failure
# Use in a shell script condition
if ./myprogram; then
echo "Program succeeded"
else
echo "Program failed with code $?"
fi
# CI/CD: a non-zero exit code fails the build step
# GitHub Actions, Jenkins, and GitLab CI all use this convention
<stdlib.h>: return EXIT_SUCCESS; and
return EXIT_FAILURE;. They communicate intent clearly and are portable — on
systems where success isn't 0, the macros have the correct value (though in practice every
modern OS uses 0 for success).
What Happens After main() Returns
When main() returns, the C runtime doesn't immediately hand control back to the OS.
It performs an ordered cleanup sequence:
- atexit() handlers run — functions registered with
atexit()are called in reverse registration order (last registered = first called). Use this for cleanup like flushing logs or releasing shared resources. - stdio buffers are flushed — all open
FILEstreams are flushed. This is whyprintfoutput always appears even without an explicitfflush(stdout)— the runtime flushes it at exit. - Open FILE streams are closed — including stdin, stdout, and stderr.
- The exit code is passed to the OS — the integer returned from main() becomes the process exit status.
#include <stdio.h>
#include <stdlib.h>
void cleanup_log() { printf("Log flushed.\n"); }
void cleanup_db() { printf("DB connection closed.\n"); }
int main() {
atexit(cleanup_log); // registered first
atexit(cleanup_db); // registered second
printf("Program running...\n");
return 0;
// Output:
// Program running...
// DB connection closed. ← last registered, first called
// Log flushed.
}
main() in Embedded Systems
In embedded systems programming, the rules around main() are slightly different.
On a bare-metal microcontroller — no operating system, no C runtime startup code — there is
no OS waiting to receive the exit code when main() returns.
For this reason, embedded main() functions almost always contain an infinite loop
— the hardware has no concept of "exit". If main() returned, the CPU would start
executing whatever is in memory after the program — random code, causing undefined behavior
or a processor fault.
int main(void) {
hardware_init(); // set up clocks, GPIO, peripherals
while (1) { // infinite loop — never returns
read_sensors();
update_outputs();
WDT_Kick(); // kick watchdog timer
}
return 0; // never reached, but required by some compilers
}
int on most embedded toolchains
(including arm-none-eabi-gcc) even though the return value is never used. Some embedded
environments use a custom startup that never calls main() with a standard
signature, but following the standard int main(void) form keeps code portable
between embedded and hosted (desktop/server) environments.
Common main() Mistakes
void main() is not valid standard C. It produces undefined behavior and gives the OS no defined exit code. Some compilers accept it as an extension, which makes it worse — it silently compiles but behaves incorrectly.int main() or int main(void).argv[1] without checking that argc >= 2 causes a crash if the user runs the program without arguments — argv[1] would be NULL, and dereferencing it is a segfault.argv[1] directly as an integer (e.g. in arithmetic) is a type error. The string "42" is not the number 42.atoi() for integers or strtod() for doubles. Use strtol() for robust conversion with error detection.return 0; or return EXIT_SUCCESS; at the end of main().main() is defined in a .h file and that header is included in two .c files, the linker will find two definitions of main and fail with "multiple definition of main".main() must be defined in exactly one .c file, never in a .h file.Frequently Asked Questions
main() is the mandatory entry point of every C program — the function the
OS calls when your program starts. It must return int (the exit code) and
can optionally receive command-line arguments through argc and argv.
Every C program must have exactly one main().
return 0 sends the exit code 0 to the operating system, which by POSIX convention
means the program completed successfully. Any non-zero exit code signals failure. Shell scripts
check this with echo $? and CI/CD systems use non-zero exit codes to detect
build or test failures.
argc (argument count) is the number of command-line strings in argv
— always at least 1 because argv[0] is the program name. argv
(argument vector) is an array of C strings where argv[1] is the first
user-provided argument, argv[2] is the second, and so on.
argv[argc] is always NULL. All values are strings — numbers must be converted
with atoi() or strtol().
void main() is not valid according to the ISO C standard. The standard
requires main() to return int. Some compilers accept
void main() as a non-standard extension, but this results in undefined behavior
— the program has no defined exit code. GCC warns about it with -Wall.
Always use int main().
main() runs, the C runtime startup code (crt0/crt1) executes: it sets
up the stack, zeroes the BSS segment (uninitialized globals), initializes explicit global
variables, sets up stdin/stdout/stderr, and assembles argc/argv
from the OS-provided command-line data. This is why global variables are already initialized
when the first line of main() executes.
.c files both define main() and are compiled together,
the linker reports "multiple definition of main" and refuses to produce an executable.
Every C program has exactly one entry point. In a multi-file project, only one designated
source file (typically main.c) contains main().