Back to ANSI C Examples

Basic C

A quick-reference and hands-on guide to the fundamentals of ANSI C programming.

How to use this page

The fastest way to keep what you learn is to teach it. The physicist Richard Feynman's trick was simple: if you can't explain something in plain words, you don't really understand it yet. For each section, work these four steps:

  1. Read the lesson once.
  2. Explain it out loud in plain words, as if teaching a friend — no jargon. If you stall, that's the gap.
  3. Do the exercise from a blank editor, without peeking. Where you get stuck is exactly what to reread.
  4. Reveal the solution, compare, and explain the idea again in your own words.

Every exercise is a complete program. Save it as main.c, then build and run it with gcc -Wall -Wextra main.c -o main && ./main. Watch for the 💡 Explain it simply prompts and ✎ Exercise boxes in each section. Hover any code block and click Copy to grab it.

Contents

  1. What is C?
  2. Program Structure
  3. Variables & Data Types
  4. Operators
  5. Input & Output
  6. Control Structures
  7. Loops
  8. Functions
  9. Arrays & Strings
  10. Pointers
  11. Structs
  12. Dynamic Memory

1. What is C?

A programming language is a precise, written notation for telling a computer what to do. C was created by Dennis Ritchie at Bell Labs between 1969 and 1973, originally to rewrite the Unix operating system. Before C, operating systems were written in assembly language, the processor's own instructions, which is tedious and different on every machine. C offered a middle ground: readable, structured code that still maps closely onto what the hardware actually does. It was standardised by ANSI in 1989 (“ANSI C” or C89), and later revisions added features such as declaring variables inside a for loop (C99).

C is a compiled language. A program called the compiler (such as gcc) translates your .c source files into a standalone executable of native machine code before the program ever runs. The result is small and very fast, with no interpreter or runtime sitting in between. That is why C still powers operating system kernels (Linux, Windows, macOS), embedded devices from microwaves to cars, databases like SQLite, and the interpreters of other languages. Python itself is written in C.

The price of that speed and control is responsibility. C is low-level: it lets you work directly with memory addresses, it does not check array bounds, and it has no garbage collector, so memory you request must be released by hand. Mistakes in C don't always produce a helpful error message; they can produce undefined behaviour, where the program may crash, give wrong answers, or appear to work until it doesn't. Learning C teaches you how computers really manage memory, knowledge that makes you better in every other language.

Compiler
The program (e.g. gcc) that translates C source into a machine-code executable.
Low-level
Close to the hardware: direct control over memory, with few safety checks.
Undefined behaviour
Code the C standard gives no meaning to, such as reading past an array's end; anything can happen.
Explain it simply

Tell a friend why operating systems are written in C and not in Python, in two sentences and without the words “compiled” or “low-level”.

Reveal a plain-language answer

C gets turned into the processor's own instructions ahead of time, so there's no middleman translating while it runs, and it lets the programmer say exactly where every byte of memory goes. An operating system is the thing that provides memory and middlemen to every other program, so it needs a language that can work without them.

2. Program Structure

Every C program is built from the same few parts. Learning to recognise them first makes every later example easier to read.

Lines beginning with # are preprocessor directives. They are handled before compilation proper begins. #include <stdio.h> pastes in the contents of a header file, which declares the functions a library provides, in this case standard input/output functions such as printf. Without it the compiler would not know what printf is. Next comes the main function, the program's entry point: execution starts at its first line. int main(void) says that main takes no arguments and returns an integer, and that integer, the exit status, tells the operating system whether the program succeeded (0) or failed (anything else).

Statements end with a semicolon, and curly braces { } group statements into blocks. C ignores extra whitespace, so indentation is purely for human readers, but good indentation is essential for them. Comments are written /* like this */, and C99 also allows // single-line comments. Building a program is a two-step cycle: compile with gcc main.c -o main, fix any errors the compiler reports, then run the result with ./main. Adding -Wall -Wextra asks the compiler to warn you about suspicious code, and you should always use it.

Preprocessor
The first stage of compilation, which handles #include, #define, and other # lines.
Header file
A .h file declaring functions and types, such as <stdio.h>.
Exit status
The integer main returns to the operating system; 0 means success.
#include <stdio.h>   /* standard I/O library */

/* main() is the entry point of every C program */
int main(void) {
    printf("Hello, World!\n");
    return 0;   /* 0 = success */
}

Compile and run:

gcc main.c -o main
./main
Common headers: <stdio.h> (I/O), <stdlib.h> (memory, exit), <string.h> (strings), <math.h> (maths).
Exercise — your turn

Save this as main.c, fill in the two missing lines so it prints exactly Hello, C! and reports success, then build and run it with gcc -Wall -Wextra main.c -o main && ./main.

#include <stdio.h>

int main(void) {
    /* TODO: print exactly: Hello, C!  (don't forget the \n) */
    /* TODO: return the "success" exit status */
}
Show solution
#include <stdio.h>

int main(void) {
    printf("Hello, C!\n");
    return 0;
}
Output
Hello, C!
Explain it simply

What would happen if you deleted #include <stdio.h>? Explain it with a phone book.

Reveal a plain-language answer

