C Basics · beginner · ~5 min

The main function

- Identify where every C program begins and understand that the runtime, not you, calls `main`. - Write both standard signatures: `int main(void)` and `int main(int argc, char **argv)`. - Explain what the value returned from `main` means and how a parent process reads it. - Read command-line arguments through `argc` and `argv`, including why `argv[0]` is the program name. - Use `return 0`, `EXIT_SUCCESS`, and `EXIT_FAILURE` correctly to signal how a program ended. - Recognise and avoid non-standard forms such as `void main()`.

Overview

Every C program you write needs one special starting point, and that starting point is a function named main. When you type a command in a terminal, double-click a program, or let a build pipeline run your executable, the operating system loads your program into memory and then jumps to main. Everything your program does flows out from there.

In the previous lesson, Compiling with gcc, you turned a source file into an executable and ran it. That executable had to start somewhere — and that somewhere is always main. This lesson zooms in on that entry point: how it is written, why it returns a number, and how it can receive information from the outside world.

In plain language, think of main as the front door of your program. The system knocks once, your program answers, does its work, and finally reports back whether things went well or badly. That final report is a small number called the exit status. In the terminology you will meet here: the C runtime (startup code the compiler adds around your program) calls main; the number main returns becomes the exit status; and the optional parameters argc and argv let your program read command-line arguments — the extra words typed after the program's name.

Why it matters

The main function is the contract between your program and everything that runs it. Getting it right matters far beyond a single exercise:

  • Automation depends on exit status. Shell scripts, make, CI pipelines, Docker health checks, and cron jobs all decide what to do next by reading the number your program returns. A backup script that ignores exit codes might silently "succeed" while your data never copied.
  • Command-line arguments make programs reusable. A tool that reads argv can process any file you name instead of being hard-wired to one. Nearly every Unix utility — cp, grep, gcc itself — is built on exactly this mechanism.
  • Portability and correctness. Non-standard forms like void main() may compile on one machine and break on another. Knowing the two legal signatures means your code behaves the same everywhere.

Master main and you understand how your program plugs into the larger system around it.

Core concepts

1. The program entry point

Definition: The entry point is the single function where execution of your program begins. In a standard hosted C program, that function is main.

Plain-language explanation: You never write a line that says "start here." Instead, C reserves the name main for that purpose. When your executable launches, control lands on the first statement inside main and runs downward from there.

How it works internally: When you compile, the toolchain links in a tiny piece of startup code (often called crt0 or the C runtime). The operating system loads your executable and jumps to that startup code first. The startup code sets up the stack, initialises global variables, prepares argc/argv, and then calls main. When main returns, the startup code hands the return value to the system and tidies up.

  OS launches program
        |
        v
  [ C runtime / crt0 ]   <-- sets up stack, globals, argc/argv
        |  calls
        v
  +---------------------+
  |   int main(...)     |  <-- YOUR code runs here
  +---------------------+
        |  returns N
        v
  [ C runtime ]  -->  exit status N handed to the OS

When to use / when not to: You always define exactly one main in a normal program. You do not define main when writing a library meant to be linked into another program — libraries have no entry point of their own.

Common pitfall: Trying to call main yourself from inside your code. It is legal in C but almost always a mistake; recursion into main is a sign you actually wanted a separate helper function.

Knowledge check: In your own words, who calls main, and what has already happened by the time your first line inside main runs?

2. The return value and exit status

Definition: The exit status is a small integer your program reports to whatever started it. The value returned from main becomes that status.

Plain-language explanation: By universal convention, 0 means "everything went fine" and any non-zero value means "something went wrong." Different non-zero numbers can signal different kinds of failure.

How it works internally: The status is not a full int on most systems — only the low 8 bits (0–255) survive. A shell that reports the status computes it as value & 0xFF, i.e. modulo 256. So return 256; is reported as 0, and return -1; is reported as 255.

  return 0    -> status 0    (success)
  return 1    -> status 1    (generic failure)
  return 256  -> status 0    (256 % 256 == 0  -- surprise!)
  return -1   -> status 255  (wrapped into a byte)

When to use / when not to: Return 0 (or EXIT_SUCCESS) on success and a non-zero code (or EXIT_FAILURE) on failure. Do not invent large or negative codes expecting them to pass through unchanged — they get truncated to a byte.

Common pitfall: Returning a computed count (like the number of errors) and assuming the shell sees the exact number. If there are 300 errors, return 300; reports 44, which looks like a small, wrong count.

Return statement Meaning Status the shell sees
return 0; Success 0
return EXIT_SUCCESS; Success (portable) 0
return 1; Generic failure 1
return EXIT_FAILURE; Failure (portable) usually 1
return 256; Truncated 0
return -1; Truncated 255

