First Steps in the C Language
Lesson 2 of 8
2First Steps in the C Language
Moving from high-level, interpreted languages to the compiled, hardware-close C language, where you actually start understanding "what really happens" when a program runs.
What the picture shows: main.c passes through the preprocessor, the compiler and the linker, and comes out as one executable.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
Python is like an automatic car: you drive without thinking about gears. C is like a manual: you control everything yourself, it is faster and closer to the engine, but mistakes are yours. Before driving, every C program is translated to machine code once and then runs directly.
C is compiled: gcc -Wall -Werror main.c -o main turns source into an executable. Every variable has a fixed type (int, char, float), the program starts at main, functions are declared before use, and printf prints with format specifiers like %d and %s.
Before You Start
🧠General programming logic
The loop and conditional concepts from Part One are enough to start, what's new here is the syntax and strict type discipline.
A first program before any theory
Before you read about the preprocessor and the linker, write a program that runs. What you see here is all you need to follow the rest of this part.
1. Print a line
Every C program starts from a function called main. printf prints, \n means a new line, and %d is a slot where a number goes. The first line brings in the definition of printf from the C standard library.
#include <stdio.h>
int main(void)
{
printf("Hello from C\n");
printf("%d\n", 2 + 3);
return (0);
}
To run this on your machine: save it as hello.c, then gcc -Wall -Werror -o hello hello.c, then ./hello. The middle step is called compiling, and we take it apart shortly.
2. A variable with a number
In C you declare a variable's type before its name: int age; means a whole number called age. Then you put a value in it and print it with %d.
#include <stdio.h>
int main(void)
{
int age;
age = 21;
printf("age: %d\n", age);
printf("next year: %d\n", age + 1);
return (0);
}
Notice the semicolon at the end of every statement and the braces around the function body. Forgetting either is the first error you will meet, and the compiler's message names the line number.
The Core Idea: From Source Code to a Runnable Program
Figure 1, unlike Python which is interpreted line by line at runtime, C goes through four full transformation stages before becoming a program that runs directly on the processor.
1The Preprocessor
Processes lines starting with # like #include, replaces them with actual content before compilation. The key thing to understand is that this stage is pure text substitution, it doesn't understand C itself yet: #include <stdio.h> literally takes the entire contents of stdio.h (the standard declarations for functions like printf) and pastes them in place of that line, before the compiler ever sees anything; this is exactly why a typo in a filename, or a missing file, shows up as a "file not found" error at this very early stage, before any syntax checking of your actual code has even started.
2The Compiler
Translates C code into assembly language specific to the target processor architecture. This is the stage where C actually checks your code's grammar and matches types, most of the compilation errors that block you from proceeding surface here, because the compiler needs to know the exact size and type of every variable to generate correct processor instructions; this is the opposite of an interpreted language, which might only discover a type error the moment that specific line actually runs, not before.
3The Linker
Combines multiple object files (your code + standard libraries like printf) into one final executable file. Every object file the compiler produces has "gaps" for any function or variable name that's used but defined somewhere else, the linker's job is to find the real definition of each one of those names across the other files or the standard libraries, and fill each gap in with the correct address. This is exactly why an "undefined reference" error comes from the linker, not the compiler: your code is perfectly valid grammar from the compiler's point of view, but the actual definition of that function was never found anywhere by the time linking happens.
4Static Typing
Every variable in C has a fixed declared type (int, char, float...) that doesn't change at runtime, this gives C speed and precise memory control, at the cost of less flexibility than languages like Python. The actual reason for the speed: by knowing a variable's type at compile time, the compiler knows exactly how many bytes to reserve for it and when, so it can generate direct processor instructions with no extra runtime check of "what type is this variable actually right now", whereas a dynamically-typed language carries that check on every single access to a variable throughout execution, and that's a large part of its extra performance cost.
How It Works, Step by Step: From Code to Execution
#include <stdio.h>
int main(void)
{
int count;
count = 0;
while (count < 3)
{
printf("Hello! This is time number %d\n", count);
count++;
}
return (0);
}
gcc -Wall -Werror -o hello hello.c, runs the whole chain (preprocessing, compiling, assembling, linking) in one command.#include <stdio.h> gives access to the printf function defined in an external standard library.int main(void) is the single entry point the Linker looks for to start execution.main (here 0) tells the operating system whether the program succeeded (0) or failed (any other number).int count; without count = 0;). In C, an uninitialized variable holds a random "garbage value" from memory, not automatically zero like in some other languages.
Worked Example: Nested Loops to Print a Pattern
Printing a triangle of stars of height n using two nested loops, a classic exercise that locks in understanding of nested loops before moving to harder concepts:
void print_triangle(int n)
{
int i, j;
for (i = 1; i <= n; i++)
{
for (j = 0; j < i; j++)
printf("*");
printf("\n");
}
}
/* for n=4 prints:
*
**
***
**** */
Figure 2, The outer loop controls the number of lines (i); the inner loop controls the number of stars per line, and its iteration count depends on the outer loop's current value.
Deep Dive: The Anatomy of a C Program, Its Types and Functions
This stage lays the foundation every C project stands on: what a program is, how functions are written and declared, where variables live, what the types are, and why the project demands strict style rules.
1. The anatomy of a C program
A C program is a text file containing functions, and every program starts from a function named main, the entry point. The operating system calls it and receives its value: the number main returns is the exit code, where zero means success and anything else an error, and it is what the shell reads in $?. Each library function you use (such as printf) needs the header file declaring it, which tells the compiler its shape before it sees it used.
Figure 3, Everything starts at main and ends by returning the exit code to the system.
2. Three ways to print
| Function | What it prints | When to use it |
|---|---|---|
putchar(c) | One character | The simplest tool, the basis of all printing |
puts(s) | A string then a newline | Ready text with no formatting |
printf(fmt, ...) | Formatted text using specifiers like %d | When you need to combine values |
3. Types and their sizes
C fixes the type of each variable at declaration, and the type determines how many bytes it reserves and the range of its values. The sizeof operator gives the size in bytes and is computed at compile time. Exact values depend on the processor and compiler, which is why the flags -m32 and -m64 exist: they ask gcc to produce a program for a 32-bit or 64-bit system, changing the sizes of some types (above all the pointer).
| Type | Typical size | Use |
|---|---|---|
char | 1 byte | A character or a small number |
int | 4 bytes | An integer |
unsigned int | 4 bytes | A non-negative integer with double the positive range |
double | 8 bytes | A floating-point number with high precision |
| Pointer | 8 bytes on 64-bit | An address in memory |
In conditions: any nonzero value counts as true and zero alone is false. So if (x) means if (x != 0). And because of the ASCII table, 'A' + 1 equals 'B', since a character is a number.
4. Functions: declaration, definition and scope
A definition is the complete body of a function, while a declaration or prototype is a single line telling the compiler the function’s name, return type and parameters without its body. The compiler needs the declaration before the first call to check it, which is why prototypes are gathered in a .h header file included by the files that need them. Scope decides where a variable is visible: a local variable lives inside the { } block where it was declared and ends with it, and each function gets its own copy of its parameters (pass by value).
Figure 4, Nested scopes: the deeper you go, the shorter a variable’s life.
5. Why the strict rules?
📐One shared code style
A style guide you follow and a tool that checks it for you (such as clang-format): indentation, line length, function size, documentation. Its value is that any programmer can read your code effortlessly, and it is also a team-work habit.
🚨-Wall -Wextra -Werror -pedantic
Turns warnings into errors so that no silent problem slips through, and forces you to follow the standard.
🧱Constraints such as no standard library
Their purpose is for you to build the tools yourself and so understand them. The allowed _putchar is just a wrapper writing one byte with a write call.
📏At most five functions per file
Teaches you to split code into small, cohesive units.
Confusing = (assignment) with == (comparison) inside if. The compiler accepts if (x = 5) since it is a valid expression, but it puts 5 in x then treats the condition as always true. The -Wall warnings catch it.
Try it yourself
The build printed “undefined reference”. Which stage produced it?
The code is syntactically fine to the compiler, but the linker found no definition of the function in any object file or library.
What does #include <stdio.h> do in the preprocessor?
The preprocessor is a purely textual substitution stage, which is why a missing-file error shows up early, before syntax is checked.
What do -Wall -Wextra -Werror -pedantic achieve together?
So no problem passes silently: every warning stops the build until you deal with it.
Quick Review Flashcards
Check Your Understanding
Why does C need a full compilation stage while Python just runs the file directly?
Because Python is interpreted, every line is translated and executed immediately at runtime, while C is fully compiled in advance into machine code the processor understands directly, which gives it much higher execution speed at the cost of an extra compile step before running.
In the triangle example, if you replaced the inner condition with j < n instead of j < i, what changes in the output?
Every line would print the same fixed number of stars n instead of an increasing count, producing a rectangle full of stars instead of a triangle.
Why does the compiler warn on if (x = 5)?
Because it is an assignment, not a comparison: it puts 5 in x and the condition is always true.
What does the -m32 flag change in a program?
It builds for the 32-bit architecture, changing some type sizes such as the pointer.
Resources for this part
Sorted by kind: start with the official docs when you want the precise reference, or with an article or video when you want the explanation first.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs1
Done with this lesson?
Sign in to save your progress.