The header is the phone book entry for printf: it tells the compiler the function exists, what it's called, and what it expects to be given. Delete it and the compiler reaches printf without having seen it listed, so it complains that it doesn't know who you're trying to call. Modern compilers treat that as an error, or at best a warning, rather than guessing.

3. Variables & Data Types

A variable is a named location in memory that holds a value. In C, a variable is quite literally a reserved block of bytes, and its name is how you refer to those bytes. Before you use a variable you must declare it, stating its type and name, as in int age;. You may initialise it at the same time: int age = 36;.

The type tells the compiler two things: how many bytes to reserve, and how to interpret the bits stored there. The core types are int for whole numbers (usually 4 bytes), double for numbers with a fractional part (8 bytes; float is a smaller, less precise version), and char for a single character (1 byte). A char is really a small integer holding a character code, which is why 'A' + 1 is 'B'. The sizeof operator reports how many bytes any type or variable occupies. Qualifiers adjust the basic types: unsigned for non-negative values, long for a larger range, and const to make a variable read-only after initialisation.

Two C-specific facts catch beginners out. First, C has no built-in string type; text is stored as an array of char ending in a special null character, '\0', covered in section 9. Second, a local variable that is declared but not initialised holds garbage, whatever bytes happened to be in that memory before. Reading it is a bug. Always give variables a value before you use them.

Declaration
Stating a variable's type and name so the compiler can reserve memory for it.
sizeof
An operator giving the size in bytes of a type or variable.
Uninitialised
A variable whose value was never set; it holds unpredictable leftover data.
int    age     = 30;          /* whole number */
float  temp    = 36.6f;        /* single-precision decimal */
double pi      = 3.14159265;  /* double-precision decimal */
char   grade   = 'A';          /* single character */
int    active  = 1;            /* C has no bool: 0=false, 1=true */

/* Constants — value cannot change */
const int MAX = 100;
#define PI 3.14159            /* preprocessor constant */
TypeSize (typical)Range / Use
char1 byteSingle character or small integer
int4 bytesWhole numbers (~±2 billion)
long4–8 bytesLarger whole numbers
float4 bytes~6–7 decimal digits of precision
double8 bytes~15 decimal digits of precision
unsigned int4 bytesNon-negative integers only
Exercise — your turn

Declare the four variables and the constant so the program prints the three lines shown in the solution. Pay attention to which printf format code goes with which type.

#include <stdio.h>

int main(void) {
    /* TODO: declare name ("Ada"), age (36), height (1.65), grade ('A')
             and a const int MAX_AGE of 120 */

    printf("%s is %d years old and %.2f m tall.\n", name, age, height);
    printf("Grade: %c\n", grade);
    printf("Years to %d: %d\n", MAX_AGE, MAX_AGE - age);
    return 0;
}
Show solution
#include <stdio.h>

int main(void) {
    char name[] = "Ada";
    int age = 36;
    double height = 1.65;
    char grade = 'A';
    const int MAX_AGE = 120;

    printf("%s is %d years old and %.2f m tall.\n", name, age, height);
    printf("Grade: %c\n", grade);
    printf("Years to %d: %d\n", MAX_AGE, MAX_AGE - age);
    return 0;
}
Output
Ada is 36 years old and 1.65 m tall.
Grade: A
Years to 120: 84
Explain it simply

Why does C make you state a variable's type up front, when Python doesn't? Explain it with a warehouse.

Reveal a plain-language answer

In C, declaring a variable is like reserving a shelf in a warehouse before anything arrives: the warehouse needs to know how big the shelf must be (4 bytes for an int, 8 for a double) and how to read the label on what's stored there. Python keeps a note on every item saying what it is and sorts it out as it goes, which is convenient but slower. C decides everything in advance so the running program never has to check.

4. Operators

An operator is a symbol that performs an operation on one or more values, called operands. In a + b, the + is the operator. A combination of operands and operators that produces a value is an expression, and the rules of precedence decide which operators apply first. *, /, and % bind more tightly than + and -, as in ordinary arithmetic. When in doubt, add parentheses.

The arithmetic operators are + - * / %. The most important subtlety is integer division: when both operands are integers, / throws the fractional part away, so 7 / 2 is 3, and % gives the remainder, 1. To get 3.5, at least one operand must be a floating-point value, either written that way (7.0 / 2) or converted with a cast: (double)7 / 2. A cast is an explicit instruction to convert a value to another type.

Comparison operators (== != < > <= >=) and logical operators (&& AND, || OR, ! NOT) produce an int: 1 for true and 0 for false. C89 has no separate boolean type. (C99 added <stdbool.h> with bool, true, and false.) In a condition, any nonzero value counts as true. Beware the classic bug of writing = (assignment) where you meant == (comparison). Compound assignment operators such as += and *= update a variable in place, and ++ / -- add or subtract one.

Integer division
Dividing two integers, which truncates toward zero: 7 / 2 is 3.
Cast
An explicit type conversion written (type)value, such as (double)n.
Truth value
In C, 0 is false and any other value is true.

Arithmetic

int a = 10, b = 3;
a + b    /* 13 */
a - b    /* 7  */
a * b    /* 30 */
a / b    /* 3  — integer division truncates */
a % b    /* 1  — modulus (remainder) */
a++      /* post-increment: use then add 1 */
++a      /* pre-increment: add 1 then use */

