Last updated: March 2025

The main() Function in C: Role, Signatures, argc/argv & Exit Codes Explained

By CoodeVerse Editorial Team ⏱ 10 min read

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() is just a function. Apart from being called first, 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.

OS loads the executableThe OS reads the ELF/PE/Mach-O binary from disk into memory, sets up the process's virtual address space, and maps code, data, and BSS segments.
OS
C runtime startup (crt0/crt1)The first code to run is the C runtime startup code. It initializes the stack pointer, sets up the heap allocator, zeroes the BSS segment (uninitialized globals), initializes explicitly initialized global variables, and sets up stdin/stdout/stderr.
C runtime
Environment and arguments preparedargc, argv[], and envp[] (environment variables) are assembled from the OS-provided data and passed as parameters to main().
C runtime
main() is calledThe C runtime calls main(argc, argv). Your code runs from here.
Your code
main() returnsThe C runtime receives your return value, flushes stdio buffers, calls atexit() handlers in reverse order, and runs any cleanup registered with the runtime.
C runtime
OS cleans up the processThe OS receives the exit code, reclaims all memory, closes file descriptors, and marks the process as terminated.
OS
Why global variables are initialized before main(): The C runtime's job includes initializing all global and static variables before calling main(). Zero-initialized globals go into the BSS segment (the runtime zeroes it). Explicitly initialized globals (like 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.

Signature 1 — no arguments
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.
Signature 2 — command-line arguments
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.
both valid signaturesC
/* 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

argc
4
argv[0]
./calc← program name
argv[1]
"10"← first argument (it's a string, not an int!)
argv[2]
"+"
argv[3]
"5"
argv[4]
NULL← always NULL sentinel
real-world argc/argv usage — a simple calculator CLIC
#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;
}
terminalShell
./calc 10 + 5
10 + 5 = 15

./calc 20 - 8
20 - 8 = 12

./calc
Usage: ./calc <num> <op> <num>
All argv values are strings. Even numbers passed on the command line arrive as strings — 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

iterate over argv using both stylesC
/* 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 codeMeaningReal-world use
0SuccessProgram completed normally. Shell scripts and CI pass.
1General errorMost programs return 1 for unspecified failures.
2Misuse of shell command / invalid usageUsed by bash builtins; also common for "wrong arguments" errors.
126Command found but not executableOS-generated; not typically returned by programs.
127Command not foundOS-generated by shells.
128+nFatal signal nE.g., exit code 139 = killed by signal 11 (SIGSEGV / segfault).
EXIT_SUCCESSMacro for 0From <stdlib.h> — readable alternative to literal 0.
EXIT_FAILUREMacro for 1From <stdlib.h> — readable alternative to literal 1.
checking exit codes in shell scripts and CIShell
# 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
Use EXIT_SUCCESS and EXIT_FAILURE. Instead of magic numbers, use the macros from <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:

  1. 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.
  2. stdio buffers are flushed — all open FILE streams are flushed. This is why printf output always appears even without an explicit fflush(stdout) — the runtime flushes it at exit.
  3. Open FILE streams are closed — including stdin, stdout, and stderr.
  4. The exit code is passed to the OS — the integer returned from main() becomes the process exit status.
atexit() — register cleanup functionsC
#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.

main() in embedded C — bare metal microcontrollerEmbedded C
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
}
The return type is still 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

Using void main() instead of int main()
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.
✅ Fix: Always use int main() or int main(void).
Forgetting to check argc before accessing argv
Accessing 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.
✅ Fix: Always validate argc before reading argv elements. Print a usage message and return 1 if required arguments are missing.
Treating argv values as numbers directly
argv arguments are always strings. Using argv[1] directly as an integer (e.g. in arithmetic) is a type error. The string "42" is not the number 42.
✅ Fix: Convert with atoi() for integers or strtod() for doubles. Use strtol() for robust conversion with error detection.
Omitting return 0 and expecting it to be implicit
In C99 and later, falling off the end of main() without a return statement implicitly returns 0. But this is unclear to readers and some static analysis tools flag it. Relying on implicit return is a bad habit.
✅ Fix: Always explicitly write return 0; or return EXIT_SUCCESS; at the end of main().
Defining main() in a header file
If 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".
✅ Fix: 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().
No. 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().
Before 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.
No. If two .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().

Continue learning C on CoodeVerse

CoodeVerse Editorial Team

The CoodeVerse editorial team consists of experienced software developers and educators specializing in C, Python, Java, and web development. All content is technically reviewed and updated regularly.