The C Path — learn C, visually

🎯 Pointers & Memory

Arrays: many values, one name

⏱ 12 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

A game tracks a hundred enemies; a weather app stores a temperature for every hour — you can't invent a separate variable name for each one. Arrays give a single name to the whole batch and a number to each slot. The catch: C's arrays come with no safety net whatsoever, and stepping one slot past the end has caused more famous crashes and security holes than any other mistake in programming history.

An array is a fixed-size sequence of elements of one type, packed back-to-back in memory. Declare with int temps[5]; — five ints, indexed temps[0] through temps[4]. Yes, from zero: the index is really an offset from the start, and the first element is zero elements in.

Declaring and indexing

temps.c
#include <stdio.h>

int main(void) {
    int temps[5] = {12, 15, 19, 14, 9};

    temps[2] = 21;                 /* write element 2 */
    printf("first %d, last %d\n", temps[0], temps[4]);

    for (int i = 0; i < 5; i++)    /* the canonical loop */
        printf("temps[%d] = %d\n", i, temps[i]);
    return 0;
}
terminal
$ gcc temps.c -o temps && ./temps
first 12, last 9
temps[0] = 12
temps[1] = 15
temps[2] = 21
temps[3] = 14
temps[4] = 9

This spot has an interactive memgrid widget — open the interactive lesson to play with it.

🧠 Checkpoint: For int a[10], the valid indices are…

  • 1 to 10
  • 0 to 10
  • 0 to 9
  • any int — C doesn’t care
Show answer

0 to 9 — Ten elements, offsets 0 through 9. Writing a[10] is the classic off-by-one: it’s one past the end, and touching it is undefined behavior.

No bounds checking. None.

Here's the deal C offers you: array indexing compiles to bare address arithmetic — maximally fast, zero safety net. Ask for a[3] in a 3-element array and C computes the address three elements past the start and reads whatever bytes happen to live there. No exception, no warning at runtime, no mercy:

oops.c — compiles clean, still broken
#include <stdio.h>

int main(void) {
    int a[3] = {1, 2, 3};

    printf("a[3]  = %d\n", a[3]);    /* UB: reads past the end   */
    printf("a[42] = %d\n", a[42]);   /* UB: reads a stranger’s   */
    return 0;                         /*     memory — or crashes  */
}
terminal
$ gcc oops.c -o oops && ./oops
a[3]  = 32764
a[42] = -1449275392
# garbage from neighboring stack memory — different every run
$ gcc -fsanitize=address oops.c -o oops && ./oops
==5150==ERROR: AddressSanitizer: stack-buffer-overflow ...
💀

Out-of-bounds access is undefined behavior — the biggest single source of C bugs and security holes in history. It may return garbage, crash, corrupt a neighboring variable, or appear to work for years. The bounds live in your head; the compiler trusts you completely. Tools like -fsanitize=address catch these at runtime — use them.

🧠 Checkpoint: What does C do when you read a[99] on an int a[3]?

  • Throws an exception
  • Returns 0
  • Refuses to compile
  • Whatever happens, happens — it’s undefined behavior
Show answer

Whatever happens, happens — it’s undefined behavior — The generated code just reads the address 99 elements in. Garbage, crash, or silent corruption — all are "correct" outcomes of UB. C sold the safety net to buy speed.

Initializers

Brace lists initialize arrays, with two lovely rules: missing trailing elements become zero, and (since C99) you can target specific indices with designated initializers:

init.c
#include <stdio.h>

int main(void) {
    int a[5]  = {1, 2};               /* rest zeroed: 1 2 0 0 0  */
    int b[]   = {1, 2, 3};            /* size inferred: 3        */
    int c[10] = {[0] = 1, [9] = 99};  /* designated (C99)        */

    printf("a: %d %d %d %d %d\n", a[0], a[1], a[2], a[3], a[4]);
    printf("b has %zu elements\n", sizeof b / sizeof b[0]);
    printf("c[9] = %d, c[5] = %d\n", c[9], c[5]);
    return 0;
}
terminal
$ gcc init.c -o init && ./init
a: 1 2 0 0 0
b has 3 elements
c[9] = 99, c[5] = 0
# partial initialization zero-fills everything you didn't mention
💡

The element-count idiom: sizeof a / sizeof a[0] — total bytes divided by bytes-per-element. Memorize it; every real C codebase has a macro for it. But beware: it only works where a is a true array (next-next lesson explains when it silently isn't).

🧠 Checkpoint: int a[5] = {7}; — what is a[4]?

  • 7
  • 0
  • Garbage (uninitialized)
  • Compile error
Show answer

0 — Any initializer list, even a partial one, zero-fills the remaining elements. (With NO initializer at all, a local array’s contents really are garbage.)

Variable-length arrays (VLAs)

C99 lets an array size be a runtime value: int buf[n];. Handy, but controversial: VLAs live on the stack, there's no way to detect allocation failure, and a large n simply crashes the program. C11 made them optional, and many codebases (the Linux kernel among them) ban VLAs outright. Prefer fixed sizes or malloc (coming soon).

This spot has an interactive editor widget — open the interactive lesson to play with it.

You may have noticed arrays and pointers keep brushing against each other — after one quick detour, we'll confront that relationship head-on.

▶ Practice this lesson interactively (with live gcc)