Comparison & Logical

a == b   /* equal */
a != b   /* not equal */
a <  b   /* less than */
a >  b   /* greater than */

a && b   /* logical AND */
a || b   /* logical OR  */
!a        /* logical NOT */

Assignment Shortcuts

x += 5;   /* x = x + 5 */
x -= 2;   /* x = x - 2 */
x *= 3;   /* x = x * 3 */
x /= 4;   /* x = x / 4 */
Exercise — your turn

A carton holds 5 eggs. Replace each 0 so the program reports full cartons and leftovers, shows integer vs. floating-point division, then apply += 5, *= 2, and ++ to total.

#include <stdio.h>

int main(void) {
    int eggs = 17;
    int per_box = 5;
    int total = 10;

    printf("Full boxes: %d\n", 0);   /* TODO: use / */
    printf("Left over: %d\n", 0);    /* TODO: use % */
    printf("7 / 2 = %d, but (double)7 / 2 = %.2f\n", 0, 0.0);  /* TODO */

    /* TODO: add 5 to total, double it, then add one */
    printf("total = %d\n", total);
    return 0;
}
Show solution
#include <stdio.h>

int main(void) {
    int eggs = 17;
    int per_box = 5;
    int total = 10;

    printf("Full boxes: %d\n", eggs / per_box);
    printf("Left over: %d\n", eggs % per_box);
    printf("7 / 2 = %d, but (double)7 / 2 = %.2f\n", 7 / 2, (double)7 / 2);

    total += 5;
    total *= 2;
    total++;
    printf("total = %d\n", total);
    return 0;
}
Output
Full boxes: 3
Left over: 2
7 / 2 = 3, but (double)7 / 2 = 3.50
total = 31
Explain it simply

Why is 7 / 2 equal to 3 in C, and how do you get 3.5? Explain it with coins.

Reveal a plain-language answer

If you only have whole coins, you can't give each of two people 3.5 of them; each gets 3 and one coin is left over (that's 7 % 2). Integer division works in whole coins only. Casting to double is like switching to money you can split into cents, so the answer can be 3.5.

5. Input & Output

Programs communicate through streams, flows of characters. Every C program starts with three open streams: standard output (stdout, normally the terminal), standard input (stdin, normally the keyboard), and standard error (stderr, for error messages). The functions that use them are declared in <stdio.h>.

printf writes formatted output. Its first argument is a format string: ordinary text mixed with conversion specifiers that begin with %, each replaced in turn by one of the following arguments. %d prints an int, %f a double, %c a char, %s a string, and %% a literal percent sign. Between the % and the letter you can add a field width and precision: %7.2f prints a number right-aligned in 7 columns with 2 decimal places, and %-8s left-aligns a string in 8 columns. Special characters are written as escape sequences, such as \n for newline and \t for tab.

scanf reads formatted input using the same specifiers. Because scanf must store what it reads into your variables, you pass their addresses with &, as in scanf("%d", &age); section 10 explains why. scanf returns how many values it successfully read, so check that return value. For whole lines of text, fgets(buffer, size, stdin) is safer than scanf("%s"), because it never writes past the end of your buffer. A mismatch between a specifier and its argument's type is undefined behaviour, and -Wall will warn you about it.

Format string
The first argument to printf/scanf: text plus % conversion specifiers.
Field width
The minimum number of columns a value is printed in, as in %7.2f.
Escape sequence
A backslash code for a special character: \n, \t, \\.
#include <stdio.h>

int main(void) {
    int age;
    char name[50];

    /* printf — formatted output */
    printf("Enter your name: ");

    /* scanf — formatted input */
    scanf("%s", name);
    scanf("%d", &age);    /* & gives scanf the address to write to */

    printf("Hello, %s! You are %d years old.\n", name, age);
    return 0;
}
Format specifierType
%dint
%ffloat / double
%cchar
%sstring (char array)
%ppointer address
%ldlong
\nnewline
\ttab
Exercise — your turn

Print a neatly aligned receipt: names left-aligned in 8 columns, prices right-aligned in 7 columns with 2 decimal places. Write the four printf calls.

#include <stdio.h>

int main(void) {
    double coffee = 3.50;
    double bagel = 2.25;

    /* TODO: print the header "Item" / "Price" using %-8s%7s */
    /* TODO: print Coffee, Bagel and Total rows using %-8s%7.2f */
    return 0;
}
Show solution
#include <stdio.h>

int main(void) {
    double coffee = 3.50;
    double bagel = 2.25;

    printf("%-8s%7s\n", "Item", "Price");
    printf("%-8s%7.2f\n", "Coffee", coffee);
    printf("%-8s%7.2f\n", "Bagel", bagel);
    printf("%-8s%7.2f\n", "Total", coffee + bagel);
    return 0;
}
Output
Item      Price
Coffee     3.50
Bagel      2.25
Total      5.75
Explain it simply

Why does scanf("%d", &age) need the & when printf("%d", age) doesn't? Explain it with mail.

Reveal a plain-language answer

printf only needs to read the value, so you can hand it a photocopy. scanf has to deliver something into your variable, and to deliver mail you need the address, not a copy of the letter. &age is the address of age, so scanf knows where to put the number it reads.

