strcat, UnrolledYou've called strcat(a, b) before. Now write the gears yourself โ and meet the two-pointer copy idiom, the most repeated pattern in C.
char dest[20] = "Hello";
char *str2 = "World";
char *p = dest; // 1. p starts at the beginning of dest
while (*p != '\0') { // 2. Advance p until it points at dest's '\0'
p++;
}
char *q = str2; // 3. q reads from str2
while (*q != '\0') { // 4. Copy in lockstep
*p = *q;
p++;
q++;
}
*p = '\0'; // 5. Re-seal the string
printf("Concatenated: %s\n", dest); // โ HelloWorld
Good news: you already know what this does. It's strcat(dest, str2) written out by hand. The book is making you build the engine so you can see every tooth on the gears.
p forward until it lands on dest's existing '\0'.
q, write to p, advance both.
'\0' at the new end. Mandatory.
Act 1 is literally your mystrlen pointer-walk โ but instead of counting, you just keep the pointer. You already know this move. The only new idea is Act 2.
Here is the whole lesson, as a diagram. Before Act 2 โ p has walked to dest's terminator, q sits at the start of str2:
During Act 2 โ five iterations of *p = *q; p++; q++;. Notice the old terminator at index 5 gets overwritten on the very first copy:
After Act 3 โ *p = '\0' writes the new terminator at index 10:
printf("%s", dest) reads from index 0 until it hits '\0' at index 10 โ prints HelloWorld. The garbage bytes at indices 11โ19 never matter โ the '\0' tells every string function where to stop.
'\0'. That's not a bug โ it's the plan.
The terminator of "Hello" only existed to mark where the string used to end. Once we're appending, that position is the start of new data. Which is exactly why Act 3 is mandatory: forget *p = '\0'; and the string has no end โ printf walks off into garbage. Forget the seal, get demons.
*p = *q; // read what q points at, write it where p points at
p++; // advance the write head
q++; // advance the read head
This two-pointer lockstep is the C idiom for "move bytes from here to there." It's not just strcat โ it's strcpy, it's memcpy, it's every custom buffer copy you'll ever write. Two heads on a zipper. Once you see it, you'll see it everywhere.
*p++ = *q++;
Postfix ++ binds tighter than *, so this means "copy, then advance both." Grown-up C code uses it constantly. Readable once you know it; a trap until you do. Keep writing the three-line version for now โ clarity beats cleverness while you're learning. Switch to the compact form only when the long form starts to feel tedious.
Before any strcat โ hand-rolled or library โ run the checklist from the reference:
dest have real storage? โ
char dest[20] โ 20 real bytes on the stack."Hello" (5) + "World" (5) + '\0' (1) = 11 โค 20. โ
This is why your exercise_concatinate_inputs.c crashed and this doesn't. Same operation, same idiom โ the only difference is whether real, correctly-sized storage backs the write pointer. The principle is doing real work for you now.
*p = '\0';?World into indices 5โ9, then stops. But index 10 still holds whatever garbage was there. printf("%s") keeps reading past 'd' until it happens to find a zero byte โ printing HelloWorld followed by random garbage, or crashing, or both. Act 3 is not optional. Every string you build by hand must end in '\0'.
p point โ and what's stored there?p points at dest[5], which holds '\0' (the terminator of "Hello"). That's the correct insertion point: the first slot of "new" data. The first copy (iter 1) overwrites this '\0' with 'W' โ which is fine, because Act 3 will place a new terminator further along.
*q != '\0' and not *p != '\0'?q walks the source (str2). We stop copying when the source runs out โ i.e. when q reaches its '\0'. Testing *p would be wrong: p walks the destination, whose contents are partially garbage. The copy length is governed by the source, always.
dest were only 8 bytes โ char dest[8] = "Hello";?"Hello" uses 6 bytes (5 + '\0'); dest[8] has 2 free slots. Appending "World" (5 chars) writes past the end into memory that isn't yours. Same bug as common_str_ops.c from Lesson 1 โ bug (2), not enough storage. The hand-rolled strcat has no idea how big dest is. Neither does the library version. Buffer sizing is always the caller's job.
Make the code reusable by wrapping it in a function:
void mystrcat(char *dest, const char *src) {
// ... your code here ...
}
dest and src instead of dest/str2.main: mystrcat(dest, str2); then print.HelloWorld, you own the pattern.mystrcat(dest, "!!!"); โ right after the first. Confirm dest now reads HelloWorld!!!. (Why does it work twice? Because Act 1 re-finds the new end each time.)#include <stdio.h>
void mystrcat(char *dest, const char *src) {
char *p = dest;
while (*p != '\0') { // Act 1: find the end of dest
p++;
}
const char *q = src;
while (*q != '\0') { // Act 2: copy in lockstep
*p = *q;
p++;
q++;
}
*p = '\0'; // Act 3: re-seal
}
int main(void) {
char dest[20] = "Hello";
const char *str2 = "World";
mystrcat(dest, str2);
mystrcat(dest, "!!!");
printf("Concatenated: %s\n", dest); // โ HelloWorld!!!
return 0;
}
Notice const char *src: we promise never to write through src. That's the same const instinct from Lesson 1, applied at the function boundary โ and it lets you pass a string literal ("!!!") without warning, because literals are const-qualified at heart.
strcat, strcpy, and any hand-rolled copy loop and see the same three-act structure underneath: find the destination's end, copy in two-pointer lockstep, re-seal with '\0'. You've also seen the compact idiom *p++ = *q++; and know what it expands to. That idiom will recur for the rest of your C life.
*p++ = *q++ work left-to-right?", "what's the difference between const char * and char * const?", "how would I write mystrcpy with the same pattern?" โ type it and we'll dig in. The two-pointer idiom is worth knowing cold; everything downstream builds on it.
strcat โ what the library version guarantees (and conspicuously does not: bounds checking).strcat is where you feel it).