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 bits โ†’ 0000 to 1111 โ†’ 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:

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 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 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 p โ†’ 50
&pAddress of pointer p
ppHolds address of p โ†’ pp = &p
*ppDereference pp: get the value stored in p โ†’ this is &x
**ppDereference twice: get value of x โ†’ 50
&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 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 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.