6. Control Structures

Statements normally run in order, top to bottom. Control structures let a program make decisions, choosing which statements to run based on a condition. This is what allows a program to react to its data.

The if statement evaluates a condition in parentheses and runs the following statement, or brace-enclosed block, only if the condition is nonzero (true). else if tests further conditions in order, and a final else handles everything else. Only the first matching branch runs. Braces are optional around a single statement, but always using them avoids a classic bug in which a second, indented line appears to belong to the if but actually always runs.

The switch statement compares one integer value (including a char) against a list of constant case labels and jumps to the matching one; default handles anything unmatched. Crucially, execution falls through from one case into the next unless you end each case with break. Forgetting a break is one of the most common C bugs, though fall-through is occasionally used on purpose to share code between cases. For a compact two-way choice inside an expression, C has the ternary operator: condition ? a : b.

Branch
One of the alternative paths a control structure can take.
Fall-through
In switch, running on into the next case because there was no break.
Ternary operator
cond ? a : b, an expression that picks one of two values.

if / else if / else

int score = 75;

if (score >= 90) {
    printf("A\n");
} else if (score >= 75) {
    printf("B\n");
} else {
    printf("C or below\n");
}

switch

int day = 2;

switch (day) {
    case 1:  printf("Monday\n");    break;
    case 2:  printf("Tuesday\n");   break;
    case 5:  printf("Friday\n");    break;
    default: printf("Other day\n"); break;
}

Ternary Operator

int max = (a > b) ? a : b;   /* condition ? if_true : if_false */
Exercise — your turn

Finish grade with if/else if (90+ A, 80+ B, 70+ C, 60+ D, else F), write the switch for days 1–3, and use the ternary operator for even/odd.

#include <stdio.h>

char grade(int score) {
    /* TODO: if / else if / else returning 'A', 'B', 'C', 'D' or 'F' */
}

int main(void) {
    int day = 3;
    int n = 7;

    printf("%c %c %c %c\n", grade(95), grade(82), grade(64), grade(40));

    /* TODO: switch (day) printing "Day 1 is Monday" ... "Day 3 is Wednesday",
             default "Some other day" -- remember break! */

    printf("%d is %s\n", n, "");  /* TODO: "even" or "odd" with ? : */
    return 0;
}
Show solution
#include <stdio.h>

char grade(int score) {
    if (score >= 90)
        return 'A';
    else if (score >= 80)
        return 'B';
    else if (score >= 70)
        return 'C';
    else if (score >= 60)
        return 'D';
    else
        return 'F';
}

int main(void) {
    int day = 3;
    int n = 7;

    printf("%c %c %c %c\n", grade(95), grade(82), grade(64), grade(40));

    switch (day) {
        case 1:  printf("Day 1 is Monday\n");    break;
        case 2:  printf("Day 2 is Tuesday\n");   break;
        case 3:  printf("Day 3 is Wednesday\n"); break;
        default: printf("Some other day\n");     break;
    }

    printf("%d is %s\n", n, n % 2 == 0 ? "even" : "odd");
    return 0;
}
Output
A B D F
Day 3 is Wednesday
7 is odd
Explain it simply

What goes wrong if you leave out every break in the day-of-week switch? Explain it with a staircase.

Reveal a plain-language answer

A switch is a staircase with a door at each step. The case label is where you enter, and without break you keep walking down through every step below it. With day = 1 and no breaks, you'd print Monday, Tuesday, Wednesday, and “Some other day”. break is the exit door right after your step.

7. Loops

A loop repeats a block of code while a condition holds. Loops let a short program do a great deal of work: processing each element of an array, each character of a string, or each line of input. Each pass through the loop body is an iteration.

C has three loops. The for loop packs its bookkeeping into one line of three parts separated by semicolons: an initialisation run once at the start, a condition tested before every iteration, and an update run after every iteration, as in for (i = 0; i < n; i++). It is the natural choice when you know how many times to repeat. The while loop tests its condition before each iteration and is used when the number of repetitions isn't known in advance, such as reading until the end of input. The do … while loop tests its condition after each iteration, so its body always runs at least once.

Inside a loop, break exits immediately and continue jumps to the next iteration. Two errors are especially common in C. The off-by-one error loops once too often or too few times, typically by writing <= where < was needed, and with arrays it reads past the end. The infinite loop runs forever because the condition never becomes false, usually because the loop variable is never updated.

Iteration
One pass through a loop's body.
Loop counter
A variable such as i that tracks which iteration is running.
Off-by-one error
Looping one time too many or too few, often <= vs <.

for

for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}
/* 0 1 2 3 4 */

while

int i = 0;
while (i < 5) {
    printf("%d\n", i);
    i++;
}

do / while

int i = 0;
do {
    printf("%d\n", i);
    i++;
} while (i < 5);   /* always runs at least once */
Use break to exit a loop early, and continue to skip to the next iteration.
Exercise — your turn

Write FizzBuzz for 1 to 15 on one line (“Fizz” for multiples of 3, “Buzz” for 5, “FizzBuzz” for both), then use a while loop to count down 3, 2, 1 before “Liftoff!”.

#include <stdio.h>

