C Basics · beginner · ~5 min

What is C?

By the end of this lesson you will be able to: - Explain in plain words what C is: a small, statically typed, compiled language, and what each of those three words means. - Describe the journey source code takes to become a running program (preprocess, compile, assemble, link). - Identify where C is used in the real world and why it is still chosen in 2026. - Recognize the basic shape of a C program: `#include`, `main`, statements, and `return 0;`. - State the trade-off at the heart of C: total control over the machine in exchange for total responsibility for correctness and safety.

Overview

C is a small, statically typed, compiled programming language created in the early 1970s at Bell Labs (the language is usually dated to 1972). It is one of the most influential pieces of software ever written, because almost everything else is built on top of it.

Let us unpack the three words in that first sentence, since they shape everything you will do in this course.

  • Small means the language itself has very few keywords and rules. You can fit the whole grammar in your head. Most of C's apparent complexity comes from what you can do with that small toolkit, not from the toolkit itself.
  • Statically typed means the type of every value (whole number, decimal number, character, address, and so on) is fixed and known before the program runs. When you write int x;, you are promising the compiler that x will only ever hold an integer. The compiler checks these promises and refuses to silently mix incompatible types.
  • Compiled means your human-readable source code is translated ahead of time into native machine instructions that the CPU executes directly. This is different from an interpreted language like Python, where another program reads and runs your code line by line while it executes.

C sits one thin layer above assembly language. That closeness to the hardware lets you manipulate memory directly, talk to the operating system through well-defined entry points called system calls (or syscalls), and reason about roughly what each line costs the CPU.

This lesson is the foundation of the whole C Basics track and has no prerequisites. The very next lesson, Compiling with gcc, shows you how to actually run the translation pipeline introduced here. For now the goal is a clear mental model: what C is, why it exists, and what you are signing up for when you learn it. Learning C teaches you what a computer is actually doing when it runs any program.

Why it matters

C matters because it is the foundation almost the entire software world stands on. When you understand C, you stop seeing computers as magic boxes and start seeing how they actually work.

Almost every higher-level language is built on C or a C derivative.

  • The Linux kernel is written in C. So are large parts of the Windows and macOS kernels and most classic Unix utilities.
  • The runtimes that execute Python, Ruby, and (historically) PHP are themselves written in C. When your Python program runs, a C program is running it.
  • Databases such as SQLite and PostgreSQL, and servers such as Nginx and Redis, are C.

Knowing C gives you concrete, practical powers:

  • You can read the source of the platforms and tools you depend on, instead of treating them as black boxes.
  • You can write the small, fast "hot path" extensions that make slow scripting code 10x or 100x faster.
  • You can diagnose crashes and memory bugs that higher-level languages hide from you, because you understand what is happening underneath.

It also matters for security. A large fraction of the serious security vulnerabilities (CVEs) in widely used software trace back to memory mistakes that C makes possible. Understanding C is the first step to understanding why those bugs happen and how to prevent them. This course teaches the language and the safety habits together, not as an afterthought.

Core concepts

We will look at four ideas that together explain what C is. Take them one at a time.

1. The compilation pipeline

Definition. Compilation is the process of turning your human-readable .c source file into a native executable the CPU can run directly.

How it works. It is not one step but a chain of small programs, each taking the previous one's output as input:

  hello.c
     |  (1) preprocessor: handles #include, #define; pure text editing
     v
  expanded source
     |  (2) compiler: translates C into assembly for your CPU
     v
  hello.s  (assembly)
     |  (3) assembler: turns assembly into machine code
     v
  hello.o  (object file, machine code but not yet runnable)
     |  (4) linker: joins your object files + libraries into one program
     v
  hello    (executable you can run)

Why it is a chain. Each stage does one job well. The preprocessor only shuffles text. The compiler only understands C. The linker only stitches pieces together. You will normally run all four with a single command (gcc hello.c), but knowing the stages helps you read error messages: a linker error ("undefined reference to ...") is a very different problem from a compiler error ("expected ';'").

When NOT to think about it. For tiny programs you can ignore the internals and just compile. The pipeline matters when projects grow and you start splitting code across files.

Common pitfall. Beginners often confuse a compile error (the code is not valid C) with a runtime crash (the code compiled fine but did something illegal while running). They are caught at completely different times.

Knowledge check. Which stage is responsible for replacing #include <stdio.h> with the actual contents of that header file?

2. Static typing

