C Basics · beginner · ~8 min
**What you will learn** - Run `gcc` to turn a single `.c` source file into a runnable executable on Linux and macOS. - Name your output binary with `-o` and run it with `./name`. - Turn on the warning flags `-Wall` and `-Wextra` and understand what they catch. - Lock your code to a language standard with `-std=c11` so the compiler behaves predictably. - Recognise the four stages a compiler runs (preprocess, compile, assemble, link) so error messages make sense. - Read and fix the most common compiler errors and warnings a beginner hits.
C source code is just plain text. If you open a .c file you see human-readable words like int, printf, and return. Your CPU cannot run that text directly — the processor only understands machine code, a stream of binary instructions specific to its architecture.
A compiler bridges that gap. It reads your text and translates it into a machine-code executable: a file your operating system can load and run. In the previous lesson, What is C?, you saw that C is a compiled language sitting close to the hardware. This lesson is where that idea becomes a concrete, repeatable command you type yourself.
The tool we use is gcc, the GNU Compiler Collection. It is free, available on almost every Linux distribution, and installable on macOS (where gcc is often the Clang compiler wearing the same name — for our purposes the commands are identical). Every other lesson on this site assumes you can compile and run a program, so this is the foundation the rest of the C track stands on.
A quick vocabulary set you will see throughout:
.c text file.gcc) that translates source into machine code.- that changes how gcc behaves, e.g. -o or -Wall.gcc drives for you behind one command.Compiling is the single action you will repeat hundreds of times while learning C — every change you make has to be recompiled before you can see its effect, so being fluent with gcc directly speeds up everything else.
Beyond convenience, the flags you choose change how safe your program is. C gives you enormous control and, in exchange, very little hand-holding: many dangerous mistakes are legal C that the compiler will silently accept unless you ask it to warn you. Turning on -Wall -Wextra is the cheapest bug-prevention step in the whole language — it catches uninitialised variables, accidental type mismatches, and missing returns before your program ever runs. In professional and security-sensitive code, those same warnings (often promoted to hard errors) are the first line of defence against memory-corruption bugs that attackers exploit. Learning to compile cleanly now builds the habit that keeps real software correct later.
Definition. A compiler is a program that translates source code in one language (C) into another (machine code) that a target CPU can execute.
Plain language. Think of gcc as a translator that takes your English-like recipe and rewrites it into the CPU's native binary language, once, ahead of time. After that, the result runs on its own without gcc present.
How it works internally. gcc hello.c -o hello looks like one step, but gcc quietly runs four stages in order:
hello.c (your text)
| 1. Preprocess -> expand #include, #define, strip comments
v
hello.i (expanded C source)
| 2. Compile -> translate to assembly for your CPU
v
hello.s (assembly text)
| 3. Assemble -> turn assembly into machine code
v
hello.o (object file: machine code, not yet runnable)
| 4. Link -> combine with the C library, resolve names
v
hello (executable you can run with ./hello)
When to care. For a single small file you never see the intermediate files — gcc deletes them. But the names matter because error messages come from different stages: a compile error means stage 2 rejected your code; a linker error ("undefined reference to ...") means stage 4 could not find a function you called.
Pitfall. Beginners often confuse the two. undefined reference to 'sqrt' is not a typo in your code — it usually means you forgot to link the math library (-lm). Knowing which stage failed tells you where to look.
Knowledge check: In the diagram above, which stage produces
hello.o, and ishello.osomething you can run with./hello.o? (Answer: stage 3, assemble; no — an object file is not yet linked, so it is not runnable.)
-oDefinition. -o name tells gcc to write the final executable to a file called name.
Plain language. Without -o, gcc falls back to a historical default name: a.out. So gcc hello.c silently creates a.out, and gcc world.c overwrites that same a.out. Naming your output keeps each program distinct and findable.
Structure. The name after -o is just a filename — hello, build/app, mytool are all fine. On Linux/macOS executables do not need a .exe extension.
When NOT to. There is no real downside to always using -o; the only time you skip it is a throwaway one-liner test where you do not care about the name.
Pitfall. Do not write gcc -o hello.c hello — that asks gcc to overwrite your source file hello.c with the compiled output. The argument right after -o is the destination. Many beginners destroy their own source this way. The safe pattern is source first, -o output last.
Knowledge check: What file does
gcc program.c(with no-o) create, and what happens if you rungcc other.cright after? (Answer: it createsa.out; the second command overwrites that samea.out.)
-Wall and -WextraDefinition. Warning flags ask the compiler to report suspicious-but-legal code without stopping the build.
Plain language. A warning is the compiler saying "this compiles, but it looks like a bug." -Wall turns on a large set of the most useful checks (despite the name, it is not literally all warnings). -Wextra adds a further batch that -Wall leaves off.
What they catch. Common examples:
return.When to use. Always, from your very first program. Treat a warning as a bug you have not understood yet.
When NOT to. You almost never want them off. Advanced builds sometimes silence one specific warning deliberately, but a beginner should keep both on.
Pitfall. Warnings scroll past and the program still produces hello, so it is tempting to ignore them. That habit hides real bugs. Read every warning; aim for a clean, warning-free build.
Knowledge check: True or false — if
gccprints a warning but still creates the executable, the program is guaranteed correct. (Answer: false; a warning means the code is suspicious, and the run may still misbehave.)
-std=c11Definition. -std= selects which version of the C language the compiler enforces.
Plain language. C has evolved over decades (C89, C99, C11, C17, ...). Different gcc versions default to different standards, so the same code can behave differently on two machines. Pinning -std=c11 makes the rules explicit and consistent everywhere your code is built.
Structure. It is one flag with the standard name attached: -std=c11, -std=c99, -std=c17. For this course we use c11.
When NOT to. If a project mandates a different standard, match the project. Otherwise stick with one standard across all your files so features and behaviour stay predictable.
Pitfall. Relying on the compiler default means a program that builds on your laptop may fail or warn on a classmate's older compiler. Always state the standard.
The general shape of a one-file compile command is:
gcc [flags] source.c -o output
A fully equipped beginner command, annotated:
gcc \
-std=c11 \ # use the C11 language standard
-Wall -Wextra \ # turn on the useful warning sets
-O2 \ # optimise the generated code (optional)
hello.c \ # the source file to compile
-o hello # name of the executable to produce
Then run it:
./hello # the ./ means "run the program in this directory"
Key points: flags can appear in any order, but the filename right after -o is the output (never your source). The ./ before the program name is required on Linux/macOS because the current directory is normally not on your search PATH.
C source code is just text. By itself, your CPU cannot run it.
The gcc program (the GNU C Compiler) reads that text and translates it into a machine-code executable — a file your CPU can run directly.
The first command most people learn is:
gcc hello.c -o hello
This compiles hello.c and produces a file named hello. The -o flag (short for output) sets that name. You then run the program with:
./hello
Add -Wall -Wextra to enable a set of useful warnings. These flags catch most beginner bugs, including:
return statementsAdd -std=c11 to lock your code to the modern C dialect (the C11 standard). This keeps the compiler's behavior predictable and consistent.
Save this as hello.c:
#include <stdio.h> /* declares printf */
int main(void)
{
int answer = 6 * 7; /* a simple computed value */
printf("Hello, C!\n");
printf("6 times 7 is %d\n", answer);
return 0; /* 0 tells the OS the program succeeded */
}
Compile and run it:
gcc -std=c11 -Wall -Wextra -O2 hello.c -o hello
./hello
What it does. The gcc line preprocesses, compiles, assembles, and links hello.c into an executable named hello, with C11 rules and warnings enabled. Because the code is clean, gcc prints nothing — no news is good news. The ./hello line runs it.
Expected output:
Hello, C!
6 times 7 is 42
Edge cases to know. If you omit -o hello, the executable is named a.out and you run it with ./a.out. If hello.c does not exist or has a typo in the name, gcc reports No such file or directory. If you forget #include <stdio.h>, -Wall will warn about an implicit declaration of printf.
Here is what happens, in order, when you run the two commands.
| Step | What runs | What happens |
|---|---|---|
| 1 | gcc ... hello.c -o hello |
gcc reads the flags: C11 standard, warnings on, optimisation level 2, output name hello. |
| 2 | Preprocess | #include <stdio.h> is replaced by the contents of that header (which declares printf); the comment is stripped. |
| 3 | Compile | The expanded C is translated to assembly for your CPU. 6 * 7 is a constant, so -O2 may compute 42 here at compile time. |
| 4 | Assemble | Assembly becomes machine code in an object file. |
| 5 | Link | The object file is joined with the C standard library so printf resolves to real code; the result is written as hello. |
| 6 | (build done) | No warnings means gcc printed nothing and returned success. The file hello now exists. |
| 7 | ./hello |
The OS loads hello, runs main, which sets answer = 42 and calls printf twice. |
| 8 | return 0; |
main returns 0; the OS sees exit status 0, meaning success. |
The two output lines appear because each printf writes its text to the terminal; %d is replaced by the value of answer, which is 42.
Mistake 1 — Overwriting your source with -o.
Wrong:
gcc -o hello.c hello # destination is hello.c — your source!
Why it is wrong: the argument after -o is the output file. This tells gcc to write the compiled result over hello.c, erasing your code. Corrected:
gcc hello.c -o hello # source first, output last
Prevent it: always read -o X as "create file X", and keep your .c filename away from the -o.
Mistake 2 — Forgetting the ./ when running.
Wrong: typing hello and getting command not found. Why: the shell searches your PATH, which does not include the current folder. Corrected: ./hello. The ./ explicitly says "in this directory".
Mistake 3 — Ignoring warnings.
Wrong: seeing warning: 'x' is used uninitialized but running the program anyway because the executable was still produced. Why it is wrong: the build succeeding does not mean the program is correct; that warning predicts a real bug. Corrected: fix the cause (initialise x), rebuild, and confirm the warning is gone. Prevent it: aim for zero warnings, and once comfortable add -Werror to make warnings stop the build.
Mistake 4 — Linker error mistaken for a code typo.
Wrong: calling sqrt and seeing undefined reference to 'sqrt', then re-reading the spelling. Why: this is a link failure — the math functions live in a separate library. Corrected: add -lm at the end: gcc prog.c -o prog -lm.
Compiler errors (stage 2 — code is rejected, no executable produced):
expected ';' before ... — a missing semicolon, usually on the line above the one named.'x' undeclared (first use in this function) — you used a variable or name you never declared, or misspelled it.implicit declaration of function 'printf' — a missing #include (here <stdio.h>).Linker errors (stage 4 — code compiled but names could not be resolved):
undefined reference to 'main' — you compiled a file with no main as if it were a program; add a main, or this file is meant to be part of a larger build.undefined reference to 'sqrt' (and similar math) — add -lm.Runtime / logic errors (the program builds but misbehaves):
Segmentation fault — the build is fine but the logic or memory use is not. Recompile with -g and run under a debugger (gdb ./hello) to step through.A debugging checklist when it does not work:
gcc print warnings you skipped? Re-read them../hello, not ./a.out)?Compiling itself does not allocate memory, but the flags you pass are your first defence against memory bugs in the code you compile. Because this is a beginner toolchain lesson, focus on these robustness habits:
-Wall -Wextra. They flag uninitialised reads and obvious type mismatches — two frequent sources of undefined behaviour in C.-Werror once your code is clean, so a regression cannot slip through unnoticed.-g while learning so that when a program crashes you can run it under a debugger and see the exact line, rather than guessing.-std=c11) so behaviour does not silently change between compilers — undefined or implementation-defined behaviour is harder to reason about when the language version is fuzzy.As you progress to programs that use arrays and pointers, you can add sanitizers during development: gcc -fsanitize=address,undefined makes many out-of-bounds accesses and undefined-behaviour bugs crash loudly with a clear report instead of silently corrupting memory. Treat that as a tool to reach for in later lessons, but know it exists now.
Concrete use. Essentially all of the GNU/Linux operating system, the Git you use for version control, the SQLite database engine, and countless embedded firmware images are built with gcc (or a compatible compiler invoked the same way). The single-file gcc file.c -o file command you just learned is the same one — scaled up by build systems like make and cmake — that produces production software.
Professional best-practice habits for compiling:
Beginner rules (do these now):
-Wall -Wextra and aim for a warning-free build.-o and run with ./name.-std=c11).Intermediate / advanced habits (grow into these):
-Werror so warnings cannot be ignored, and -g for debug builds.-fsanitize=address,undefined) in development and testing.gcc by hand to a build system (make/cmake) once you have more than a couple of files, so builds are reproducible and incremental.-O0 -g while debugging, -O2 for releases.Beginner 1 — First compile.
Objective: prove your toolchain works. Create hello.c that prints Hello, C! and compile it with gcc -std=c11 -Wall -Wextra hello.c -o hello, then run ./hello. Requirement: the build must produce no warnings. Expected output: Hello, C!. Hint: include <stdio.h>. Concepts: -o, running with ./.
Beginner 2 — Default name detective.
Objective: understand the a.out default. Compile the same program with just gcc hello.c (no -o). Requirement: list your files, find the new executable, and run it. Write down its name. Expected: a file a.out appears and ./a.out prints Hello, C!. Hint: use ls. Concepts: default output name vs. -o.
Intermediate 1 — Make a warning appear, then fix it.
Objective: experience -Wall catching a bug. Write a program that declares int x; (uninitialised) and prints it with printf("%d\n", x);. Compile with -Wall -Wextra. Requirement: capture the warning text, then fix the code so the build is clean. Hint: give x a value before using it. Concepts: warning flags, uninitialised variables.
Intermediate 2 — Treat warnings as errors.
Objective: enforce clean builds. Take any program that still produces one warning and add -Werror. Requirement: observe that the build now fails, then fix the warning until the build succeeds. Constraint: do not remove -Wall -Wextra. Hint: -Werror turns every warning into a hard error. Concepts: -Werror, reading compiler output.
Challenge — Two-file build and a linker error.
Objective: feel the difference between a compile error and a linker error. Split a program into math_util.c (defines int square(int n)) and main.c (calls square). First compile only main.c (gcc -Wall main.c -o app) and read the undefined reference to 'square' linker error. Then compile both together: gcc -Wall -std=c11 main.c math_util.c -o app. Requirement: explain in one sentence why the first command failed at the link stage, not the compile stage. Constraint: warning-free. Hint: each .c compiles fine alone; the missing definition only matters when linking. Concepts: object files, linking, multi-file builds.
gcc translates C source text into a runnable executable by running four stages for you: preprocess, compile, assemble, link.gcc source.c -o name, then run it with ./name. Without -o, the output is a.out..c file right after -o — that overwrites your source.-Wall -Wextra enable warnings that catch common beginner bugs (uninitialised variables, missing returns, type mismatches). A warning means "likely bug", even if the build still succeeds — fix them all.-std=c11 pins the language version so behaviour is consistent across compilers.undefined reference errors come from the link stage (e.g. add -lm for math). Knowing which stage failed tells you where to look../.