Lesson 3 Β· ~15 minutes Β· ties to mission: read and write C without spooky bugs

The const Matrix

You've seen const char *src in a function signature. Now learn the whole picture β€” three declarations, two questions, and you'll never be confused again.

The problem: where does const lock the door?

In lesson 2's model solution, you wrote:

void mystrcat(char *dest, const char *src) { ... }

The const next to src tells the compiler: "I promise not to modify the characters src points at." That's useful β€” it lets you pass string literals without warnings, and it catches accidental writes to the source.

But C lets you put const in three different places around a pointer declaration, and each one locks a different door. This is one of the most confusing pieces of C syntax β€” but it resolves to two questions you can always ask.

The trick: read right-to-left

The Clockwise/Spiral Rule (simplified) Start at the variable name. Read const as "constant." Read * as "pointer to." Move right-to-left. Whatever const is closest to, that's what it locks.

Let's read all three forms aloud:

DeclarationRead aloud (right→left)What's locked?
const char *p; p is a pointer to a char that is constant The data β€” *p is read-only. p itself can change.
char * const p; p is a constant pointer to a char The pointer β€” p can't point elsewhere. *p can change.
const char * const p; p is a constant pointer to a char that is constant Both β€” locked down entirely.

That's the whole table. Three declarations, no more.

See each one in action

β‘  const char *p β€” "don't touch the data"

const char *p = "Hello";

p = "World";    // βœ“ pointer can change β€” now points at "World"

p[0] = 'J';         // βœ— compile error: read-only location

This is the one you used in mystrcat(dest, const char *src). The function promises: "I'll read src's characters, but I won't change them." The caller can pass literals, arrays, anything β€” without fear.

char const *p is the same thing const char *p and char const *p are identical to the compiler. Both mean "pointer to constant char." The C standard allows both orderings. Pick one and stick with it β€” most C code uses const char * (const-first). But if you see char const * in someone else's code, now you know it's the same.

β‘‘ char * const p β€” "don't move the pointer"

char buf[] = "Hello";
char * const p = buf;

p = buf + 1;    // βœ— compile error: cannot assign to 'p' (it's const)

p[0] = 'J';        // βœ“ the data itself is writable

This locks the arrow, not the target. The pointer is nailed in place β€” it will always point at buf β€” but you can freely modify the characters through it. Less common in function signatures, but you'll see it in loops where you want a fixed starting point.

β‘’ const char * const p β€” "don't touch anything"

const char * const p = "Hello";

p = "World";    // βœ— cannot change pointer
p[0] = 'J';         // βœ— cannot change data

Total lockdown. Used for things like configuration strings that should never change and never be redirected. Rare, but unambiguous when you see it.

The two questions you always ask

Forget the table. Ask this: When you see const near a pointer, ask:
(1) Can I change where it points? (Can I reassign the pointer variable?)
(2) Can I change what it points at? (Can I write through *p?)

If const is before the * β†’ answer (2) is no.
If const is after the * β†’ answer (1) is no.
If const is in both places β†’ both answers are no.
const char *p  β†’  data locked, pointer free
char * const p  β†’  pointer locked, data free
const char * const p  β†’  both locked

Why this matters for your code

Look at your concatenate_own_func.c:

char *q = str2;            // q reads from str2
while (*q != '\0') {
    *p = *q;                 // reads *q, writes *p
    p++;
    q++;
}

q only reads. It never writes through *q. So in a function version, src should be const char * β€” it tells the compiler (and anyone reading your code) that this pointer is input-only.

By contrast, p writes into dest. So dest must not be const β€” the whole point is to modify it.

void mystrcat(char *dest, const char *src);
        ^^^^^^^^ writable          ^^^^^^^^^^^^ read-only

This is the standard C convention for any function that transforms data: input is const, output is not. You'll see it in every library function signature β€” strcpy(char *dst, const char *src), strstr(const char *hay, const char *needle), etc.

Function parameters β€” where const earns its keep

const in function signatures is primarily a documentation tool. It tells callers "this function won't modify your data." But it's documentation the compiler enforces:

void process(const char *s) {
    s[0] = 'X';    // compiler error β€” caught before it becomes a bug
}

Without const, that line compiles fine β€” and silently corrupts the caller's data. With const, the compiler stops you. It's a seatbelt, not a handbrake.

Rule of thumb In function parameters, almost every pointer that is input-only should be const. If you find yourself passing char * when you only read, change it to const char *. It catches bugs and communicates intent. There is no downside.

Check yourself

For each declaration, answer: can you reassign the pointer? and can you write through *p? Then open the answer.

β‘  const int *x;

Show answer Reassign pointer? Yes. Write through *x? No.
const is before the * β†’ data is locked, pointer is free. This is the "pointer to a constant int" β€” same pattern as const char *.

β‘‘ int * const x = &n;

Show answer Reassign pointer? No. Write through *x? Yes.
const is after the * β†’ pointer is locked, data is free. x will always point at n, but you can change n's value through *x.

β‘’ char *p = "hello"; p[0] = 'H'; β€” what happens?

Show answer Undefined behavior β€” the string literal "hello" lives in read-only memory. Writing through p is a runtime violation. The compiler might not catch it (because p is declared as char *, not const char *).

This is exactly the bug from Lesson 1. The fix: declare p as const char *p = "hello"; and now the compiler will catch the p[0] = 'H' at compile time. This is why const matters β€” it turns silent UB into a visible error.

β‘£ Which const form goes in a function parameter for read-only input?

Show answer const type * β€” the "pointer to constant data" form. The function receives a pointer it can advance, but the data behind it is locked. Example: size_t mystrlen(const char *s); β€” mystrlen walks s forward reading characters, but never writes any.

πŸ› οΈ Hands-on β€” add const to your own functions

Open your concatenate_own_func.c (or any file with mystrlen, mystrcat, etc.) and:

  1. Wrap the concatenation logic into a function: void mystrcat(char *dest, const char *src)
  2. Wrap a mystrlen too: size_t mystrlen(const char *s) β€” notice the input is const
  3. Deliberately break it: add src[0] = 'X'; inside mystrcat. Compile. What happens?
  4. Remove the break. Compile clean. Run and confirm it still works.
  5. Now try: change dest to const char *dest. What breaks, and why?

The goal: feel the compiler push back when const is violated, so the rule becomes muscle memory, not theory.

What you can now do Read any const pointer declaration and immediately know what's locked and what's free β€” using the two-question test. You know that const char * and char const * are the same thing. And you know to use const on input-only function parameters as a compiler-enforced seatbelt.
Questions? Ask your agent. Anything murky β€” "what about const and malloc?", "why can I pass char[] to a const char * parameter?", "does const affect performance?" β€” type it and we'll dig in.