Definition. Every variable has a single, fixed type, decided when you write the code and checked by the compiler before the program ever runs.

Plain-language explanation. A type tells the compiler two things: how much memory a value needs, and what operations are legal on it. int x; reserves a fixed-size slot for a whole number. double y; reserves a (usually larger) slot for a number with a decimal point.

How it works internally. Because the type is known in advance, the compiler can generate exact machine instructions and pick the right amount of memory. There is no runtime guessing.

When to use vs. avoid. In C you do not get to opt out: every variable is typed. The skill is choosing the right type (we cover this in later lessons on integer and floating types).

Common pitfall. Assuming the compiler will catch every type mistake. It catches many, but C also allows risky conversions (for example squeezing a large value into a small type) that silently lose data.

Knowledge check. In double pi = 3; the literal 3 is an integer. Explain in your own words what the compiler must do so that pi ends up holding a floating-point value.

3. Manual memory management

Definition. In C, you decide when memory is reserved and when it is released. There is no garbage collector running behind your back to clean up after you.

How it works. Most simple data lives on the stack, which is reserved and freed automatically as functions start and finish. For data whose size or lifetime you control yourself, you request memory from the heap with malloc and return it with free.

  Memory of a running C program (simplified)

  high addresses
  +---------------------+
  |       stack         |  <- local variables; grows DOWN as calls nest
  |          |          |
  |          v          |
  |                     |
  |          ^          |
  |          |          |
  |        heap         |  <- malloc/free live here; grows UP
  +---------------------+
  |  globals / static   |
  +---------------------+
  |   program code      |
  low addresses

When to use the heap. When you need data that outlives the function that created it, or whose size you only learn at runtime. Otherwise prefer the stack: it is simpler and frees itself.

Common pitfall. Forgetting to free heap memory (a memory leak), or using memory after freeing it. These are the classic C bugs and we will return to them often.

Knowledge check. A program calls malloc 1,000 times in a loop but never calls free. What kind of problem is this, and where in the diagram above is the memory accumulating?

4. Direct hardware access through pointers

Definition. A pointer is a variable whose value is a memory address: it points at where some other data lives.

Why it is powerful. With pointers you can read and write specific locations in memory. Combined with operating-system facilities, this is exactly how device drivers, kernels, and high-performance libraries do their work.

Why it is dangerous. Nothing stops you from pointing at the wrong place. Following a bad pointer is one of the most common ways a C program crashes or becomes insecure. Pointers get their own dedicated lessons later; for now just know they exist and are the source of much of C's power and its risk.

Common pitfall. Treating an uninitialized pointer as if it already points somewhere valid. It points at garbage until you make it point at something real.

Syntax notes

A C program has a small, predictable shape. Here are the ground rules, with a minimal annotated example.

#include <stdio.h>   // bring in declarations for printf and friends

int main(void) {     // execution starts here; 'void' = takes no arguments
    printf("Hi\n");  // a statement; \n is a newline; line ends with ;
    return 0;        // 0 tells the operating system "success"
}                    // block closes with a matching brace

Key rules:

  • Source files end with .c; header files end with .h.
  • A complete program needs a main function. When main returns 0, it signals success to the operating system; a non-zero value signals an error.
  • Every statement ends with a semicolon ;.
  • Blocks of code are wrapped in matching braces { }.
  • Comments are // for a single line or /* ... */ for a block.
  • C is case-sensitive: Main, MAIN, and main are three different names, and only main is the entry point.
  • #include lines start with # and have no semicolon; they are instructions to the preprocessor, not normal statements.

Lesson

C is a small, compiled, statically typed programming language designed in the early 1970s at Bell Labs.

It has shaped almost every layer of modern computing:

  • Operating systems: Linux and the internals of Windows.
  • Language runtimes: Python, JavaScript, and Ruby.
  • Firmware in routers and microcontrollers.
  • The high-performance core of databases like SQLite and PostgreSQL.

Compared to higher-level languages, C gives you direct access to memory and only a thin abstraction over the CPU.

That power comes with responsibility. Most security vulnerabilities (CVEs) in low-level software trace back to mistakes that C makes possible. This course teaches both the language and how to use it safely.

Code examples

#include <stdio.h>

int main(void) {
    /* The classic first program: print a greeting and exit cleanly. */
    printf("Hello, World!\n");
    return 0;  // 0 = success status to the operating system
}

What it does. This program prints the text Hello, World! followed by a newline, then exits and reports success to the operating system.