int main(void) {
    int i;
    int count = 3;

    /* TODO: for i from 1 to 15, print Fizz / Buzz / FizzBuzz / the number,
             followed by a space (or a newline after the last one) */

    /* TODO: while count > 0, print count and a space, then decrease it */
    printf("Liftoff!\n");
    return 0;
}
Show solution
#include <stdio.h>

int main(void) {
    int i;
    int count = 3;

    for (i = 1; i <= 15; i++) {
        if (i % 15 == 0)
            printf("FizzBuzz");
        else if (i % 3 == 0)
            printf("Fizz");
        else if (i % 5 == 0)
            printf("Buzz");
        else
            printf("%d", i);
        printf(i < 15 ? " " : "\n");
    }

    while (count > 0) {
        printf("%d ", count);
        count--;
    }
    printf("Liftoff!\n");
    return 0;
}
Output
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
3 2 1 Liftoff!
Explain it simply

When would you choose do … while over while? Explain it with a vending machine.

Reveal a plain-language answer

A vending machine should always show its menu at least once before asking “anything else?”. That's do … while: do the thing, then check whether to repeat. A plain while checks first, like a bouncer who might not let you in at all. Use do … while whenever the body must run once before the question even makes sense.

8. Functions

A function is a named block of code that performs one task and can be called from anywhere in the program. Functions let you split a large program into small, understandable pieces, give each piece a descriptive name, and reuse it instead of repeating code. main is itself a function; so are printf and strlen.

A function definition gives the return type, name, parameter list, and body: int max(int a, int b) { … }. A function that returns nothing has the return type void. Because C compilers read files from top to bottom, a function must be declared before it is called. If you want to define it later, for example after main, you first write a prototype: the function's header followed by a semicolon, int max(int a, int b);. Header files are mostly collections of prototypes.

C passes every argument by value: the function receives a copy, so changing a parameter inside the function has no effect on the caller's variable. To let a function modify a caller's variable, you pass its address instead (a pointer, section 10). A function may also call itself, which is called recursion. Every recursive function needs a base case that stops the recursion, such as factorial(1) = 1. Variables declared inside a function are local and disappear when the function returns.

Prototype
A function declaration without a body, telling the compiler its name and types.
Pass by value
The function receives a copy of each argument, not the original.
Recursion
A function calling itself on a smaller problem until a base case is reached.
#include <stdio.h>

/* Function declaration (prototype) — tells compiler about the function */
int add(int a, int b);

int main(void) {
    printf("%d\n", add(3, 4));   /* 7 */
    return 0;
}

/* Function definition */
int add(int a, int b) {
    return a + b;
}

/* void function — no return value */
void greet(char *name) {
    printf("Hello, %s!\n", name);
}
C passes arguments by value — the function gets a copy. To modify the original, pass a pointer.
Exercise — your turn

The prototypes and main are written. Implement max, a recursive factorial, and add_one, then notice why x doesn't change.

#include <stdio.h>

int max(int a, int b);
long factorial(int n);
void add_one(int x);

int main(void) {
    int x = 10;

    printf("max(4, 9) = %d\n", max(4, 9));
    printf("5! = %ld\n", factorial(5));
    add_one(x);
    printf("x is still %d\n", x);
    return 0;
}

/* TODO: define max (use ? :), factorial (recursive, base case n <= 1)
         and add_one (x = x + 1) */
Show solution
#include <stdio.h>

int max(int a, int b);
long factorial(int n);
void add_one(int x);

int main(void) {
    int x = 10;

    printf("max(4, 9) = %d\n", max(4, 9));
    printf("5! = %ld\n", factorial(5));
    add_one(x);
    printf("x is still %d\n", x);
    return 0;
}

int max(int a, int b) {
    return a > b ? a : b;
}

long factorial(int n) {
    if (n <= 1)
        return 1;
    return n * factorial(n - 1);
}

void add_one(int x) {
    x = x + 1;   /* changes only the local copy */
}
Output
max(4, 9) = 9
5! = 120
x is still 10
Explain it simply

Why doesn't add_one(x) change x in main? Explain it with a photocopy.

Reveal a plain-language answer

When you call add_one(x), C doesn't hand over your original page; it makes a photocopy of the number 10 and gives that to the function. The function scribbles 11 on the photocopy and throws it away when it returns. Your original still says 10. To let the function change the original, you'd give it the page's location, its address, instead.

9. Arrays & Strings

An array is a fixed-size sequence of elements of the same type, stored side by side in memory. int scores[4]; reserves room for four integers, which you reach by index from scores[0] to scores[3]. The size is fixed when the array is created. Crucially, C does not check bounds: writing scores[4] silently reads or overwrites whatever memory comes next. This is undefined behaviour and the source of many real-world security bugs. For an array in the same function, sizeof(arr) / sizeof(arr[0]) gives the element count.

C has no string type. A string is an array of char terminated by the null character '\0', a byte with value zero that marks where the text ends. The literal "Ada" therefore occupies four bytes: 'A', 'd', 'a', '\0'. Every string function relies on that terminator: strlen counts characters until it finds it, and printf("%s") prints until it finds it. A string without one sends these functions running off into unrelated memory.