Knowledge check (predict the output): After running a program that ends with return 42;, you type echo $? in the shell. What does it print? What about after return 257;?

3. The two standard signatures

Definition: C defines exactly two portable forms of main: int main(void) and int main(int argc, char **argv).

Plain-language explanation: Use the first when your program needs no input words from the command line. Use the second when you want to read what the user typed after the program name.

How it works internally: argc (argument count) is the number of words, including the program name. argv (argument vector) is an array of C strings; argv[0] is the program name, argv[1] the first real argument, and so on. The array is always terminated by a NULL pointer at argv[argc].

  Command typed:   ./greet Alice 42

  argc = 3

  argv ---> [0] --> "./greet\0"
           [1] --> "Alice\0"
           [2] --> "42\0"
           [3] --> NULL      <-- sentinel, always present

When to use / when not to: Prefer int main(void) for small demos with no arguments — it documents that none are expected. Use the argc/argv form for real tools. Avoid mixing them up: reading argv[1] in a void program is not possible because the parameters do not exist.

Common pitfall: Accessing argv[1] without first checking argc >= 2. If the user ran the program with no arguments, argv[1] is out of bounds and reading it is undefined behaviour.

Signature Use when Access to arguments
int main(void) No command-line input needed None
int main(int argc, char **argv) You want command-line arguments argv[0]..argv[argc-1]

Knowledge check (find the bug): A program does printf("%s\n", argv[1]); as its very first statement. Under what condition does this crash, and what one check would prevent it?

Syntax notes

The two legal shapes of main, annotated:

#include <stdlib.h>   /* for EXIT_SUCCESS / EXIT_FAILURE */

/* Form 1: no command-line arguments */
int main(void) {
    /* ... your code ... */
    return 0;              /* 0 == success */
}

/* Form 2: with command-line arguments */
int main(int argc, char **argv) {
    /* argc : number of arguments, including program name        */
    /* argv : array of C strings; argv[0] is the program name    */
    /* argv[argc] is always NULL                                 */
    return EXIT_SUCCESS;   /* portable name for 0 */
}

Key points:

  • The return type is always int — never void.
  • char **argv and char *argv[] mean the same thing in this parameter position.
  • You may omit return 0; in main (C99 and later add it implicitly), but writing it is clearer.
  • EXIT_SUCCESS and EXIT_FAILURE come from <stdlib.h>.

Lesson

Where a C program starts

Every C program begins at a single function named main.

You do not call main yourself. The C runtime — the small amount of startup code added around your program — calls it for you when the program launches.

What main returns

The value that main returns becomes the program's exit status: a number the operating system hands back to whatever started the program.

  • 0 means success.
  • Any non-zero value signals a failure.

The parent process — the program that launched yours, such as your shell, the system's init process, or a CI pipeline — reads this status to decide whether the run worked.

The two standard signatures

C defines two valid forms for main:

  • int main(void) — use this when the program takes no command-line arguments.
  • int main(int argc, char **argv) — use this when you want command-line arguments.

In the second form:

  • argc ("argument count") is how many arguments were passed.
  • argv ("argument vector") is the array holding them.
  • argv[0] is the program's own name.

Code examples

#include <stdio.h>
#include <stdlib.h>

/*
 * Sums the integer arguments passed on the command line.
 * Usage:  ./sumargs 3 10 -2
 */
int main(int argc, char **argv) {
    if (argc < 2) {
        /* No numbers given: report usage and fail. */
        fprintf(stderr, "usage: %s <int> [int ...]\n", argv[0]);
        return EXIT_FAILURE;      /* non-zero: signals misuse */
    }

    long sum = 0;
    for (int i = 1; i < argc; i++) {   /* start at 1: skip program name */
        char *end;
        long n = strtol(argv[i], &end, 10);   /* parse each argument */
        if (*end != '\0' || end == argv[i]) {  /* leftover chars = not a number */
            fprintf(stderr, "error: '%s' is not an integer\n", argv[i]);
            return EXIT_FAILURE;
        }
        sum += n;
    }

    printf("sum = %ld\n", sum);
    return EXIT_SUCCESS;          /* 0: success */
}

What it does: The program reads every word after its own name, converts each to an integer with strtol, adds them up, and prints the total. It validates its input and reports failure through both a message on stderr and a non-zero exit status.

Expected output:

$ ./sumargs 3 10 -2
sum = 11
$ echo $?
0
$ ./sumargs
usage: ./sumargs <int> [int ...]
$ echo $?
1
$ ./sumargs 3 hello
error: 'hello' is not an integer
$ echo $?
1

