C Basics · beginner · ~8 min

Variables

- Declare a variable of a basic C type, and understand what memory the declaration reserves - Tell apart *declaration*, *initialisation*, and *assignment* — and know why the difference matters - Read and update a variable's value, and predict what it holds at each step - Recognise variable *scope* (where a name is visible) and *storage duration* (how long it lives) - Use `const` to mark values that must never change, and name variables clearly - Explain why reading an uninitialised variable is undefined behaviour, and always avoid it

Overview

A variable is a named box in memory that holds one value of a fixed type. Instead of juggling raw memory addresses, you give a piece of data a name — like count or rate — and the compiler remembers where it lives for you.

In the previous lesson, printf and format specifiers, you printed fixed text and literal numbers. That is useful, but a program that only ever prints the same thing cannot remember anything. Variables are how a program keeps track of data that changes as it runs: a running total, how many bytes are left to send, whether a user is logged in.

You work with a variable in three steps:

  • Declare it once, giving it a type and a name: int counter;
  • Assign a value into it: counter = 0;
  • Read the value back whenever you need it: printf("%d\n", counter);

That is the whole idea in plain language. In C terminology: a declaration reserves storage of a specific type, an assignment writes bytes into that storage, and reading fetches the current bytes back and interprets them as that type. Because C is statically typed, once you declare count as an int, it stays an int for its entire life — the compiler checks and enforces this at every use.

Why it matters

Variables are how a program remembers state — the data it holds in mind while it runs.

Without variables, every computation would have to be one giant expression, and nothing could carry over from one step to the next. With them, you break a problem into clear, named steps: read a value, transform it, store the result, use it later.

Once a value has a good name, you can reason about it, print it while debugging, change it safely, and — months later — still understand what your own code does. In real systems, a well-named variable like bytes_remaining is documentation that never goes stale, while a value buried inside a nested expression is a puzzle every time you reread it.

Variables are also where a whole class of beginner bugs lives: using a value before you set it, confusing two similarly named variables, or assuming a value survives a function call when it does not. Understanding variables precisely is what lets you avoid those bugs instead of chasing them.

Core concepts

1. Declaration — reserving space

A declaration tells the compiler the type and name of a variable, so it can set aside memory for it.

int counter;   // reserve room for one int, call it counter

Internally, for a local variable this reserves a slot on the stack — a region of memory that grows and shrinks as functions are called and return. The compiler now knows the size (an int is typically 4 bytes) and the name.

When to use: every time you need a new named value. When not to: don't declare a variable you never read — it is clutter, and compilers warn about it.

Pitfall: a bare declaration does not give the variable a usable value. Its contents are whatever bytes were already sitting in that memory ("garbage").

Stack after `int counter;`  (value is garbage until you assign)

  +-----------------------+
  |  counter : int        |   bytes = ?? ?? ?? ??
  +-----------------------+

Knowledge check: After int counter; with no assignment, what value does counter hold?

2. Initialisation vs. assignment

Initialisation gives a variable its first value at the moment it is declared. Assignment changes the value of a variable that already exists.

int x = 5;   // initialisation: declare AND set in one step
x = 10;      // assignment: overwrite the existing value

They look similar but happen at different times. Initialising at declaration is the safest habit, because it means the variable never spends a moment holding garbage.

Term When it happens Example Leaves garbage?
Declaration only Reserve storage int x; Yes — until you assign
Initialisation At declaration int x = 5; No
Assignment Any time later x = 10; No

Pitfall: = is assignment, not equality. x = 5 stores 5 into x; testing for equality uses ==. Writing if (x = 5) compiles but always assigns and is almost never what you meant.

Knowledge check (predict the output): After int x = 5; x = x + 3;, what does printf("%d\n", x); print?

3. Types and static typing

Every variable has a fixed type that decides how many bytes it uses and how those bytes are interpreted.

int    count  = 0;      // whole numbers
char   letter = 'A';    // a single character (a small integer)
double rate   = 0.05;   // real numbers with a fractional part

Because C is statically typed, the type is fixed at compile time. You cannot later store text in an int; the compiler enforces the type at every assignment and operation. (The next lesson, Data types, goes deep on the full set of types and their sizes and ranges.)

Pitfall: mismatched format specifiers. printf("%d", rate) prints garbage because %d expects an int but rate is a double. Match the specifier to the type: %d for int, %c for char, %f for double.