Because strings are arrays, you cannot copy or compare them with = or ==. Instead you use functions from <string.h>: strcpy copies, strcat appends, strcmp compares (returning 0 when equal), and strlen measures. None of them know how big your destination array is, so it is your job to make sure it has room for the result plus the terminator. Safer bounded versions such as strncpy and snprintf take the size as an argument.

Bounds checking
Verifying an index is inside the array; C does not do this for you.
Null terminator
The '\0' byte that marks the end of every C string.
Buffer overflow
Writing past the end of an array, overwriting neighbouring memory.

Arrays

int nums[5] = {10, 20, 30, 40, 50};
nums[0];                   /* 10 — zero-indexed */
nums[4];                   /* 50 */
nums[2] = 99;              /* update value */

/* Iterate with a for loop */
for (int i = 0; i < 5; i++) {
    printf("%d\n", nums[i]);
}

/* 2D array */
int grid[2][3] = {{1,2,3}, {4,5,6}};
grid[1][2];                /* 6 */

Strings

#include <string.h>

/* A string is a char array ending with '\0' */
char name[20] = "Alice";
char greeting[] = "Hello";   /* size inferred */

strlen(name);               /* 5 — length (not counting '\0') */
strcpy(dest, src);          /* copy src into dest */
strcat(dest, src);          /* append src to dest */
strcmp(a, b);               /* 0 if equal, <0 or >0 otherwise */
strncpy(dest, src, 10);     /* safer: copy at most n chars */
Always ensure destination buffers are large enough. Buffer overflows are a common and dangerous bug in C.
Exercise — your turn

Compute the sum and average of the scores, build “Hello, Ada” with strcpy and strcat, and reverse word in place by swapping characters from both ends.

#include <stdio.h>
#include <string.h>

int main(void) {
    int scores[] = {88, 92, 75, 95};
    int n = sizeof(scores) / sizeof(scores[0]);
    int sum = 0;
    int i;
    char greeting[32];
    char word[] = "stressed";
    int len = strlen(word);

    /* TODO: add up scores into sum */
    printf("Sum: %d, Average: %.2f\n", sum, (double)sum / n);

    /* TODO: copy "Hello, " into greeting, then append "Ada" */
    printf("%s (%d chars)\n", greeting, (int)strlen(greeting));

    /* TODO: swap word[i] with word[len - 1 - i] for the first half */
    printf("Reversed: %s\n", word);
    return 0;
}
Show solution
#include <stdio.h>
#include <string.h>

int main(void) {
    int scores[] = {88, 92, 75, 95};
    int n = sizeof(scores) / sizeof(scores[0]);
    int sum = 0;
    int i;
    char greeting[32];
    char word[] = "stressed";
    int len = strlen(word);

    for (i = 0; i < n; i++)
        sum += scores[i];
    printf("Sum: %d, Average: %.2f\n", sum, (double)sum / n);

    strcpy(greeting, "Hello, ");
    strcat(greeting, "Ada");
    printf("%s (%d chars)\n", greeting, (int)strlen(greeting));

    for (i = 0; i < len / 2; i++) {
        char tmp = word[i];
        word[i] = word[len - 1 - i];
        word[len - 1 - i] = tmp;
    }
    printf("Reversed: %s\n", word);
    return 0;
}
Output
Sum: 350, Average: 87.50
Hello, Ada (10 chars)
Reversed: desserts
Explain it simply

Why does "Ada" take four bytes, not three? Explain it with a train.

Reveal a plain-language answer

A C string is a train of character carriages with no sign saying how long it is. The only way anyone knows where the train ends is the caboose, the '\0' byte. strlen and printf walk along the carriages until they hit the caboose. So “Ada” needs three carriages plus the caboose. Forget the caboose and they keep walking into whatever is parked behind.

10. Pointers

Every variable lives somewhere in memory, and every location in memory has a numeric address. A pointer is a variable whose value is an address, the location of some other data. Pointers are C's most powerful and most notorious feature, and they are the key to how C passes data around efficiently, builds data structures, and manages memory.

Two operators work with pointers. The address-of operator & gives the address of a variable: &a. The dereference operator * goes the other way. Given a pointer, it reaches the value stored at that address, for reading or writing. A pointer's type records what it points to: int *p declares p as a pointer to an int. So after p = &a;, writing *p = 5; changes a itself. This is how a function can modify its caller's variables: instead of a copy of the value, pass a copy of its address, as the classic swap(&a, &b) does.

Pointers and arrays are closely linked. In most expressions an array's name converts to a pointer to its first element, and pointer arithmetic moves in whole elements: p + 2 points two ints further along, so *(p + 2) is exactly arr[2]. The special value NULL means “points to nothing”. Dereferencing NULL, an uninitialised pointer, or a pointer to memory that has been freed is undefined behaviour, and usually crashes the program.

Address
The numeric location of a byte in memory; &x gives x's address.
Dereference
Following a pointer to the value it points to, written *p.
NULL
A pointer value meaning “no object”; it must never be dereferenced.
int  x   = 42;
int *ptr = &x;    /* ptr holds the address of x */

printf("%p\n", ptr);    /* prints the address */
printf("%d\n", *ptr);   /* 42 — dereference: read the value at the address */