Key edge cases: Running with no arguments triggers the usage message and EXIT_FAILURE. A non-numeric argument (like hello) is rejected. Very large sums are held in a long to reduce overflow risk, and strtol's end pointer is checked so "12x" is not silently accepted as 12.

Line by line

  1. #include <stdio.h> / #include <stdlib.h> — pull in printf/fprintf and strtol/EXIT_*.
  2. int main(int argc, char **argv) — the runtime calls this, filling argc with the argument count and argv with the array of strings.
  3. if (argc < 2) — if the user typed only the program name, there are no numbers to add; we bail out early.
  4. fprintf(stderr, "usage: %s ...", argv[0]) — the usage line goes to standard error, not standard output, so it does not pollute piped results. argv[0] is the program's own name.
  5. return EXIT_FAILURE; — hands a non-zero status back to the runtime, which passes it to the shell.
  6. long sum = 0; — the running total, wide enough to hold big sums.
  7. for (int i = 1; i < argc; i++) — the loop starts at 1, deliberately skipping argv[0] (the program name).
  8. strtol(argv[i], &end, 10) — converts the string to a base-10 long; end is set to the first character it could not parse.
  9. if (*end != '\0' || end == argv[i]) — if there are leftover characters, or nothing was parsed at all, the argument is not a clean integer, so we fail.
  10. sum += n; — accumulate the valid value.
  11. printf("sum = %ld\n", sum); — report the result on standard output.
  12. return EXIT_SUCCESS; — status 0, telling the caller everything worked.

Trace for ./sumargs 3 10 -2:

Step i argv[i] n sum
enter loop 1 "3" 3 3
next 2 "10" 10 13
next 3 "-2" -2 11
loop ends 11

Final line prints sum = 11, and main returns 0.

Common mistakes

Mistake 1: Declaring void main().

void main() {          /* WRONG: non-standard */
    printf("hi\n");
}

Why it is wrong: the C standard requires main to return int. Some compilers tolerate void main, but the exit status becomes garbage, and the code is not portable.

Corrected:

int main(void) {
    printf("hi\n");
    return 0;
}

How to prevent it: always start from int main(void) or int main(int argc, char **argv). Enable -Wall -Wextra and the compiler will warn you.

Mistake 2: Reading argv[1] without checking argc.

int main(int argc, char **argv) {
    printf("%s\n", argv[1]);   /* WRONG if run with no argument */
    return 0;
}

Why it is wrong: with no argument, argc is 1 and argv[1] is the NULL sentinel or out of bounds; dereferencing it is undefined behaviour and often crashes.

Corrected:

int main(int argc, char **argv) {
    if (argc < 2) {
        fprintf(stderr, "usage: %s <name>\n", argv[0]);
        return EXIT_FAILURE;
    }
    printf("%s\n", argv[1]);
    return 0;
}

How to recognise it: crashes only when you forget the argument. Always guard array accesses with an argc check.

Mistake 3: Returning a large or computed number as a status.

return error_count;   /* if error_count == 300, shell sees 44 */

Why it is wrong: only the low byte survives, so the reported code is misleading. Corrected: return EXIT_FAILURE (or a small fixed code) on failure and print the real count instead.

Mistake 4: Confusing assignment and comparison in the guard. Writing if (argc = 2) assigns 2 to argc and is always true. Use ==. Compilers warn about this with -Wall.

Debugging tips

Compiler errors and warnings:

  • warning: return type of 'main' is not 'int' — you wrote void main; change it to int main.
  • warning: control reaches end of non-void function — add a return (or rely on the implicit return 0 only if intentional).
  • warning: 'argv' undeclared / too few arguments — you used argv but declared int main(void); switch to the argc/argv signature.
  • warning: suggest parentheses around assignment used as truth value — you wrote = where you meant ==.

Runtime errors:

  • Segmentation fault right at startup usually means you dereferenced argv[i] for an i that does not exist. Print argc first, or run under a debugger.
  • Under gdb: compile with -g, run gdb ./prog, then run arg1 arg2, and use backtrace after a crash to see the offending line.
  • Under valgrind ./prog you will see "Invalid read" messages pointing at out-of-bounds argv access.

Logic errors:

  • Exit status looks wrong? Check it directly with echo $? right after running. Remember it is truncated to 0–255.
  • Loop skips or double-counts the program name? Confirm the loop starts at i = 1, not 0.

Questions to ask when it does not work: Did I check argc before indexing argv? Is my return type int? Am I returning a value in 0–255? Did I send error messages to stderr and results to stdout?

Memory safety

