A quick-reference and hands-on guide to the fundamentals of ANSI C programming.
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:
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.
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.
gcc) that translates C source into a machine-code executable..c extension; headers use .hgcc main.c -o main)main() function;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”.
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.
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.
#include, #define, and other # lines..h file declaring functions and types, such as <stdio.h>.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
<stdio.h> (I/O), <stdlib.h> (memory, exit), <string.h> (strings), <math.h> (maths).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 */
}
#include <stdio.h>
int main(void) {
printf("Hello, C!\n");
return 0;
}
Hello, C!
What would happen if you deleted #include <stdio.h>? Explain it with a phone book.
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.
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.
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 */
| Type | Size (typical) | Range / Use |
|---|---|---|
char | 1 byte | Single character or small integer |
int | 4 bytes | Whole numbers (~±2 billion) |
long | 4–8 bytes | Larger whole numbers |
float | 4 bytes | ~6–7 decimal digits of precision |
double | 8 bytes | ~15 decimal digits of precision |
unsigned int | 4 bytes | Non-negative integers only |
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;
}
#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;
}
Ada is 36 years old and 1.65 m tall. Grade: A Years to 120: 84
Why does C make you state a variable's type up front, when Python doesn't? Explain it with a warehouse.
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.
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.
7 / 2 is 3.(type)value, such as (double)n.0 is false and any other value is true.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 */
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 */
x += 5; /* x = x + 5 */
x -= 2; /* x = x - 2 */
x *= 3; /* x = x * 3 */
x /= 4; /* x = x / 4 */
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;
}
#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;
}
Full boxes: 3 Left over: 2 7 / 2 = 3, but (double)7 / 2 = 3.50 total = 31
Why is 7 / 2 equal to 3 in C, and how do you get 3.5? Explain it with coins.
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.
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.
printf/scanf: text plus % conversion specifiers.%7.2f.\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 specifier | Type |
|---|---|
%d | int |
%f | float / double |
%c | char |
%s | string (char array) |
%p | pointer address |
%ld | long |
\n | newline |
\t | tab |
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;
}
#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;
}
Item Price Coffee 3.50 Bagel 2.25 Total 5.75
Why does scanf("%d", &age) need the & when printf("%d", age) doesn't? Explain it with mail.
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.
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.
switch, running on into the next case because there was no break.cond ? a : b, an expression that picks one of two values.int score = 75;
if (score >= 90) {
printf("A\n");
} else if (score >= 75) {
printf("B\n");
} else {
printf("C or below\n");
}
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;
}
int max = (a > b) ? a : b; /* condition ? if_true : if_false */
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;
}
#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;
}
A B D F Day 3 is Wednesday 7 is odd
What goes wrong if you leave out every break in the day-of-week switch? Explain it with a staircase.
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.
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.
i that tracks which iteration is running.<= vs <.for (int i = 0; i < 5; i++) {
printf("%d\n", i);
}
/* 0 1 2 3 4 */
int i = 0;
while (i < 5) {
printf("%d\n", i);
i++;
}
int i = 0;
do {
printf("%d\n", i);
i++;
} while (i < 5); /* always runs at least once */
break to exit a loop early, and continue to skip to the next iteration.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;
}
#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;
}
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 3 2 1 Liftoff!
When would you choose do … while over while? Explain it with a vending machine.
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.
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.
#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);
}
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) */
#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 */
}
max(4, 9) = 9 5! = 120 x is still 10
Why doesn't add_one(x) change x in main? Explain it with a photocopy.
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.
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.
'\0' byte that marks the end of every C string.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 */
#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 */
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;
}
#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;
}
Sum: 350, Average: 87.50 Hello, Ada (10 chars) Reversed: desserts
Why does "Ada" take four bytes, not three? Explain it with a train.
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.
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.
&x gives x's address.*p.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).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;
}
#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;
}
Before: a=1, b=2 After: a=2, b=1 Third element via pointer: 30
Explain the difference between p, *p, and &a using houses on a street.
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.
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.
struct Point can be written Point.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 */
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;
}
#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;
}
Point: (3, 4) Moved: (5, 1) Rect area: 10
Why does move take a Point * while area takes a plain Rect? Explain it with a form.
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.
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.
#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;
}
malloc / calloc must have a matching free. Forgetting to free memory causes memory leaks. Never access a pointer after freeing it.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;
}
#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;
}
0 1 4 9 16 0 1 4 9 16 25 36 49 Freed.
What's the difference between the stack and the heap? Explain it with a hotel.
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).