*ptr = 99;              /* change x through the pointer */
printf("%d\n", x);      /* 99 */

/* Passing a pointer to a function allows it to modify the original */
void double_it(int *n) {
    *n = *n * 2;
}
double_it(&x);          /* x is now 198 */

/* Array name is a pointer to its first element */
int arr[] = {1, 2, 3};
int *p = arr;
printf("%d\n", *(p + 1));  /* 2 — pointer arithmetic */
& means "address of" — * means "value at address" (dereference).
Exercise — your turn

Write swap so it exchanges the caller's two integers through pointers, then use pointer arithmetic on p to print the third array element.

#include <stdio.h>

void swap(int *a, int *b) {
    /* TODO: exchange the values *a and *b (you need a temporary) */
}

int main(void) {
    int a = 1, b = 2;
    int nums[] = {10, 20, 30, 40};
    int *p = nums;

    printf("Before: a=%d, b=%d\n", a, b);
    /* TODO: call swap with the addresses of a and b */
    printf("After: a=%d, b=%d\n", a, b);

    printf("Third element via pointer: %d\n", 0);  /* TODO: use *(p + ...) */
    return 0;
}
Show solution
#include <stdio.h>

void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void) {
    int a = 1, b = 2;
    int nums[] = {10, 20, 30, 40};
    int *p = nums;

    printf("Before: a=%d, b=%d\n", a, b);
    swap(&a, &b);
    printf("After: a=%d, b=%d\n", a, b);

    printf("Third element via pointer: %d\n", *(p + 2));
    return 0;
}
Output
Before: a=1, b=2
After: a=2, b=1
Third element via pointer: 30
Explain it simply

Explain the difference between p, *p, and &a using houses on a street.

Reveal a plain-language answer

a is a house with something inside. &a is that house's street address, written on a slip of paper. A pointer p is a variable that holds such a slip. p on its own is the address itself. *p means “go to that address and look inside”, so reading or changing *p reads or changes what's in house a.

11. Structs

Real things have several properties at once. A point has an x and a y; a student has a name, an ID, and a grade. A struct (structure) is a user-defined type that groups several related variables, called members, under one name, and the members may each have a different type. Where an array holds many values of the same type reached by number, a struct holds a fixed set of named values of possibly different types.

You define a struct with struct Point { int x; int y; };, then declare variables of that type with struct Point p;. Writing struct every time gets tedious, so C programmers usually add a typedef, which gives the type a plain name: typedef struct { int x; int y; } Point; lets you write Point p;. A struct can be initialised with a brace list, Point p = {3, 4};, and its members are accessed with the dot operator: p.x. Structs can contain other structs, arrays, and pointers, which is how complex data such as records and linked lists is built.

Unlike arrays, structs can be assigned with = and passed to and returned from functions. But like everything in C they are passed by value, so the function gets a copy. That is fine for reading a small struct, but to modify the caller's struct, or to avoid copying a large one, you pass a pointer to it. Accessing a member through a pointer is so common that C has a dedicated arrow operator: p->x is shorthand for (*p).x.

Member
One of the named fields inside a struct.
typedef
Creates an alias for a type, e.g. so struct Point can be written Point.
Arrow operator
ptr->member, accessing a member through a pointer to a struct.
#include <stdio.h>
#include <string.h>

/* Define the struct type */
struct Person {
    char  name[50];
    int   age;
    float height;
};

int main(void) {
    /* Declare and initialise */
    struct Person p1;
    strcpy(p1.name, "Alice");
    p1.age    = 30;
    p1.height = 1.68f;

    printf("%s is %d years old.\n", p1.name, p1.age);

    /* Pointer to struct — use -> to access members */
    struct Person *ptr = &p1;
    printf("%.2f\n", ptr->height);

    return 0;
}

/* typedef makes the syntax cleaner */
typedef struct {
    char name[50];
    int  age;
} Person;

Person p2 = {"Bob", 25};   /* no 'struct' keyword needed */
Exercise — your turn

The types are defined. Implement move using the arrow operator so it changes the caller's point, and area using the dot operator.

#include <stdio.h>

typedef struct {
    int x;
    int y;
} Point;

typedef struct {
    Point corner;
    int width;
    int height;
} Rect;

void move(Point *p, int dx, int dy) {
    /* TODO: add dx to x and dy to y, using p-> */
}

int area(Rect r) {
    /* TODO: return width times height */
}

int main(void) {
    Point p = {3, 4};
    Rect r = {{0, 0}, 5, 2};

    printf("Point: (%d, %d)\n", p.x, p.y);
    move(&p, 2, -3);
    printf("Moved: (%d, %d)\n", p.x, p.y);
    printf("Rect area: %d\n", area(r));
    return 0;
}
Show solution
#include <stdio.h>

typedef struct {
    int x;
    int y;
} Point;

typedef struct {
    Point corner;
    int width;
    int height;
} Rect;

void move(Point *p, int dx, int dy) {
    p->x += dx;
    p->y += dy;
}

int area(Rect r) {
    return r.width * r.height;
}

