๐ซ [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 ๐