argv is one of the first arrays beginners touch, so the memory rules matter:

  • Bounds: Valid indices are argv[0] through argv[argc-1]. argv[argc] is guaranteed to be NULL, but reading argv[argc+1] or any index past argc when you have not verified it is undefined behaviour. Always gate access with an argc check.
  • Lifetime and ownership: The strings in argv are provided by the runtime and live for the whole program. You do not own or free them — never call free(argv[i]). If you need a modified copy, allocate your own buffer.
  • Immutability in practice: Although you can modify the characters in argv strings, treat them as read-only input. Overwriting them can corrupt what tools like ps display and invites bugs.
  • Initialisation: argc and argv are always set up by the runtime before main runs — you do not initialise them, but everything you declare inside main (like sum) must be initialised before use.
  • Integer overflow at the boundary: The exit status is only a byte. Returning values outside 0–255 silently wraps (modulo 256), which is a data-loss bug even though it is not a memory bug. Keep status codes small.
  • Untrusted input: Command-line arguments come from outside your program. Validate them (as the example does with strtol and the end check) before using them as sizes, indices, or format strings. Never pass argv[i] directly as a printf format string — that is a format-string vulnerability; always use printf("%s", argv[i]).

Real-world uses

Concrete use case: Almost every Unix command-line tool is a main that reads argv. When you run gcc hello.c -o hello, gcc's main receives argc == 4 and inspects argv to learn the input file, the -o flag, and the output name. Its exit status is what make checks to decide whether to keep building. Backup scripts, deployment pipelines, and test runners all branch on that same status.

Professional best-practice habits:

  • Beginner: Always use int main, always check argc before touching argv, return 0 on success and non-zero on failure, and print a helpful usage line when arguments are missing.
  • Advanced: Send diagnostics to stderr and real output to stdout so your tool composes cleanly in pipelines. Use EXIT_SUCCESS/EXIT_FAILURE for portability. Adopt distinct small exit codes for distinct failure categories and document them. For anything beyond a couple of flags, parse arguments with getopt/getopt_long instead of hand-rolled indexing. Validate and sanitise every argument, since it is untrusted input.

Good naming, early argc guards, clean resource cleanup before each return, and clear error messages are what separate a toy program from a tool other people can rely on.

Practice tasks

Beginner 1 — Hello with a name. Write int main(void) that prints a fixed greeting and returns 0. Then convert it to int main(int argc, char **argv) and print argv[0] (the program name) as well. Concepts: entry point, both signatures, return 0. Hint: run it as ./prog and note that argv[0] is whatever path you typed.

Beginner 2 — Success or failure switch. Write a program that takes exactly one argument: ok or fail. If it is ok, print ok and return EXIT_SUCCESS; if it is fail, print nothing and return EXIT_FAILURE; otherwise print a usage message and also fail. Verify with echo $? after each run. Concepts: argc check, exit status, EXIT_SUCCESS/EXIT_FAILURE. Requirement: must reject the wrong number of arguments.

Intermediate 1 — Argument echo. Print each command-line argument on its own line, numbered starting at 1, skipping the program name. Example: ./echoargs a b prints 1: a then 2: b. Return 0. Constraints: must work for zero arguments (print nothing, still succeed). Concepts: looping over argv from index 1, argc bound.

Intermediate 2 — Exit code from count. Read one integer argument n and exit with a status that equals n clamped into 0–255 (wrap with modulo 256, matching how the shell reports it). Print nothing. Confirm with echo $?. Concepts: return value as status, byte truncation, strtol parsing. Hint: compute ((n % 256) + 256) % 256 to handle negatives cleanly.

Challenge — Mini calculator. Accept exactly three arguments: a number, an operator (+, -, x), and another number, e.g. ./calc 6 x 7. Print the result and return 0; on any bad input (wrong count, unknown operator, non-numeric operand, or division-like misuse) print an error to stderr and return EXIT_FAILURE. Constraints: use strtol with the leftover-character check; validate before computing. Concepts: full argc/argv validation, distinct success/failure statuses, stderr vs stdout. Hint: use x (not *) for multiply so the shell does not expand it into filenames.

Summary

  • Entry point: every C program starts in main, which the C runtime calls for you after setting up the stack, globals, and argc/argv.
  • Two legal signatures: int main(void) for no arguments, int main(int argc, char **argv) to read command-line arguments; the return type is always int, never void.
  • Exit status: the value main returns is the program's exit status — 0 (or EXIT_SUCCESS) means success, non-zero (or EXIT_FAILURE) means failure. Only the low byte (0–255) survives, so keep codes small.
  • Arguments: argc counts the words including the program name; argv[0] is that name, real arguments start at argv[1], and argv[argc] is NULL. Always check argc before indexing argv.
  • Common mistakes: void main, reading argv[1] without an argc guard, returning oversized status codes, and treating command-line input as trusted.
  • Remember: results to stdout, errors to stderr, validate every argument, and return a meaningful status so scripts and pipelines can trust your program.

Practice with these exercises