Module 11 · Pointers and Memory
Memory and Addresses: Every Box Has a Number
In this lesson
- Print where a variable or an array box lives with
&and%p, and explain the(void *)cast. - Read two runs of one program: say what changes, what stays, and why an array steps by
sizeofone box. - Explain why
scanfneeds&agebut not an array's name, on a picture of memory with its stack and static area.
Maria has typed scanf("%d", &n) for eight modules, and she still asks the question Module 3 left open. Why does scanf need that ampersand, when printf never does? Module 3 said it hands over an address, but nobody has shown her one. This lesson prints real ones, from two runs of the Playground, and then answers her in one sentence.
Memory is a row of numbered bytes
A byte is 8 bits, the unit sizeof counts in (Module 2 lesson 5). A running program's memory is a very long row of bytes. Each byte has a number, its position in the row, called its address.
A variable takes as many bytes as its type needs, side by side. Its address is the address of its first byte. Module 2 lesson 1 printed three addresses from one run and promised nothing about them. This lesson measures them.
So every variable has three facts: its value, its size, and its address.
The address-of operator gives a variable's address
An ampersand in front of a variable's name gives its address. The & is the address-of operator, and &a reads "the address of a". It works on anything that names a box: a variable, or one box such as &marks[2].
Printing an address
printf("%p\n", (void *)&name);
&nameis the address ofname: where it lives, not what it holds.%pprints an address, on the Playground as0xand hexadecimal digits.(void *)is a cast:%pexpects avoid *, C's general address type, and the cast makes one.
Hexadecimal is Module 1 lesson 4's base 16: after 9 come a to f, worth 10 to 15. The 0x marks it, as in Module 3 lesson 1.
Here is a program that prints seven addresses: three variables, then the four boxes of an array.
#include <stdio.h>
int main(void)
{
int a = 1;
int b = 2;
double d = 3.0;
int marks[4] = {70, 80, 90, 60};
printf("&a = %p\n", (void *)&a);
printf("&b = %p\n", (void *)&b);
printf("&d = %p\n", (void *)&d);
for (int i = 0; i < 4; i++) {
printf("&marks[%d] = %p\n", i, (void *)&marks[i]);
}
printf("%d %d %.1f %d\n", a, b, d, marks[0]);
return 0;
}
Trace it first. This table's addresses are made up, small and in decimal, so that you can read them. The real ones are hexadecimal and change every run.
| Name | Type | Bytes | Made-up address | Value |
|---|---|---|---|---|
a | int | 4 | 1000 | 1 |
b | int | 4 | 1004 | 2 |
d | double | 8 | 1008 | 3.0 |
marks[0] | int | 4 | 1016 | 70 |
marks[1] | int | 4 | 1020 | 80 |
marks[2] | int | 4 | 1024 | 90 |
marks[3] | int | 4 | 1028 | 60 |
The last line prints 1 2 3.0 70 on every run. Only the seven addresses move.
So &x is a number: the address of the first byte of x.
Two runs on the Playground: new numbers, same distances
Here are two runs of that program on the Playground, with nothing changed between them. The last column is each address's distance from &a, in bytes.
| Line | Run 1 | Run 2 | Bytes after &a |
|---|---|---|---|
&a | 0x7ffe8f424bc0 | 0x7ffddb90d230 | 0 |
&b | 0x7ffe8f424bc4 | 0x7ffddb90d234 | 4 |
&d | 0x7ffe8f424bc8 | 0x7ffddb90d238 | 8 |
&marks[0] | 0x7ffe8f424bd0 | 0x7ffddb90d240 | 16 |
&marks[1] | 0x7ffe8f424bd4 | 0x7ffddb90d244 | 20 |
&marks[2] | 0x7ffe8f424bd8 | 0x7ffddb90d248 | 24 |
&marks[3] | 0x7ffe8f424bdc | 0x7ffddb90d24c | 28 |
To read a distance, compare the last digits. bc0 to bc4 is 4. bc8 to bd0 is 8, because after c8 come c9, ca to cf, then d0. And c is 12, so bd8 to bdc is 4.
Every address changed; every distance stayed. The cause is ASLR, address space layout randomisation. On each run, the system starts the program's memory at a random spot. So a printed address is true only for the run that printed it.
The distances hold two kinds of fact. The step of 4 inside marks is a rule of C, because an array's boxes are contiguous (Module 9 lesson 01). The order of a, b and d is only GCC's choice. Module 2 lesson 1's run put its three ints in falling order, and that was allowed too.
So compare addresses inside one run, never across two.
An array steps by sizeof one box
Module 9 lesson 01 promised that Module 11 would print where box i starts. The runs above did: the boxes of marks sit 4 bytes apart. The step is sizeof marks[0], the size of one box.
The step depends on the type. One Playground run of a program printing box 0 and box 1 of four small arrays gave this.
| Type of the array | Box 0 | Box 1 | Step in bytes |
|---|---|---|---|
int | 0x7ffd8d6d3850 | 0x7ffd8d6d3854 | 4 |
char | 0x7ffd8d6d384d | 0x7ffd8d6d384e | 1 |
double | 0x7ffd8d6d3860 | 0x7ffd8d6d3868 | 8 |
long long | 0x7ffd8d6d3880 | 0x7ffd8d6d3888 | 8 |
That run's sizeof line said 4, 1, 8 and 8, the same as the steps. It also gave the size of an address itself: 8 bytes, whatever it leads to.
So box i starts i times sizeof one box after box 0, and that is how the machine finds marks[i].
The memory picture: code, the static area and the stack
Module 7 lesson 5 promised a proper drawing of memory. Here it is, for a program on the Playground's Linux, with the lowest addresses at the bottom.
The code is your program's instructions. The static area holds globals and static locals for the whole run (Module 7 lesson 5). The stack holds one frame per running call, with its parameters and locals.
Module 8 drew the newest frame on top of the pile. In addresses it sits lower, because the stack grows downwards (Module 8 lesson 02). The heap, memory asked for while the program runs, is Module 14.
Every address in this lesson starts with 0x7ff, because a, b, d and marks are locals of main, and Linux puts the stack there. At -O2 GCC keeps many locals in registers (Module 7 lesson 5). A printed address, though, must lead to a real box.
So a local lives in its call's frame on the stack, and a global or static in the static area.
A pointer is a variable that holds an address
An address is a number, so a variable can hold one. A variable that holds an address is a pointer. If it holds &score, it points at score.
Addresses have types. For int score, &score has the type int *, read "pointer to int". That is a pointer type. So GCC's 'int *' in Module 9 and the char * of string.h in Module 10 were addresses all along.
Lesson 02 declares pointers and reads through them. The shape, for now: int *where = &score; makes a pointer where that holds the address of score.
So &score is an address of type int *, and a pointer keeps one.
Why scanf needs the ampersand
Now Maria's question. A call copies each argument's value (Module 7 lesson 3), and writing into a copy changes nothing. scanf must write into your variable, so it needs to know where the variable lives. &age tells it.
#include <stdio.h>
void set_to_31(int n);
int main(void)
{
int age = 0;
set_to_31(age);
printf("after set_to_31: age = %d\n", age);
scanf("%d", &age);
printf("after scanf: age = %d\n", age);
return 0;
}
void set_to_31(int n)
{
n = 31;
printf("inside set_to_31: n = %d\n", n);
}
Trace it for the input 27, with a made-up address for age.
| Moment | What the call received | age, at 1000 | n, in the frame of set_to_31 |
|---|---|---|---|
| Before any call | 0 | does not exist yet | |
Inside set_to_31, after n = 31 | the value 0, a copy | 0 | 31 |
After set_to_31 returns | 0 | gone with its frame | |
After scanf reads 27 | 1000, the address of age | 27 |
inside set_to_31: n = 31
after set_to_31: age = 0
after scanf: age = 27
That output is for the input 27. set_to_31 changed its own copy. scanf got the address of age and wrote 27 into the box there. That answers Module 7 lesson 3's teaser too: scanf has always changed your variables through their addresses. Lesson 04 shows you how to write such a function.
Module 3 lesson 4 named one exception: scanf("%19s", word) has no ampersand. Module 9 lesson 05 said a function handed an array gets where its boxes are. That value now has its name: the address of box 0. This program prints it three ways.
#include <stdio.h>
#define BOXES 3
void show_where(int a[], int n);
int main(void)
{
int marks[BOXES] = {40, 55, 72};
printf("main: marks = %p\n", (void *)marks);
printf("main: &marks[0] = %p\n", (void *)&marks[0]);
show_where(marks, BOXES);
return 0;
}
void show_where(int a[], int n)
{
printf("show_where: a = %p, n = %d\n", (void *)a, n);
}
Trace it with made-up addresses. The frame of show_where sits lower than main's.
| Name | Where it lives | Made-up address | What it holds |
|---|---|---|---|
marks[0] | main's frame | 1000 | 40 |
marks[1] | main's frame | 1004 | 55 |
marks[2] | main's frame | 1008 | 72 |
a | the frame of show_where | 960 | 1000, the address of marks[0] |
n | the frame of show_where | 968 | 3, a copy |
One run on Compiler Explorer printed the same address on all three lines. Your number will differ, and your three lines will still match. The parameter a holds the address of marks[0], which is how Module 9's functions reached main's boxes.
So the ampersand rule never broke: & asks where a box lives, and an array's name already says it. Module 3's scanf("%d", roll) passed the number inside roll, and scanf treated it as an address.
The smallest program about memory: sizeof for four variables and one address.
#include <stdio.h>
#define BOXES 4
int main(void)
{
int score = 87;
double price = 7.5;
char grade = 'B';
int marks[BOXES] = {70, 80, 90, 60};
printf("what bytes\n");
printf("score %5zu\n", sizeof score);
printf("price %5zu\n", sizeof price);
printf("grade %5zu\n", sizeof grade);
printf("marks %5zu\n", sizeof marks);
printf("&score %5zu\n", sizeof &score);
return 0;
}
what bytes
score 4
price 8
grade 1
marks 16
&score 8
marks is four boxes of 4 bytes, so 16. The last row is new: sizeof &score is the size of the address, not of score, the same 8 the measured run printed.
Zara trusts nothing she has not predicted. Before looking at any address, she works out where each box must start, from sizeof alone.
#include <stdio.h>
#define MARKS 4
#define PRICES 3
int main(void)
{
int marks[MARKS] = {70, 80, 90, 60};
double prices[PRICES] = {1.5, 2.5, 3.5};
for (int i = 0; i < MARKS; i++) {
printf("marks[%d] starts %2zu bytes after marks[0]\n", i, i * sizeof marks[0]);
}
for (int i = 0; i < PRICES; i++) {
printf("prices[%d] starts %2zu bytes after prices[0]\n", i, i * sizeof prices[0]);
}
return 0;
}
marks[0] starts 0 bytes after marks[0]
marks[1] starts 4 bytes after marks[0]
marks[2] starts 8 bytes after marks[0]
marks[3] starts 12 bytes after marks[0]
prices[0] starts 0 bytes after prices[0]
prices[1] starts 8 bytes after prices[0]
prices[2] starts 16 bytes after prices[0]
The product is a size_t, so it prints with %zu. Now check her against run 1: the boxes of marks ended in bd0, bd4, bd8 and bdc. That is 0, 4, 8 and 12 bytes along. A prediction that holds on every run beats any single address.
Amara reads a student's name, a count, and that many marks. It is the program a beginner writes, with three kinds of scanf target.
#include <stdio.h>
#define MAX_N 50
int main(void)
{
char name[20] = {0};
int n = 0;
int marks[MAX_N] = {0};
int total = 0;
scanf("%19s", name);
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &marks[i]);
total += marks[i];
}
printf("%s: %d marks, total %d\n", name, n, total);
printf("first %d, last %d\n", marks[0], marks[n - 1]);
return 0;
}
| Read | What scanf receives | Why |
|---|---|---|
name | the address of name[0] | a bare array name already is an address |
&n | the address of n | one variable needs its ampersand |
&marks[i] | the address of box i, 4 x i bytes after box 0 | one box of an int array is one int |
Amara: 5 marks, total 356
first 70, last 48
That output is for the input Amara, 5 and 70 85 62 91 48. A variable takes an ampersand, one box takes one, and a bare array name never does.
Where this is used
- Debuggers. In GDB, the GNU debugger,
print &scoreshows a variable's address, andx/4dw &marksshows the fourints stored there. - Memory maps. On Linux,
/proc/self/mapslists every area of a running program with its address range. The stack's line is labelled[stack]. - Security. Linux, Windows and macOS all use ASLR, so an attack cannot aim at an address from an earlier run. On Linux,
/proc/sys/kernel/randomize_va_spaceholds 2 when the stack, libraries and heap all move. - Crash reports. When a program touches memory it may not, Linux stops it. The kernel log,
dmesg, recordssegfault atwith the address touched, the first clue to the bug.
Common mistakes
1. Printing an address with %d.
int score = 87;
printf("%d\n", &score);
Silent on the Playground. A local gcc -Wall on GCC 12 says warning: format '%d' expects argument of type 'int', but argument 2 has type 'int *' [-Wformat=]. An address is 8 bytes, and %d reads 4. One run on Compiler Explorer printed a negative ten-digit number: the address's last 4 bytes. Write printf("%p\n", (void *)&score);. You will reach for %d because every number so far was an int.
2. %p without the cast.
printf("%p\n", &score);
No message at any command line. One run on Compiler Explorer printed the same number with and without the cast. C17 still asks for a void * (section 7.21.6.1). Only -Wpedantic, a stricter switch, says warning: format '%p' expects argument of type 'void *', but argument 2 has type 'int *' [-Wformat=]. Every x86-64 address has one form, so it works here; the cast keeps it right everywhere. You will drop it because nothing complains.
3. An ampersand on a number or a sum.
printf("%p\n", (void *)&60);
printf("%p\n", (void *)&(score + 1));
An error on every command line, the Playground included, once per line: error: lvalue required as unary '&' operand. An lvalue is an expression that names a box, one that could stand left of =. 60 and score + 1 live in no box, so they have no address. Store the value in a variable, then take that variable's address. You will try this when you want the address of a result.
4. An ampersand on an array name.
char name[20] = {0};
scanf("%19s", &name);
Silent on the Playground. A local gcc -Wall on GCC 12 says warning: format '%s' expects argument of type 'char *', but argument 2 has type 'char (*)[20]' [-Wformat=]. It still reads the name: on Compiler Explorer, the input Maria printed hello, Maria. &name is the same number with another type, char (*)[20], the address of the whole array. Write scanf("%19s", name);. You will add the ampersand out of habit; lesson 03 shows why an array's name is different.
Kenji says variables always sit in the order they are declared. Check it on your own program: three variables and an array of five boxes.
Input. None.
Output. Eight lines like &seats = address: seats, price, row, then each box of stock. Then one box: N bytes, from sizeof stock[0].
Constraints. Print every address with %p and a (void *) cast. Read the step between boxes by eye, from the last digits.
Sample. No two runs match. The lesson's first run shows the shape of a line: &marks[0] = 0x7ffe8f424bd0. Whatever your numbers, the step between boxes must equal your last line.
#include <stdio.h>
#define BOXES 5
int main(void)
{
int seats = 40;
double price = 7.5;
char row = 'C';
int stock[BOXES] = {12, 0, 7, 3, 25};
/* Print the address of seats, price and row, one per line.
Then print the address of every box of stock, and
sizeof stock[0]. Which step do you read between the boxes? */
return 0;
}
Not graded on its own. The addresses change every run, and a judge compares text.
Run in CompilerMaria wants the rule, not one example: is the step between two boxes always the size of one box? Test five types, one of them new to this lesson.
Input. None.
Output. Five lines, one per array: the type, sizeof one box, and the addresses of box 0 and box 1.
Constraints. Print sizes with %zu and addresses with %p and (void *). Write your expected steps in a comment before you run it.
Sample. The addresses differ every run. For int, the measured run had box 0 at 0x7ffd8d6d3850 and box 1 at 0x7ffd8d6d3854, a step of 4. Your short line is the one no table here shows.
#include <stdio.h>
int main(void)
{
char letters[2] = {'a', 'b'};
short years[2] = {2025, 2026};
int seats[2] = {40, 42};
long long views[2] = {1000000000000, 2000000000000};
double prices[2] = {7.5, 9.25};
/* For each array, print on one line: the type, sizeof one box,
and the addresses of box 0 and box 1. Before you run it,
write the step you expect for each type in a comment. */
return 0;
}
Not graded on its own, for the same reason: the addresses change every run, and a judge compares text.
Run in CompilerCommon doubts
Why does every address start with 0x7ff, and why only 12 digits?
An address is 8 bytes, room for 16 hexadecimal digits. But Linux on x86-64 gives a program only the numbers up to
0x7fffffffffff, twelve digits. The higher ones belong to the operating system. The stack sits just below that ceiling, so its addresses start with 0x7ff.Does a variable's address change while the program runs?
No. A variable keeps one address for its whole life. A global or a
staticlives all run, so its address is fixed for that run. A local lives for one call, and the next call may place it elsewhere.Can I pick an address myself, such as 1000, and write there?
Not for a variable: the compiler and the system place those. Writing through an address nobody gave you is a wild pointer, lesson 07's topic. On the Playground, Bob's measured attempt ended in Runtime error.
Kenji's laptop prints addresses differently. Is it broken?
No, it works on his laptop too. C17 leaves the look of
%pto each system (section 7.21.6.1). The Playground's GNU C Library writes0xand small letters. Microsoft's C library on Windows writes 16 capital digits and no0x.
Key takeaways
- Memory is a row of numbered bytes. A variable's address is the number of its first byte, and
&xgives it. - Print an address with
%pand a(void *)cast:%pexpectsvoid *, the general address type. - Addresses change every run because of ASLR, but distances inside one run do not. Compare addresses within one run.
- An array's boxes sit
sizeofone box apart: 4 forint, 1 forchar, 8 fordouble. An address takes 8 bytes. - Locals live in their call's frame on the stack, globals and
statics in the static area. The heap is Module 14. scanfwrites into your variable, so it needs its address; an array's name already is one. A variable holding an address is a pointer.
Next, Bob writes int *p; and then *p = 5;, and asks where his 5 went. Lesson 02 declares a pointer properly and follows it to its box.
End of lesson 1
Mark it done, and your progress moves with you.
Next: Pointers: a Variable That Holds an Address