Expected output:

Hello, World!

Why each piece is there. #include <stdio.h> makes the printf function available. int main(void) is the entry point every C program needs. printf writes text to standard output, and the \n is an escape sequence that produces a newline so the cursor moves to the next line. return 0; ends the program and tells the operating system everything went fine.

Edge cases to notice. If you forget the \n, the text still prints but without a trailing newline, so your shell prompt may appear glued to the output. If you write print instead of printf, or forget the #include, the program will not compile, because the name printf would be unknown.

Line by line

Here is what happens, in order, when you build and run the program above.

Step What happens
1 The preprocessor sees #include <stdio.h> and pastes in the declarations for printf so the compiler knows it exists.
2 The compiler reads int main(void) { ... }, checks the types and syntax, and translates it to assembly.
3 The assembler and linker turn that into a runnable executable (joining in the standard library code for printf).
4 You run the program. The operating system loads it and calls main.
5 printf("Hello, World!\n"); runs: the characters are sent to standard output and \n moves the cursor to a new line.
6 return 0; runs: main finishes and hands the value 0 back to the operating system as the exit status.
7 The program ends. In a shell you can confirm the status with echo $?, which prints 0.

The most important idea: nothing in main runs until the operating system calls main, and main runs top to bottom. Statements execute in the order they are written. There is no hidden setup beyond loading the program and jumping to main.

Common mistakes

Realistic beginner mistakes, each with the wrong version, why it fails, and the fix.

1. Using print instead of printf.

print("Hello\n");   // WRONG: there is no function called print in C

Why it is wrong: C's output function is printf (the f is for "formatted"), declared in <stdio.h>. Fix:

printf("Hello\n");  // correct

How to recognize it: the compiler complains about an undeclared or implicitly declared function named print.

2. Forgetting the #include.

int main(void) {
    printf("Hi\n");  // WRONG without the include above
    return 0;
}

Why it is wrong: without #include <stdio.h>, the compiler has never heard of printf. Fix: add #include <stdio.h> as the first line. Recognize it by warnings/errors mentioning an implicit declaration of printf.

3. Missing the semicolon.

printf("Hi\n")     // WRONG: no semicolon
return 0;

Why it is wrong: every statement must end with ;. The compiler reads the next line as part of the unfinished statement and reports a confusing error, often pointing at the next line. Fix: end the printf line with ;.

4. Capitalizing Main.

int Main(void) { return 0; }  // WRONG: this is not the entry point

Why it is wrong: C is case-sensitive, and the required entry point is the lowercase main. The linker will report that it cannot find main. Fix: use main.

5. Putting a semicolon after #include.

#include <stdio.h>;   // WRONG: preprocessor lines take no semicolon

Why it is wrong: #include is a preprocessor directive, not a statement. The stray ; becomes part of the line and can cause errors. Fix: remove the semicolon.

Debugging tips

Match your approach to the kind of problem you are having.

