🏫 [CS50x 2025] 4-1 Pointer
🧮 Q: How many bits to count to 15?
Let’s rephrase it:
What’s the smallest number of bits (binary digits) you need to represent the decimal number
15
?
🔥 Here’s the magic:
In binary:
- 1 bit → you can count up to
1
(0
,1
→ 2 numbers = 2¹ ) - 2 bits →
00
,01
,10
,11
→ up to3
(2² = 4 numbers) - 3 bits →
000
to111
→ up to7
(2³ = 8 numbers) - 4 bits →
0000
to1111
→ 0 to 15 ✅ (2⁴ = 16 numbers)
So to count from 0
to 15
, you need 4 bits, because 2⁴ = 16
possible values. That covers 0~15 perfectly.
🧡 And this ties into hex:
Each hex digit (0
to F
) maps perfectly to 4 bits:
1
F = 1111 (binary) = 15 (decimal)
So whenever you see 1 hex digit, your can immediately say:
“Ahh! That’s 4 bits in disguise 😎.”
💬 Q: How many bytes is an integer today?
On most modern systems (especially 64-bit ones), an int
(in C or C-like languages) is usually:
4 bytes = 32 bits
🧠 But why 4 bytes?
✦ 1. Performance & Compatibility
- Most CPUs are 64-bit, but they still handle 32-bit data really efficiently.
- 32-bit ints are a sweet spot: enough range for most things, faster to move around in memory than 64-bit.
✦ 2. Standardization (and legacy reasons!)
- C and C++ don’t guarantee exact sizes, just minimum ranges.
But compilers on modern systems choose 4 bytes for
int
because it’s:- Compatible with older systems
- Balanced in memory usage
✦ 3. Memory Efficiency
- Using 64-bit ints everywhere would double memory use without needing the huge range.
int
(4 bytes) can go from:- −2,147,483,648 to +2,147,483,647
- That’s enough for most counters, loops, and general variables!
👀 TL;DR - On 64-bit machines:
Type | Typical Size | Notes |
---|---|---|
char | 1 byte (8 bits) | For letters/bytes |
int | 4 bytes (32 bits) | Most common! |
long | 8 bytes (64 bits) | If you need bigger numbers |
💡 Q: So, how do we know an int
is 4 bytes = 32 bits?
Let’s go step by step:
🧮 Step 1: Understand what a byte is
- 1 byte = 8 bits (this is universal across all modern systems).
So:
1 2 3
1 byte = 8 bits 2 bytes = 16 bits 4 bytes = 32 bits
🧪 Step 2: In C (or C++), test it!
Here’s a little program you could whisper to a compiler:
1
2
3
4
5
6
#include <stdio.h>
int main() {
printf("Size of int: %lu bytes\n", sizeof(int));
return 0;
}
If you run this on most 64-bit systems, you’ll see:
1
Size of int: 4 bytes
Boom. That’s 4 bytes, confirmed with your own fingers 👩💻
🧠 Why 4 bytes exactly?
Because it lets us represent this many values:
2³² = 4,294,967,296 possible values (when unsigned, it’s from 0 to 4 billion+)
Or:
int
(signed) → −2,147,483,648 to +2,147,483,647
That’s enough for 99.9% of normal tasks, and it doesn’t take up as much memory as a long
(8 bytes).
💘 TL;DR - How to calculate?
1
2
3
4 bytes × 8 bits = 32 bits
→ max value for unsigned: 2^32 - 1
→ max value for signed: ±2^31 - 1
💡 How to Calculate How Many Bytes a Number Takes
🌟 TL;DR: To calculate how many bytes a number needs:
Find the minimum number of bits required to represent the number in binary, then divide by 8 (and round up) to get the number of bytes.
🧮 Step-by-step - Let’s break it down:
✦ Step 1: Convert the number to binary
You can use bin()
in Python or just count powers of 2 in your head.
Examples:
Decimal | Binary | Bits |
---|---|---|
5 | 101 | 3 bits |
255 | 11111111 | 8 bits |
256 | 100000000 | 9 bits |
1,000 | 1111101000 | 10 bits |
16,777,215 | 11111111 11111111 11111111 | 24 bits |
✦ Step 2: Divide by 8 (since 1 byte = 8 bits)
But you gotta round up - even if it only takes 9 bits, it still uses 2 bytes in memory.
Bits | Bytes (rounded up) |
---|---|
3 | 1 byte |
8 | 1 byte |
9 | 2 bytes |
16 | 2 bytes |
32 | 4 bytes |
64 | 8 bytes |
🧪 Example Time!
Let’s say we want to store the number 300
:
- Binary:
100101100
→ 9 bits - Divide by 8:
9 / 8 = 1.125
- Rounded up →
2 bytes
So storing
300
needs 2 bytes.
💻 In Python:
Wanna cheat a little? You can do this:
1
2
3
n = 300
bytes_needed = (n.bit_length() + 7) // 8
print(bytes_needed)
Output: 2
🧾 Understand “The value at memory address 0x123
is 50
”
This is a classic low-level memory concept - and it’s hot stuff if you’re playing with C, assembly, or writing exploits 😏
💡 What it actually means:
Think of memory as a huge hotel 🏨 full of numbered rooms. Each room is a memory address like 0x000
, 0x001
, 0x002
, …
And in each room, you can store one value - like a number, a character, or part of a variable.
So when we say:
"The value at address 0x123 is 50"
It means: Go to memory room number0x123
(which is 291 in decimal) Inside, there’s the number50
stored.
🔧 In code:
In C, you could write:
1
2
int *ptr = (int *)0x123; // create a pointer to memory address 0x123
printf("%d\n", *ptr); // print whatever value is stored there
🧠 *ptr
is saying: “Give me the value at the memory location you’re pointing to.”
So:
1
*ptr == 50
means the data stored at 0x123
is the number 50
.
👁🗨 Real-world visualization:
Let’s say memory looks like this:
Address | Value |
---|---|
0x120 | 88 |
0x121 | 22 |
0x122 | 99 |
0x123 | 50 |
0x124 | 19 |
💬 So if your code accesses *(int*)0x123
, boom, it gets 50
.
💡 Understand *
- Dereference Operator?
💘 Imagine a Pointer:
1
2
int x = 50;
int *ptr = &x;
Let’s unpack this:
x
is a variable with value50
&x
means: “the address ofx
“ptr
is a pointer that stores the address ofx
So ptr
might hold something like:
1
0x123
Now comes the magic:
1
*ptr
💥 This is the dereference operator.
It means: “Go to the address stored in
ptr
, and fetch the value there.”
So if ptr = &x
, then *ptr
gives you the value of x, which is 50
.
🧠 Why is it called “dereference”?
Because:
ptr
is a reference to memory.*ptr
says: “Don’t give me the reference. Give me what’s at the reference.”
It’s like:
📍
ptr
= map location 🌟*ptr
= the treasure at that location
🧠 Summary:
Expression | Meaning |
---|---|
int *p; | Declare p as a pointer to an int |
p = &x; | Store the address of x into p |
*p | Dereference: get the value at the address in p |
💡 Q: The Address and The Value are Located at Different Places in Memory?
Yes - and that’s the whole beautiful, wild, chaotic point of pointers. 💻
A pointer lives in one memory location, but the value it holds is another memory location - and that second place is where the real value (like50
) lives.
😼 Let’s break it down visually and sensually
Look at this image:
1
2
3
p ➜ holds 0x123
🧠 ↓
memory[0x123] ➜ 50
Let me make it even more real with metaphor:
👑 Imagine this:
You are p
, and you’re holding a little sticky note that says:
🗒️ “Go to locker 0x123”
You’re not carrying the treasure (the value 50
) - you’re carrying the location of the treasure.
When you do *p
, you tell the CPU:
“Hey! Go to the place that my sticky note points to - and give me what’s inside that locker.”
And that is how:
p
= pointer (address)*p
= dereferenced value (what’s at that address)
🧠 What’s happening in memory?
Address | Value Stored | What it means |
---|---|---|
0x456 | 0x123 | Where pointer p lives. It stores address 0x123 |
0x123 | 50 | This is where x lives - the actual number 50 |
So:
p
is at 0x456*p
goes to 0x123, and gets the value50
- The pointer has its own address (because it’s a variable too!)
- But what it points to is someone else’s address
✅ This is why we can:
- Pass pointers to functions - because they know where to look
- Chain pointers (pointers to pointers)
- Access data without copying big chunks of memory
- Read or modify values at any location - just by knowing the address 🔥
Pointer Recursion
So now you may ask:
“Who’s pointing at the pointer?”
“And what’spp
,&pp
,*pp
,**pp
…!?”
It’s about to get nerdy, flirty, and pointer-dirty 👾
💡 Definitions Time!
Let’s declare:
1
2
3
int x = 50; // normal int
int *p = &x; // p points to x
int **pp = &p; // pp points to p
Now this beautiful pointer triangle happens:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌────────────┐
│ x = 50 │
└────┬───────┘
│
&x │
▼
┌────────────┐
│ p = &x │ ←───┐
└────┬───────┘ │
&p │ │
▼ │
┌────────────┐ │
│ pp = &p │◀────┘
└────────────┘
📍 Let’s map the pointer chain:
Expression | Meaning |
---|---|
x | Value: 50 |
&x | Address of x |
p | Holds address of x → p = &x |
*p | Dereference: get value at address in p → 50 |
&p | Address of pointer p |
pp | Holds address of p → pp = &p |
*pp | Dereference pp : get the value stored in p → this is &x |
**pp | Dereference twice: get value of x → 50 |
&pp | The address where the pointer-to-pointer is stored |
😵💫 So YES:
❓ Does a pointer have its own value? ✔️ Yes! Its value is an address - the memory location it points to.
❓ Can I do
*pp
? ✔️ YES -*pp
gives you the value insidepp
, which is a pointer (p
) →*pp == p
→**pp == x
❓ What’s
&pp
? ✔️ The address of the pointer-to-pointer. A full-on memory nesting doll 🪆🖤
🔥 Real Output Demo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main() {
int x = 50;
int *p = &x;
int **pp = &p;
printf("x = %d\n", x); // 50
printf("*p = %d\n", *p); // 50
printf("**pp = %d\n", **pp); // 50
printf("p = %p\n", (void *)p); // address of x
printf("*pp = %p\n", (void *)*pp); // also address of x
printf("pp = %p\n", (void *)pp); // address of p
}
💡 What does (void *)
mean?
It’s called a type cast.
In this case:
1
printf("%p\n", (void *)p);
You’re telling printf()
:
“Yo,
p
is a pointer, and I want you to treat it as a generic memory address (avoid *
) and print it properly.”
🧠 But wait - why do we need to cast?
Because in C:
%p
is for printing a pointer (address)- But
printf()
expects the argument to be of typevoid *
for%p
So if you do:
1
2
int *p = &x;
printf("%p\n", p); // ⚠️ Maybe warning
You might get:
1
warning: format '%p' expects argument of type 'void *'
So we make the compiler shut up (and behave 😏) with:
1
printf("%p\n", (void *)p); // ✅ Proper way
🛑 WITHOUT (void *)
, what happens?
You might write:
1
printf("%p\n", pp); // compiler warns ⚠️
Because %p
expects a void *
, not an int **
.
It might still run, but the output could be:
- Misaligned
- Misformatted
- Confusing AF
✅ TL;DR - Why (void *)
?
You wanna… | Then write… |
---|---|
Print the address a pointer points to | printf("%p", (void *)ptr); |
Print the value at that address | printf("%d", *ptr); |
Print the address where the pointer lives | printf("%p", (void *)&ptr); |
🧠 Q: The difference between int x;
and int *x;
?
int x; | declares a normal int variable - just stores a number like 42 directly. |
---|---|
int *x; | declares a pointer to an int - it stores an address, not the value itself. |
So…
1
2
3
4
int x = 42; // "x" literally holds the number 42
int *p; // "p" will hold the address of an int
p = &x; // now "p" points to "x"
💡 Why is the second code valid?
1
2
3
4
5
x = malloc(sizeof(int));
*x = 42;
y = x;
*y = 13; // ✅ VALID!
“Does
y = x
meany = malloc(sizeof(int))
?”
💥 Technically… yes in effect, but let me explain the detail.
🧠 Step-by-step: What’s happening?
1. x = malloc(sizeof(int));
malloc
gives you a memory address (on the heap).x
now points to a chunk of memory big enough to store anint
.
2. *x = 42;
- You write
42
into that newly allocated memory. - The value
42
lives inside the memory block pointed to byx
.
3. y = x;
- You’re not allocating again - you’re saying:
“Make
y
point to the same memory thatx
is pointing to.”
Now both x
and y
point to the same chunk of memory.
✅ So yes! After y = x;
:
1
*y = 13;
- Is the same as:
1
*x = 13;
Because both x
and y
point to the same address in memory. 💌
🔬 Memory Diagram
1
2
3
4
5
6
7
x ---------┐
▼
+-----------+
| 42 | ← malloc'd memory
+-----------+
▲
y ---------┘
After *y = 13;
:
1
2
3
+-----------+
| 13 |
+-----------+
💌 TL;DR
Line of code | What it does |
---|---|
x = malloc(...) | allocates memory and stores address in x |
*x = 42; | writes 42 to that memory |
y = x; | makes y point to the same memory |
*y = 13; | updates the memory (now *x == 13 too) |
💡 Why not swap(*x, *y)
?
Let’s take a closer look at line 11:
1
swap(&x, &y); // ✅ correct!
But:
Why not
swap(*x, *y)
?
👀 Let’s compare
❌ If you did this:
1
swap(*x, *y);
You’re saying:
“Deref x and y - get the values, then pass those into swap.”
So:
*x = 1
and*y = 2
- You’d be calling
swap(1, 2);
BUTTTT - your swap function is:
1
void swap(int *a, int *b)
So it expects addresses (int *
), not raw values (int
). Passing 1
and 2
as pointers is like saying:
“Yo, go look in memory address
0x1
and0x2
and swap that data.”
💥 CRASH CITY. You’re dereferencing garbage. 🚫
✅ Why this works:
1
swap(&x, &y);
Because:
&x
means “the address of x”&y
means “the address of y”- So now
swap()
receives real pointers to x and y - It can dereference them safely with
*a
and*b
, and modify them directly
💘 TL;DR
You write | What it means | Why it’s 🔥 or 💀 |
---|---|---|
swap(*x, *y) | Dereference and pass values → swap(1, 2) | ❌ wrong type - passes ints, not pointers |
swap(&x, &y) | Pass addresses → int *a = &x; | ✅ correct - lets you swap original variables |
💡 Understand scanf("%i", &n);
“Wait… I declared
int n;
, but then I never assigned ton
- so how did it get the value50
?”
The answer is:
✨ It did assign the value - just indirectly, using a pointer.
🧪 Let’s look again:
1
2
int n;
scanf("%i", &n);
Here’s what’s happening:
Code | Meaning |
---|---|
int n; | Creates space for an integer - but no value is assigned yet (⚠️ garbage at first) |
&n | “The address of n ” |
scanf("%i", &n); | Reads input and writes it into the address of n |
So scanf()
literally:
- Takes your input (
50
) - Finds out where
n
lives (via&n
) - And shoves that
50
directly inton
’s memory slot 💥
💬 In hacker-speak:
You’re not assigning n
the value like this:
1
n = 50; // ❌ this is direct
You’re doing this instead:
1
scanf("%i", &n); // ✅ "Hey scanf, go write the input to this address"
It’s like saying:
“Here’s the location of my int - go fill it up.”
scanf()
= a function that uses pointer dereferencing under the hood!
So n
is totally assigned, just not by direct assignment - it’s done through memory, by giving scanf()
the address 💌