Lesson 4 ยท ~15 minutes ยท ties to mission: read and write C confidently

Unpacking *p++ = *q++

You saw this compressed idiom in lesson 2 and were told to keep writing the three-line version for now. Now it's time to own it โ€” by seeing exactly what the compiler does.

The three-line version you already know

*p = *q;     // copy: read q's target, write to p's target
p++;         // advance the write head
q++;         // advance the read head

This is the two-pointer lockstep from lesson 2 โ€” clear, explicit, nothing hidden. Every grown-up C programmer compresses it to one line. Let's see why that works.

The one operator you need: postfix ++

Postfix ++ does two things, and the order is the whole lesson:

StepWhat happens
1. YieldThe expression produces the current value (before incrementing).
2. Side effectThe variable is incremented after the value is yielded.

So p++ means: "give me p's current value, then add 1 to p." It's a post-increment โ€” use, then change.

Contrast: prefix ++p Prefix ++p increments first, then yields the new value. Change, then use. We won't need it in this lesson, but knowing it exists is important โ€” it's the other side of the coin.

Now layer in the * (dereference) operator

You know *p means "the thing p points at." The question is: in *p++, which binds tighter โ€” * or ++?

ExpressionOperator precedenceWhat it means
*p++++ (postfix) binds tighter than **(p++) โ€” dereference the old value of p, then increment p
(*p)++Explicit groupingDereference p, then increment the value at that address
*++p++ (prefix) binds tighter than *Increment p first, then dereference the new address
The key insight *p++ reads from p's current target, then moves p forward. The data you get is from the old position. The pointer has already moved on by the time the expression is done. That's exactly the lockstep move โ€” in a single expression.

Unroll the full line: *p++ = *q++

The assignment = evaluates right-to-left. The compiler sees:

*p++ = *q++;
โ–ผ step-by-step
// Right side: *q++
1. Yield current value of q  โ†’  say, address 0x1000
2. Dereference that address  โ†’  say, character 'W'
3. Side effect: q becomes 0x1001

// Left side: *p++
4. Yield current value of p  โ†’  say, address 0x2005
5. Dereference that address  โ†’  slot at 0x2005
6. Side effect: p becomes 0x2006

// Assignment
7. Write the right-side result ('W') into the left-side slot (0x2005)

Seven steps, one line. And it produces exactly the same result as the three-line version:

*p = *q;     // copy 'W' from 0x1000 to 0x2005
p++;         // p โ†’ 0x2006
q++;         // q โ†’ 0x1001

The memory diagram โ€” same as lesson 2

Nothing has changed about what happens in memory. *p++ = *q++ is just a shorthand for the lockstep copy you already understand. Here's one iteration:

dest:   H  e  l  l  o '\0'  ?   ?   ?  ...
                     โ†‘
                     p

str2:   W  o  r  l  d  '\0'
        โ†‘
        q
After one iteration of *p++ = *q++:

dest:   H  e  l  l  o  W   ?   ?  ...
                          โ†‘
                          p  (advanced)

str2:   W  o  r  l  d  '\0'
            โ†‘
            q  (advanced)

Same bytes moved, same pointers advanced. Just fewer keystrokes.

The six forms โ€” at a glance

Because combining * and ++ creates confusion, here's the complete set. Don't memorize this โ€” derive each one from the rule "postfix ++ binds tighter than *." The table is for later reference.

ExpressionExpandedEffect
*p++*(p++)Read value at p, then advance p. โ† this is the one you use
*++p*(++p)Advance p first, then read new value.
(*p)++โ€”Read value at p, increment the value (not the pointer).
*--p*(--p)Step p back, then read new value. โ† reverse copy
*p--*(p--)Read value at p, then step p back.
++*p++(*p)Increment the value at p, yield new value.

When to use the compact form

Your progression Phase 1 (where you were): write *p = *q; p++; q++; โ€” three lines, explicit, no ambiguity.
Phase 2 (where you are now): understand *p++ = *q++ because you've seen it unrolled. Use it when reading other people's code.
Phase 3 (where you're heading): start using *p++ = *q++ in your own code, when the three-line version feels tedious. You'll know you're ready when you can read *p++ = *q++ and instantly see "copy, advance both" without mentally unrolling it.

Check yourself

โ‘  What does *p++ evaluate to?

Show answer The value at p's current address (before the increment). It dereferences the old value, then moves p forward as a side effect. So *p++ yields a char (or whatever p points at), not a pointer.

โ‘ก What's the difference between *p++ and (*p)++?

Show answer *p++ (i.e. *(p++)) advances the pointer. (*p)++ advances the value the pointer targets.

Example: if p points at an int holding 42: *p++ yields 42 and moves p to the next int. (*p)++ yields 42 and changes the value at the current address to 43, without moving p.

โ‘ข Rewrite *p++ = *q++ using prefix increment instead.

Show answer You'd need: *p = *q; ++p; ++q; โ€” you can't directly replace *p++ with *++p because the semantics would change (prefix increments first, so you'd skip the first element). The compact *p++ = *q++ works precisely because both are postfix โ€” they yield the old position, then advance.

โ‘ฃ In while (*p++ = *q++); โ€” what's the loop condition actually testing?

Show answer The value assigned โ€” the character copied from *q. The loop continues while that character is non-zero (i.e. not '\0'). It stops when *q yields '\0' (which is 0), which also copies that '\0' into *p before terminating. This is actually the classic one-liner strcpy implementation โ€” it copies the terminator and stops, all in the condition. Elegant, but don't write this in production โ€” it's a readability trap.

๐Ÿ› ๏ธ Hands-on โ€” rewrite your mystrcat using the compact form

Take your working mystrcat function from the lesson 2 hands-on and create a second version:

  1. Copy the function. Name it mystrcat_compact.
  2. Replace the three-line lockstep in Act 2 with *p++ = *q++;
  3. Keep Acts 1 and 3 in expanded form (don't compress those yet โ€” one thing at a time).
  4. Compile, run, and verify it produces the same output.
  5. Once it works: try compressing Act 1 into while (*p) p++;. Does it still work? (Yes โ€” *p is non-zero until '\0', so this skips the explicit != '\0' test.)
  6. Compare: read both versions side by side. Which is easier to understand at a glance? Both are correct โ€” the question is what your eyes can parse without effort.
What you can now do Read *p++ = *q++ and instantly know it means "copy, advance both." You can mentally unroll it to the three-line version. You know postfix ++ binds tighter than *, and you know the difference between *p++ and (*p)++. You've also seen the classic one-liner strcpy trick โ€” even if you shouldn't write it yourself yet.
Questions? Ask your agent. Anything murky โ€” "what about *p++ + *q++?", "is there a performance difference?", "why does the one-liner strcpy work?" โ€” type it and we'll dig in.
Sources & further reading
ยท cppreference: C operator precedence โ€” the full table. Postfix ++ is precedence 2 (second-highest); dereference * is precedence 3.
ยท comp.lang.c FAQ โ€” Pre vs Post increment
ยท Beej's Guide to C Programming โ€” pointers chapter, covers the *p++ idiom.
ยท Companion cheat sheet ยท Lesson 2: strcat unrolled ยท Lesson 3: the const matrix