It does not compile (compiler errors).

  • Read the first error, not the last. Later errors are usually fallout from the first one.
  • Common messages: expected ';' (missing semicolon, often on the line above where it points), implicit declaration of function 'printf' (missing #include <stdio.h>), undefined reference to 'main' (this is a linker error: your entry point is misspelled or missing).
  • Compile with extra warnings turned on: gcc -Wall -Wextra hello.c. Warnings often reveal the real bug before it bites.

It compiles but crashes (runtime errors).

  • A crash usually means an illegal memory operation (you will meet these properly in the pointer lessons).
  • Run it under a debugger: gdb ./a.out, then type run. When it crashes, type bt (backtrace) to see the chain of function calls that led to the crash.

It runs but gives the wrong answer (logic errors).

  • The code is valid C and does not crash, so the compiler cannot help: the mistake is in your reasoning.
  • Add temporary tracing lines like printf("debug: x=%d\n", x); to watch how values change.

Questions to ask when stuck: Did I save the file before compiling? Am I running the binary I just built? Did I read the first error message? Did I include the header for every function I call?

Memory safety

C trusts you completely, which means it will happily let you do dangerous things without warning. The key risks, even in simple programs, are:

  • Reading uninitialized memory. A variable you declare but never assign holds whatever garbage was already at that location.
  • Writing past the end of an array (a buffer overflow), which silently corrupts neighboring data.
  • Using memory after you free it (a use-after-free), or freeing it twice.

These are rarely caught at compile time. They are silent corruption that may not crash immediately; the program can keep running and fail mysteriously much later, sometimes in a completely unrelated part of the code. This is exactly what makes C bugs hard to diagnose, and it is the root of many real-world security vulnerabilities.

The defensive mindset to start building now:

  • Initialize variables when you declare them wherever practical.
  • Treat every malloc, every array index, and every pointer use as a small contract you must personally uphold.
  • Compile with -Wall -Wextra and, later, run with sanitizers (for example gcc -fsanitize=address) to catch memory errors at runtime.

You are not expected to master memory safety yet. The point of this first lesson is to understand why it matters so much in C specifically: the language gives you the power, and never the seatbelt.

Real-world uses

C is chosen wherever performance, small size, or unusual hardware matters.

  • Operating systems: the Linux kernel, the BSD family, and the FreeRTOS embedded scheduler.
  • Language runtimes: the CPython interpreter (which runs most Python in the world) and the Lua engine embedded in many games.
  • Servers and databases: the Nginx web server, the Redis in-memory database, and SQLite (the most widely deployed database engine on Earth, shipping inside phones, browsers, and apps).
  • Security and infrastructure: the OpenSSL cryptography library and countless device drivers.
  • Embedded and graphics: microcontroller firmware in routers and appliances, and the performance-critical rendering paths of game engines.

A useful rule of thumb: if something has to be fast, small, or run on weird hardware, it is probably C.

Professional best-practice habits to start forming now.

Beginner rules:

  • Use clear, descriptive names; comment only the non-obvious.
  • End every program's main with an explicit return 0;.
  • Always include the header for every function you call.
  • Compile with -Wall -Wextra and fix the warnings.

More advanced habits (for later in the course):

  • Validate every input and check the result of every call that can fail (malloc, file opens, and so on).
  • Free what you allocate and close what you open; pair every resource acquisition with its cleanup.
  • Keep functions small and single-purpose so they are easy to test and reason about.

Practice tasks

Work through these in order; each builds on the last. Do not look for full solutions: aim to figure them out from the lesson.

Beginner 1 — Run your first program.

  • Objective: compile and run the Hello, World! example unchanged.
  • Requirements: save it as hello.c, compile it, and run the result.
  • Expected output: Hello, World! on its own line.
  • Hint: the next lesson, Compiling with gcc, covers the exact command. Concepts: compilation pipeline, main, printf.

Beginner 2 — Personalize the greeting.

  • Objective: change the program to print your own name.
  • Requirements: it must print a greeting that includes your name, followed by a newline.
  • Example output: Hello, Sam!
  • Hint: you only need to edit the string inside printf. Concepts: string literals, \n.

Intermediate 1 — Three lines of numbers.

  • Objective: print three integers, each on its own line.
  • Requirements: use printf and the %d format specifier to print 1, 2, and 3.
  • Example output:
1
2
3
  • Hint: printf("%d\n", 1); prints a single integer and a newline. Concepts: printf formatting, statements run top to bottom.

Intermediate 2 — Read the compiler.

  • Objective: deliberately break the program and study the error.
  • Requirements: remove one semicolon, compile, and write down (in your own words) what the first error message says and which line it points to.
  • Hint: notice that the error sometimes points to the line after the missing semicolon. Concepts: compile errors, reading error output.

Challenge — Confirm the exit status.

  • Objective: prove that your program returns success to the operating system.
  • Requirements: run your compiled program, then immediately inspect the exit status with echo $? in your shell; it should print 0. Then change return 0; to return 3;, rebuild, run, and confirm echo $? now prints 3.
  • Constraint: do not change anything else.
  • Hint: the value main returns becomes the program's exit status. Concepts: return from main, exit status, the role of the operating system.

Summary

  • C is a small, statically typed, compiled language from the early 1970s, sitting one thin layer above assembly.
  • Static typing fixes every value's type before the program runs; compilation translates your source into native machine code through a pipeline of preprocessor, compiler, assembler, and linker.
  • C gives you manual memory management and direct hardware access through pointers: enormous power, and no automatic safety net.
  • The smallest complete program needs #include, int main(void), statements ending in ;, and return 0; for success.
  • The most common early mistakes are typos like print for printf, a forgotten #include, and a missing semicolon. Read the first compiler error.
  • What to remember: C's strength and its danger are the same thing. It does exactly what you tell it, so understanding the machine, and writing carefully, is the whole game. The next lesson, Compiling with gcc, turns this mental model into a working build.

Practice with these exercises