Why C, and What a Program Really Is
Compiler, Interpreter, and What Happens When You Press Run
In this lesson
- Name the four stages between
hello.cand a running program. - Read an error message and say which stage produced it.
- State the trade-off between a compiler and an interpreter.
David writes eight lines to work out the diagonal of a square. He includes <math.h>, calls sqrt, presses Run, and gets this back: undefined reference to 'sqrt'. He stares at it. He did include the header.
He did not write a function called sqrt, so something else is complaining. It is not the part of the compiler that reads his code.
David is meeting the fact that "compiling" is not one program. It is four programs in a row, each with its own job and its own complaints. Once you know which one is talking, most error messages stop being frightening.
The translator and the interpreter
Imagine a technical manual written in Japanese that you need in Bangla.
Option A, the compiler. A translator takes the whole manual and works for a week. You get a complete Bangla book. The translation happens once, so reading it afterwards is as fast as reading any book. A grammar mistake in the original is caught during that week, before you read a page.
Option B, the interpreter. A human interpreter stands beside you and translates line by line as you read. You can start immediately. Every reading is slower, and a mistake on page 400 is only found when you reach page 400.
C is Option A. Python, JavaScript and PHP are closer to Option B. That one choice explains most of the differences you will meet.
The four stages of compiling a C program
When you compile hello.c, four separate programs run one after another. Each takes a file and produces a new one.
The fourth stage is the one that caught David. The compiler was happy: <math.h> promised that a function named sqrt exists somewhere. The linker's job is to find the actual machine code for it. On many Linux systems the maths part of the library is a separate file. You ask for it by adding -lm to the command.
Note
A header like <math.h> is a promise, not the code. It says "a function called sqrt takes a double and returns a double". The code itself lives in the library. This split is why stage 1 and stage 4 can disagree.
Why this matters when you see an error
Every message below is real output from GCC. Read the table as a lookup: find the shape of your message, and it tells you who is speaking.
| What the message says | Stage | Usual cause |
|---|---|---|
fatal error: stdioo.h: No such file or directory | Preprocessor | The header name is misspelled, or not installed |
error: expected ';' before 'return' | Compiler | A missing semicolon on the line above the one named |
error: 'count' undeclared (first use in this function) | Compiler | A name used before it was declared, often a typo |
warning: implicit declaration of function 'squareOf' | Compiler | A function called with no prototype above it |
warning: initialization of 'int' from 'char *' | Compiler | A value of the wrong type put into a variable |
warning: format '%s' expects argument of type 'char *' | Compiler | The printf placeholder does not match the value |
warning: unused variable 'total' | Compiler | Declared and never used, often a half-finished edit |
undefined reference to 'area' | Linker | A function was declared and promised, but never written |
undefined reference to 'sqrt' | Linker | The maths library was not linked; add -lm |
Segmentation fault | Runtime | It built fine, then touched memory it does not own |
A version note. The two middle rows, the missing prototype and the wrong type, are warnings on GCC 12 and errors on GCC 14.
Two things are worth noticing. A warning does not stop the build, and it is usually right anyway. And the last row has no compiler in it at all. That brings us to the most important sentence in this lesson.
"It compiles" is not "it is correct"
The compiler checks grammar, not meaning. It will happily build a program that gives the wrong answer every single time. You will see one in Example 3 below.
The gcc command line
gcc -std=c17 -Wall -Wextra hello.c -o hello
gccis the program that runs all four stages for you.-std=c17picks the language standard. This track uses C17.-Wall -Wextraturns on the useful warnings. Make this a habit on day one.hello.cis your input file.-o hellonames the output. Leave it out and you get a file calleda.out.
Seven lines. This is the file that becomes four files on its way to running.
#include <stdio.h>
int main(void)
{
printf("Hello, C.\n");
return 0;
}
Hello, C.
Note what is not here. There is no code for printf. Your file only says "call it". Stage 4 is where that promise is kept.
This is the program from the first paragraph, written out in full.
#include <math.h>
#include <stdio.h>
int main(void)
{
double side = 2.0;
printf("The diagonal is %.4f\n", side * sqrt(2.0));
return 0;
}
The diagonal is 2.8284
In the Playground this runs as written. On a Linux machine you may need gcc david.c -o david -lm, because there the maths code sits in a separate library file. Same source, same compiler, different linker instruction.
No warnings. No errors. A confident, incorrect answer.
#include <stdio.h>
int main(void)
{
int marks[2] = {7, 8};
int average = (marks[0] + marks[1]) / 2;
printf("Average of 7 and 8 is %d\n", average);
return 0;
}
Average of 7 and 8 is 7
The average of 7 and 8 is 7.5. Dividing one whole number by another in C throws away the part after the point. The compiler has no opinion about this, because nothing is ungrammatical. You will fix this properly in Module 4.
Run in CompilerCompiler and interpreter, side by side
| Compiler (C, C++, Rust, Go) | Interpreter (Python, JavaScript, PHP) | |
|---|---|---|
| When translation happens | Once, before running | Every time, while running |
| Speed of the finished program | Fast; it is native machine code | Slower; translated again and again |
| When errors are found | Most of them before it runs | Many only when that line runs |
| Where the output can run | Tied to one kind of CPU and system | Anywhere the interpreter exists |
| Typical startup | A compile step, then instant start | Instant start, slower running |
| Good for | Operating systems, games, embedded devices, contests | Scripts, web backends, data analysis |
Here is a demonstration of row three. This Python program has a spelling mistake in a function that is never called.
def never_called():
prnt("this line is wrong")
print("Python starts running anyway")
Python prints its line and exits happily. The mistake is inside a function nobody calls, so nothing ever reads it. Put the same kind of mistake in a C file and the compiler refuses to build. It reads every line, whether or not that line will run.
Real life is a little blurrier: JIT
Java and C# do both. They first compile to a middle form called bytecode. While the program runs, a JIT (just-in-time) compiler turns the busiest parts of that bytecode into real machine code.
Modern JavaScript engines do the same thing to your browser's code. The classic split above still explains the trade-off you are choosing. Almost nothing in 2026 is purely one or the other.
What the Playground actually does
When you press Run on a Progsity Playground page, nothing magic happens. Your program is sent to a separate machine, written to a file, and compiled by GCC 12. The command is gcc -O2 -std=c17 -lm, with no -Wall and no -Wextra on it. All four stages run there, in the same order as Figure 1.
That missing -Wall matters. The Playground prints the messages GCC gives by default, so a missing prototype still shows, and a %s fed a number does not. The syntax card above is the command to run on your own machine, where you want every warning.
The compiled program then runs inside a sandbox, with a time limit and a memory limit. Its output comes back to your browser. That is the whole trick. You are using the same toolchain you would install locally, without installing it.
Where this is used
- The Linux kernel. Built by GCC or Clang running these exact four stages over roughly 30 million lines. A full build produces tens of thousands of
.ofiles, and the linker joins them into one image. - CPython. The Python interpreter is itself a compiled C program. So your future Python runs inside something that went through this pipeline first.
- The Java Virtual Machine. HotSpot, the standard JVM, starts by interpreting bytecode and switches to JIT compilation for code that runs often. It is the blurry middle of the table above.
- The Progsity Playground. Sends your file to the Progsity runner, compiles it with GCC 12 at
-O2 -std=c17 -lm, and runs it in a sandbox with limits. Same stages, different machine.
Common mistakes
1. Fixing the line the error names, when the fault is the line above.
printf("Hi\n")
return 0;
GCC says error: expected ';' before 'return'. There is nothing wrong with the return line. The compiler only noticed the missing semicolon when it reached the next word. Always check the line above the one named.
2. Declaring a function and never writing it.
int area(int w, int h); /* a promise */
int main(void) {
printf("%d\n", area(3, 4)); /* nobody kept it */
return 0;
}
The compiler is satisfied by the promise. The linker is not: undefined reference to 'area'. The fix is to write the function body, or to link the file that has it.
3. Ignoring warnings because the program ran.
int n = 5;
printf("%s\n", n);
GCC says warning: format '%s' expects argument of type 'char *', but argument 2 has type 'int'. It builds. Then it treats the number 5 as a memory address and usually crashes. Beginners who turn on -Wall -Wextra find bugs days earlier than beginners who do not.
4. Believing a clean build means a correct program.
Example 3 above compiles with zero warnings and prints the wrong average. No stage of the pipeline checks meaning. That job is yours, and it is why the exercises in this track always name the inputs to test.
A guided terminal walk. Do this on a Linux machine, on macOS, or in WSL on Windows. Save Example 1 as hello.c, then run the four commands below one at a time.
gcc -E hello.c -o hello.i # stop after stage 1
gcc -S hello.c -o hello.s # stop after stage 2
gcc -c hello.c -o hello.o # stop after stage 3
gcc hello.o -o hello # stage 4 only
What to look for. Open hello.i: your seven lines are now more than a thousand, because the whole of stdio.h was pasted in. Your own last six lines are still at the bottom, unchanged.
Open hello.s: it is assembly, and somewhere in it you will find .ascii "Hello, C. with your message inside. hello.o is not text, so do not open it in an editor.
Check yourself. Run ./hello at the end. If it prints Hello, C. you have just done by hand what one gcc command does for you.
Break Example 1 on purpose, four times, and collect the real messages. Repair it after each one.
The four breaks. Misspell stdio.h as studio.h. Delete the semicolon after the printf line. Change printf to printff. Change %s into the string and pass a number instead.
What the Playground shows. The first two stop the build. The third gives a warning about the missing prototype, then a linker error. The fourth gives no message at all, because the format check lives behind -Wall, which the Playground does not pass.
Rules. Write down the exact message for each break, and the stage it came from. Do not guess the message; copy what the tool printed.
Check yourself. You should end with three different messages from at least two different stages. The fourth break is the lesson: silence is not the same as correct. On your own machine, gcc -std=c17 -Wall -Wextra gives you its message too.
Write, in plain English, why a program compiled on your laptop will not run on your phone. The C source would compile on both machines, so the answer is not the language.
Rules. Two paragraphs. The first must use the words "machine code" and name the stage where the source stops being portable. The second must use the table above. It must explain why a Python script can be emailed to a friend and simply work.
Check yourself. A good answer says that the source is portable and the output of stage 3 is not. If your answer blames the language rather than the stage, read Figure 1 again.
Common doubts
Do I need to install GCC to follow this track?
No. Every program here runs in the Playground, which is the same compiler on a different machine. Install GCC when you start building projects with several files, which is Module 15.
What is the difference between GCC and Clang?
They are two compilers for the same language. Both run these four stages and both accept the code in this track. Their error messages are worded differently, and Clang's are often easier to read.
Why does one missing semicolon produce ten errors?
Because after the first confusion the compiler is reading your program wrongly, and keeps reporting what it finds. Always fix the first error and rebuild. Most of the others usually disappear.
Is a warning safe to ignore?
Treat it as an error you have not been bitten by yet. Every warning in the table above describes real broken code. Professional projects build with warnings turned into errors, on purpose.
Is C always faster than Python?
For the same algorithm, usually by a large margin, because there is no translation while it runs. But a good algorithm in Python beats a bad one in C every time. Choosing the algorithm matters more than choosing the language.
Key takeaways
- Compiling is four programs in a row: preprocessor, compiler, assembler, linker.
- Each stage has its own errors, and the message tells you which one spoke.
- A header is a promise; the library code joins at the linker, the last stage.
- A compiler translates once before running; an interpreter translates while running.
- "It compiled" means the grammar is fine, and says nothing about the answer.
- The Playground runs GCC 12 at C17 in a sandbox, without
-Wall, through the same four stages.
Next you will look for C in the day you have already had. It starts with the alarm that woke you and ends with this page.
Keyboard: j for the next lesson, k for the previous one, r to open the editor.