int main(void) {
    Point p = {3, 4};
    Rect r = {{0, 0}, 5, 2};

    printf("Point: (%d, %d)\n", p.x, p.y);
    move(&p, 2, -3);
    printf("Moved: (%d, %d)\n", p.x, p.y);
    printf("Rect area: %d\n", area(r));
    return 0;
}
Output
Point: (3, 4)
Moved: (5, 1)
Rect area: 10
Explain it simply

Why does move take a Point * while area takes a plain Rect? Explain it with a form.

Reveal a plain-language answer

area only needs to read the rectangle, so a photocopy of the form is fine. move has to change the point, and changing a photocopy would do nothing to the original. So you give move the location of the original form (a pointer), and it writes on that one with p->x.

12. Dynamic Memory

So far every variable has had a size known when the program was written, and has lived on the stack, a region of memory that the compiler manages automatically. Local variables are created when a function starts and destroyed when it returns. But often you don't know how much memory you need until the program is running, such as how many lines are in a file or how many users will sign up. For that, C lets you request memory from the heap, a large pool that you manage yourself.

The functions in <stdlib.h> manage heap memory. malloc(bytes) allocates a block of the given size and returns a pointer to it; the contents start out as garbage. calloc(count, size) does the same but zeroes the memory. realloc(ptr, new_bytes) resizes a block, moving it if necessary. It returns the new location, and returns NULL on failure while leaving the old block untouched, so assign the result to a temporary pointer first. Always compute sizes with sizeof, as in n * sizeof(int), and always check for NULL, which these functions return when memory can't be allocated.

Every block you allocate must eventually be released with free, exactly once. Forgetting leaves a memory leak: memory the program can no longer use but never gave back. That matters for long-running programs such as servers. Using memory after freeing it (a use-after-free) or freeing it twice (a double free) is undefined behaviour, and a classic source of crashes and security holes. A good habit is to set a pointer to NULL right after freeing it. Tools such as valgrind or the compiler flag -fsanitize=address will find these mistakes for you.

Stack vs. heap
Automatic memory for locals / manually managed memory for runtime-sized data.
Memory leak
Allocated memory that is never freed.
Use-after-free
Accessing memory through a pointer after it has been freed; undefined behaviour.
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    int n = 5;

    /* malloc — allocate n ints, uninitialised */
    int *arr = (int *)malloc(n * sizeof(int));

    if (arr == NULL) {
        fprintf(stderr, "Allocation failed\n");
        return 1;
    }

    for (int i = 0; i < n; i++)
        arr[i] = i * i;

    /* realloc — resize the allocation */
    arr = (int *)realloc(arr, 10 * sizeof(int));

    /* calloc — allocate and zero-initialise */
    int *zeroed = (int *)calloc(n, sizeof(int));

    /* free — ALWAYS free when done */
    free(arr);
    free(zeroed);

    return 0;
}
Every malloc / calloc must have a matching free. Forgetting to free memory causes memory leaks. Never access a pointer after freeing it.
Exercise — your turn

Allocate room for 5 ints, fill them with squares, then realloc to 8 and fill the rest. Check every allocation for NULL, and free the memory at the end.

#include <stdio.h>
#include <stdlib.h>

void print_all(const int *arr, int n) {
    int i;
    for (i = 0; i < n; i++)
        printf(i < n - 1 ? "%d " : "%d\n", arr[i]);
}

int main(void) {
    int n = 5;
    int i;
    int *arr = NULL;   /* TODO: malloc room for n ints, and check for NULL */
    int *bigger;

    for (i = 0; i < n; i++)
        arr[i] = i * i;
    print_all(arr, n);

    /* TODO: realloc arr to 8 ints via `bigger` (free arr and return 1 if it fails),
             then fill elements 5..7 with their squares */
    print_all(arr, 8);

    /* TODO: free the memory and set arr to NULL */
    printf("Freed.\n");
    return 0;
}
Show solution
#include <stdio.h>
#include <stdlib.h>

void print_all(const int *arr, int n) {
    int i;
    for (i = 0; i < n; i++)
        printf(i < n - 1 ? "%d " : "%d\n", arr[i]);
}

int main(void) {
    int n = 5;
    int i;
    int *arr = malloc(n * sizeof(int));
    int *bigger;

    if (arr == NULL) {
        fprintf(stderr, "Allocation failed\n");
        return 1;
    }
    for (i = 0; i < n; i++)
        arr[i] = i * i;
    print_all(arr, n);

    bigger = realloc(arr, 8 * sizeof(int));
    if (bigger == NULL) {
        free(arr);
        return 1;
    }
    arr = bigger;
    for (i = n; i < 8; i++)
        arr[i] = i * i;
    print_all(arr, 8);

    free(arr);
    arr = NULL;
    printf("Freed.\n");
    return 0;
}
Output
0 1 4 9 16
0 1 4 9 16 25 36 49
Freed.
Explain it simply

What's the difference between the stack and the heap? Explain it with a hotel.

Reveal a plain-language answer

The stack is like a hotel that checks you out automatically: when a function ends, its rooms (local variables) are cleaned and reused, with no action needed. The heap is long-term rental: malloc signs a lease for exactly the space you ask for, for as long as you like, but nobody will end it for you. You must hand back the keys with free. Forget, and the room stays rented and empty forever (a leak). Keep using the keys after handing them back, and you walk in on the next tenant (use-after-free).