Addresses and the Stack
Every byte of memory has an address: reveal where your variables live with the & operator and %p, and build the picture of the stack the rest of the course sits on.
Every Byte Has a Number
Lesson 2 left a variable described as a named, typed, measurable patch of memory, and one question open: where exactly is that patch?
Memory is one enormous row of bytes, and the hardware needs a way to say which byte it means. So the bytes are numbered, from zero upward, one number per byte. That number is a byte's address, and it is the only name the hardware has for a location. Every read and every write the CPU performs is aimed at an address.
Your variables are in there somewhere. int age = 25; reserved four bytes, and those four bytes are numbered like all the rest. C will tell you the first of those numbers if you ask.
The & Operator
& written in front of a variable means "the address of". &age is not the value stored in age, it is the number of the byte where age begins. To print an address you need a conversion specifier, and for addresses that is %p:
#include <stdio.h>
int main(void)
{
int age = 25;
printf("age holds %d\n", age);
printf("age lives at %p\n", (void *)&age);
return 0;
}
age holds 25
age lives at 0xfbffaa0f0020
That address came out of one particular run. Run the same program again and the number changes, and on a different machine it will not even be the same length. Operating systems deliberately move a program's memory somewhere new each time it starts, a defence called address space layout randomization, because fixed addresses are exactly what an attacker would like to count on. So read the number as the answer to "where was age that time", never as a property of the variable.
The 0x prefix marks the digits as hexadecimal, base 16, whose digits run 0 to 9 and then a to f. Addresses are conventionally shown in hex because it is compact and because memory boundaries fall on round numbers in that base. The precise spelling is left to the implementation; this platform prints 0x followed by lowercase hex.
Now the cast, which is not decoration. %p expects a void *, an address with no type attached to it. &age is not that: it is the address of an int. Arguments after the format string reach printf through a mechanism that converts nothing for you, so the type has to be right before it leaves your hand, and (void *) is how you strip the int off the address. Two reasons to build this in now. It is a requirement of the language rather than a preference. And unlike the mismatches in lessons 1 and 2, -Wall -Wextra does not report this one, so nothing will nudge you. Every %p gets a (void *) cast.
Where It Starts, How Far It Goes, How to Read It
An address by itself tells you where a value begins and nothing else. Bytes carry no labels, so the byte at a given address could be part of a whole number, part of a price, or part of a machine instruction. The type is what settles it, and lesson 2 already gave you the tool for measuring the span. Put & and sizeof together and a variable is completely described:
#include <stdio.h>
int main(void)
{
double price = 19.99;
printf("value: %.2f\n", price);
printf("address: %p\n", (void *)&price);
printf("size: %zu bytes\n", sizeof price);
return 0;
}
value: 19.99
address: 0xfbffb38f0020
size: 8 bytes
Where it starts, how far it extends, and how to interpret what is in there. Those three facts are all a variable is, and that shape is behind nearly everything the rest of this course does with memory.
Locals Live on the Stack
Ask for several addresses at once and a pattern appears:
#include <stdio.h>
int main(void)
{
int first = 1;
int second = 2;
double third = 3.0;
printf("first is at %p\n", (void *)&first);
printf("second is at %p\n", (void *)&second);
printf("third is at %p\n", (void *)&third);
return 0;
}
One run printed this:
first is at 0xfbff823f0020
second is at 0xfbff823f0030
third is at 0xfbff823f0040
and the very next run of the same executable, not a character changed, printed this:
first is at 0xfbff9dbf0020
second is at 0xfbff9dbf0030
third is at 0xfbff9dbf0040
Every address moved. What did not move is the relationship between them: inside a single run the three agree on all but their last digits, which is to say the three variables sit close together in one region of memory.
That region is the stack, and it is where variables declared inside a function live. Be careful about what is being claimed here. The claim is that these variables share a region. It is not that second comes after first, nor that the spacing is what you see above, nor that addresses climb in declaration order. The arrangement inside the region is the compiler's business: it reorders and pads for alignment and speed, and this platform's AddressSanitizer adds padding of its own. Anything you write that depends on the layout is something that breaks when a flag changes.
The stack is organized into frames, one for each function call. Every variable above lives in main's frame, and when main finishes, its frame is gone and so is everything in it. That is the whole lifetime of a local variable: from its declaration until the function it was declared in returns.
Step back and a running program's memory looks roughly like this on a typical system:
high addresses
+----------------------------+
| stack |
| main's frame: |
| first, second, third |
+----------------------------+
| |
| unused space |
| |
+----------------------------+
| heap | memory you ask for by hand (chapter 4)
+----------------------------+
| globals and constants |
+----------------------------+
| code | the instructions the CPU is running
+----------------------------+
low addresses
Every box in that picture is just bytes with numbers on them. The regions differ in who decides when the memory is handed out and taken back, and the stack is the region where that decision is made for you, automatically, by entering and leaving a function.
Where This Is Going
Two payoffs are already in view.
The next lesson introduces scanf, which reads a value typed by the user and has to put it somewhere. Handing a function the value of a variable is no use when the function's job is to change that variable, so you will pass & and give it the address instead. That is where addresses stop being a curiosity and start being how work gets done.
Chapter 3 then gives every function call a frame of its own, stacked on the caller's, and introduces variables whose contents are addresses rather than values. Both are this lesson extended: memory is numbered, and once you can name a location you can pass it around.
Key Takeaways
- Every byte of memory has a number called its address, and addresses are how the hardware refers to locations.
&xis "the address ofx", the number of the first byte the variable occupies.- Print an address with
%p, and always cast the argument:printf("%p\n", (void *)&age);.%pexpects avoid *, and-Wall -Wextrawill not warn you when it does not get one. - Printed addresses differ from run to run and machine to machine, because the operating system randomizes where a program's memory goes. Never build anything on a specific value.
- An address says where a variable starts,
sizeofsays how many bytes it spans, and the type says how to read them. Together they describe a variable completely. - Variables declared inside a function live in a region called the stack, grouped into a frame per call, and
main's locals are inmain's frame. They exist until that function returns. - Locals cluster in one region, but their order and spacing inside it belong to the compiler. Never write code that assumes a layout.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Addresses and the Stack - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!