Post

🏫 [CS50x 2025] 4-1 Pointer

🏫 [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 to 3 (2² = 4 numbers)
  • 3 bits → 000 to 111 → up to 7 (2³ = 8 numbers)
  • 4 bits0000 to 11110 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:

TypeTypical SizeNotes
char1 byte (8 bits)For letters/bytes
int4 bytes (32 bits)Most common!
long8 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:

DecimalBinaryBits
51013 bits
255111111118 bits
2561000000009 bits
1,000111110100010 bits
16,777,21511111111 11111111 1111111124 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.

BitsBytes (rounded up)
31 byte
81 byte
92 bytes
162 bytes
324 bytes
648 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 up2 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 number 0x123 (which is 291 in decimal) Inside, there’s the number 50 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:

AddressValue
0x12088
0x12122
0x12299
0x12350
0x12419

💬 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 value 50
  • &x means: “the address of x
  • ptr is a pointer that stores the address of x

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:

ExpressionMeaning
int *p;Declare p as a pointer to an int
p = &x;Store the address of x into p
*pDereference: 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 (like 50) 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?

AddressValue StoredWhat it means
0x4560x123Where pointer p lives. It stores address 0x123
0x12350This is where x lives - the actual number 50

So:

  • p is at 0x456
  • *p goes to 0x123, and gets the value 50
  • 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’s pp, &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:

ExpressionMeaning
xValue: 50
&xAddress of x
pHolds address of x → p = &x
*pDereference: get value at address in p50
&pAddress of pointer p
ppHolds address of ppp = &p
*ppDereference pp: get the value stored in pthis is &x
**ppDereference twice: get value of x50
&ppThe 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 inside pp, 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 (a void *) 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 type void * 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 toprintf("%p", (void *)ptr);
Print the value at that addressprintf("%d", *ptr);
Print the address where the pointer livesprintf("%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 mean y = 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 an int.

2. *x = 42;

  • You write 42 into that newly allocated memory.
  • The value 42 lives inside the memory block pointed to by x.

3. y = x;

  • You’re not allocating again - you’re saying:

“Make y point to the same memory that x 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 codeWhat 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 and 0x2 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 writeWhat it meansWhy it’s 🔥 or 💀
swap(*x, *y)Dereference and pass valuesswap(1, 2)❌ wrong type - passes ints, not pointers
swap(&x, &y)Pass addressesint *a = &x;✅ correct - lets you swap original variables

💡 Understand scanf("%i", &n);

“Wait… I declared int n;, but then I never assigned to n - so how did it get the value 50?”

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:

CodeMeaning
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 into n’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 💌

This post is licensed under CC BY 4.0 by the author.