4. Scope — where a name is visible

Scope is the region of source code where a variable's name can be used. A variable declared inside a { } block exists only from its declaration to the matching closing brace.

int main(void) {
    int a = 1;          // visible for the rest of main
    {
        int b = 2;      // visible only inside this inner block
        // a and b both usable here
    }
    // b no longer exists here; a still does
}
SCOPE NESTING

  main() {
    a  --------------------------------+
      { inner block {                  |
        b  ----------------+           |
        (a and b visible)  |  b's scope | a's scope
      } b ends here -------+           |
    (only a visible)                   |
  } a ends here ----------------------+

Pitfall — shadowing: declaring a new variable with the same name in an inner block hides the outer one. The outer variable still exists; you just cannot see it by that name until the inner block ends. This makes bugs hard to spot.

Knowledge check (find the bug): Why does printf("%d\n", b); placed after the inner block above fail to compile?

5. Storage duration — how long a variable lives

Storage duration controls how long a variable keeps its value.

  • A plain local variable has automatic storage: it is created each time its block runs and destroyed when the block ends. Its value does not survive between calls.
  • A static local variable has static storage: it is created once, keeps its value across calls, and is zero-initialised.
void tick(void) {
    static int calls = 0;   // created once, remembers across calls
    calls = calls + 1;
    printf("call number %d\n", calls);
}
Kind Lives Default value Survives between calls?
Automatic (int x;) While its block runs Garbage No
Static (static int x;) Whole program Zero Yes

When to use static: for a counter or cache that must persist across calls. When not to: for ordinary temporary values — static makes them shared state, which complicates reasoning and multithreading.

Knowledge check (explain in your own words): Why does a static int calls keep counting up across calls, while a plain int calls would reset to garbage each time?

6. const — values that must not change

const marks a variable as read-only after initialisation. Any later attempt to assign to it is a compile error.

const double PI = 3.14159;
// PI = 3.2;   // error: assignment of read-only variable

Use const for fixed physical constants, limits, and configuration you never intend to overwrite. It documents intent and lets the compiler catch accidental writes.

Syntax notes

The core forms you will use constantly:

type name;              // declaration only (value is garbage until set)
type name = expr;       // declaration + initialisation (preferred)
name = expr;            // reassignment of an existing variable
int a = 1, b = 2;       // several declarations of the same type on one line

Annotated example:

int    count = 0;       // int variable, initialised to 0
char   letter = 'A';    // char holds one character literal (single quotes)
double rate = 0.05;     // double for fractional numbers
count = count + 1;      // reassignment: read count, add 1, store back -> 1

Constants and booleans:

const int LIMIT = 10;   // read-only after this line

#include <stdbool.h>
bool ready = true;      // true/false; you may also use an int holding 0 or 1

Naming rules: names are case-sensitive (count and Count differ), must start with a letter or underscore, may contain letters, digits, and underscores, and cannot be a reserved word such as int or return.

Lesson

A variable is a named storage location with a fixed type.

Declare it with type name; or type name = value;.

Naming rules:

  • Names are case-sensitive (count and Count are different).
  • A name must start with a letter or an underscore.
  • A name cannot be a reserved word (such as int or return).

C is statically typed: the compiler checks and enforces the type at every assignment and operation. Once a variable is declared as int, it stays an int for the rest of that scope.

Code examples

#include <stdio.h>
#include <stdbool.h>

int main(void) {
    /* Declaration + initialisation of several types. */
    int    count  = 0;         // a running counter
    double rate   = 0.05;      // an interest rate (5%)
    char   grade  = 'B';       // a single character
    const int LIMIT = 3;       // read-only cap on the loop
    bool   done   = false;     // a true/false flag

    printf("start:  count=%d rate=%.2f grade=%c\n", count, rate, grade);

    /* Assignment: change count over time until we hit the const LIMIT. */
    while (!done) {
        count = count + 1;                 // reassign: read, add 1, store back
        printf("tick:   count=%d\n", count);
        if (count == LIMIT) {
            done = true;                   // flip the flag to exit the loop
        }
    }

    /* Scope: this block introduces its own variable. */
    {
        int bonus = count * 2;             // visible only inside these braces
        printf("bonus:  %d\n", bonus);
    }
    /* bonus does not exist here anymore. */

    /* Apply the rate to a computed amount. */
    double amount = 100.0;
    double total  = amount + amount * rate;
    printf("total:  %.2f\n", total);

    return 0;   // main returns 0 to signal success; no heap to free here
}

What it does: it declares variables of several basic types, counts up to a const limit with assignment inside a loop, shows a block-scoped variable, and finishes with a small double calculation.

Expected output:

start:  count=0 rate=0.05 grade=B
tick:   count=1
tick:   count=2
tick:   count=3
bonus:  6
total:  105.00

Edge cases: if you removed the done = true; line the loop would run forever; if LIMIT were 0 the loop body would still run once because count starts at 0 and is incremented before the comparison; using %d for rate (a double) would print garbage.

Line by line

  1. #include <stdio.h> / #include <stdbool.h> — pull in printf and the bool/true/false names.
  2. int count = 0; — reserve an int slot and store 0 in it immediately (no garbage phase).
  3. double rate = 0.05; — reserve a double and store the fractional value 0.05.
  4. char grade = 'B'; — store the character 'B' (a single quote literal, one byte).
  5. const int LIMIT = 3; — a read-only int; any assignment to LIMIT later would fail to compile.
  6. bool done = false; — a flag initialised to false (0).
  7. The first printf reads the current values and prints them using matching specifiers (%d, %.2f, %c).
  8. while (!done) — loop while done is false. count = count + 1; reads count, adds 1, and stores the result back into the same slot.
  9. When count == LIMIT (3), done = true; flips the flag so the next test of !done is false and the loop stops.
  10. The inner { } block declares bonus, which is visible only there; after the closing brace its name is gone.
  11. total = amount + amount * rate; computes 100.0 + 100.0 * 0.05 = 105.0.

Trace of the loop:

Step count before action count after done
enter 0 !done true 0 false
iter 1 0 +1 1 false
iter 2 1 +1 2 false
iter 3 2 +1, count==LIMIT 3 true
exit 3 !done false 3 true

Common mistakes

1. Reading an uninitialised variable.

// WRONG
int total;
total = total + 5;   // total started as garbage

Why it is wrong: total had no defined value, so total + 5 is meaningless and the program has undefined behaviour. Corrected:

// RIGHT
int total = 0;
total = total + 5;   // now clearly 5

Prevent it by initialising at declaration and by compiling with -Wall, which warns "may be used uninitialized".

2. Confusing = with ==.

// WRONG: assigns 3 to count, condition is always true
if (count = 3) { ... }

Corrected:

// RIGHT: compares count to 3
if (count == 3) { ... }

Recognise it: the compiler with -Wall warns "suggest parentheses around assignment used as truth value".

3. Shadowing without meaning to.

int value = 10;
{
    int value = 99;   // hides the outer value inside this block
    printf("%d\n", value);  // prints 99, not 10
}

Why it bites: you think you are updating the outer value but you created a second one. Fix by giving the inner variable a distinct name. Enable -Wshadow to be warned.

4. Assuming a plain local survives a function call.

void f(void) { int seen = 0; seen++; }  // seen is 1 every call, never 2

If you need it to persist, use static int seen = 0;.

Debugging tips

Compiler errors:

  • 'x' undeclared — you used a name outside its scope, or misspelled it, or forgot the declaration.
  • assignment of read-only variable 'PI' — you tried to write to a const.
  • redeclaration of 'x' — two declarations of the same name in the same scope.

Warnings worth turning into habits: always compile with -Wall -Wextra. "x may be used uninitialized" and "unused variable x" catch two of the most common beginner mistakes before the program even runs.

Runtime / logic errors:

  • A variable holds a surprising value: print it immediately before and immediately after every assignment to see exactly where it changes.
  • In gdb: print counter shows the value without changing it; watch counter stops the program whenever counter changes so you can catch the culprit assignment.

Questions to ask when it misbehaves:

  • Did I initialise this before reading it?
  • Am I looking at the variable I think I am, or a shadowed one with the same name?
  • Does the format specifier match the type?
  • Should this value persist across calls (needs static) or reset each time?

Memory safety

The central hazard for this topic is reading an uninitialised local variable, which is undefined behaviour in C.

int x;
printf("%d\n", x);   // reads x before it was ever set — undefined behaviour

An automatic (non-static) local variable starts out holding whatever bytes were already on the stack — "garbage". Reading it might print 0, might print a large stray number, and the optimizer is even permitted to assume this situation never happens, which can make the program behave in ways that seem impossible. The value can also change between runs, which makes such bugs maddening to reproduce.

Defensive practices:

  • Initialise at declaration (int x = 0;). This single habit removes the entire class of uninitialised-read bugs.
  • Compile with -Wall -Wextra so the compiler flags likely uninitialised reads.
  • Keep scope small. Declare a variable close to where you first use it, so there is little room to accidentally read it early.
  • Watch integer overflow. An int has a limited range; a + b can overflow if both are large. When adding many values, use a wider type such as long — exactly what the related "Sum of three" exercise practises.
  • Respect const. Marking values read-only lets the compiler prevent accidental writes.

Note that static locals are zero-initialised automatically, so static int n; is defined as 0 — but relying on that for readability's sake, prefer writing static int n = 0;.

Real-world uses

Every non-trivial program uses variables — but the professional skill is in naming and scoping them well.

Concrete example — a network read loop. Code that receives data over a socket keeps a variable like bytes_remaining and decrements it each time a chunk arrives. A clear name makes the loop's intent obvious; a static counter might track total connections handled since the server started. A single confused or uninitialised counter here can cause the loop to read past a buffer — a real security bug.

Best-practice habits:

  • Naming: descriptive names (tcp_segment_count, bytes_remaining) make code self-documenting; vague names (x, temp, flag) force a reread every time. Use snake_case for variables in C by convention.
  • Initialisation: initialise at the point of declaration; never leave a window where a variable holds garbage.
  • Scope: declare variables in the narrowest scope that works; a variable that lives only where it is needed cannot be misused elsewhere.
  • const correctness: mark anything that should not change as const.

Beginner focus: get declaration, initialisation, and assignment right, and always compile with -Wall. Advanced focus: minimise mutable state, prefer const, understand storage duration and linkage (static at file scope limits visibility to one file), and be deliberate about where shared/persistent state lives — especially in multithreaded code, where a static variable is shared across threads and needs protection.

Practice tasks

1. Beginner — three assignments. Declare a single int, assign three different values to it in turn, and printf it after each assignment. Objective: see that one variable holds one value at a time and each assignment overwrites the last. Expected output: three different numbers on three lines. Concepts: declaration, assignment.

2. Beginner — mixed types. Declare an int, a double, and a char, initialise each, and print all three in one printf using the correct specifiers (%d, %f, %c). Constraint: no compiler warnings with -Wall. Hint: a char literal uses single quotes, e.g. 'Q'. Concepts: types, format specifiers.

3. Intermediate — persistent counter. Write a function void count_call(void) that prints how many times it has been called, and call it four times from main. Expected output: call 1, call 2, call 3, call 4. Hint: a plain local resets each call — you need one that persists. Concepts: storage duration, static.

4. Intermediate — const guard. Declare const int LIMIT = 10;, then attempt to assign a new value to it. Objective: read and understand the exact compile error, then remove the bad line so the program builds. Deliverable: paste the error message as a comment and explain it in one sentence. Concepts: const, compile errors.

5. Challenge — scope and shadowing. In main, declare int value = 1;. Open a nested block, declare a second int value = 2; inside it, print value inside the block and again after the block. Objective: predict both outputs before running, then confirm; compile with -Wshadow and explain the warning it produces. Constraint: do not rename either variable. Concepts: scope, shadowing, storage duration. (No full solution given — reason it out from the scope diagram above.)

Summary

  • A variable is a named memory cell with a fixed type; C is statically typed, so the type is enforced at every use.
  • Three distinct steps: declare (int x; reserves space), initialise (int x = 5; sets the first value), assign (x = 10; changes it). Prefer initialising at declaration so a variable never holds garbage.
  • Scope is where a name is visible (its { } block); storage duration is how long it lives (automatic resets each call, static persists).
  • Use const for values that must never change; use clear snake_case names.
  • The classic mistake is reading an uninitialised variable — undefined behaviour. Initialise first and compile with -Wall -Wextra. Also watch = vs ==, shadowing, and integer overflow.
  • Remember: name things well, keep scope small, and never read a value you have not written.

Practice with these exercises