thecodingidiot.com

The ToolkitPointer Depth

Pointer Depth

A char is one byte — eight bits, 256 possible states. The ASCII standard assigns a meaning to each: 65 is A, 32 is a space, 10 is a newline. You saw how bits and voltage produce those states in f03a/01. What follows here is what happens when you start chaining bytes together — and how the number of pointers you need reflects the shape of the data.

One pointer: a string

A single char * gives you a start address. Pointer arithmetic steps you one byte forward at a time — one character per step. The \0 terminator is the agreed end: when the byte at the current address is zero, the string is over. One axis of movement, one pointer.

The information is a vector. It has a beginning — the address the pointer holds — and an end — wherever \0 sits. You traverse it in one direction. Here is "hello" laid out in memory, one byte per cell, with the null terminator at the end:

[0][1][2][3][4]\0
hello\0

Two pointers: a table

A string is a char *. An array of strings is a block of char * values sitting next to each other in memory. To point at the start of that block, you need a pointer to a char * — which is char **.

char  *words[3];
 
words[0] = "hello";
words[1] = "world";
words[2] = NULL;

The subscript notation is shorthand for pointer arithmetic. These two forms are equivalent at every level:

char  *words[3];        /* subscript notation — fixed-size block */
char **words;           /* pointer notation — pointer to char* */
words[0] = "hello";     /* subscript notation */
*(words + 0) = "hello"; /* pointer notation */

The outer pointer selects the row. The inner pointer walks the characters in that row. Two axes, two pointers. Think of a spreadsheet where each row is a string. The same two words in binary — each byte is eight bits, the final 00000000 is \0:

[0][1][2][3][4][5]
words[0]011010000110010101101100011011000110111100000000
words[1]011101110110111101110010011011000110010000000000

In decimal — the integer each byte encodes:

[0][1][2][3][4][5]
words[0]1041011081081110
words[1]1191111141081000

In ASCII — the character each decimal value represents:

[0][1][2][3][4][5]
words[0]hello\0
words[1]world\0

argv in main is exactly this. argc tells you how many rows there are; \0 terminates each string on the inner axis.

Three pointers: a cube

Add another layer. If a table is a block of strings, a cube is a block of tables. char *** is a pointer to a char ** — the start of an array of arrays of strings. Three axes, three pointers. The geometry still holds: you can picture height, width, and depth.

The limit

Beyond the cube, physical intuition fails. There is no fourth direction you can point your finger in.

The word "dimension" encodes this — it describes a direction in which things can be separated. In Greek, διάσταση [thee-AH-stah-see] literally means "standing apart." Beyond three, the vocabulary runs out.

Beyond char ***, the pointer axes no longer map to anything you can picture. You are managing bookkeeping labels, not directions in space.

Data can genuinely be higher-dimensional. A video frame has three axes: x, y, and colour channel. Add a frame index and you have four. Add a batch index — as machine learning models process groups of images — and you have five.

C programmers do not reach for float ***** to represent that. They use a single flat array and a set of integers describing the shape: width, height, channels, frame count, batch size. Access becomes an index calculation instead of a chain of dereferences.

The pointer nesting communicates structure to the compiler. When the structure has more axes than geometry allows, the integers carry it instead. The geometry is replaced by arithmetic.

Where the curriculum goes

The immediate next step is tci_printf. It needs tci_strlen to measure each string argument, tci_memchr to scan the format string for the next % specifier, and the digit-extraction loop — the same logic tci_atoi uses in reverse — to convert %d and %x to characters. Every piece is already in libtci. The next chapter assembles them into a function that puts text on a screen.

Every arcade cabinet that ever displayed "HIGH SCORE" solved this problem by hand. The Pac-Man score counter, the NES tile display, the PlayStation save screen — all of it is printf, written on platforms that had no standard library to call. You now have what those programmers had.

You will reach char *** further along. A pipeline like ls -la | grep foo breaks down naturally into three layers:

  • char *word — a single token: "ls"
  • char **argv — one command's arguments: ["ls", "-la", NULL]
  • char ***cmds — the full pipeline: [["ls", "-la", NULL], ["grep", "foo", NULL], NULL]

Three layers, each null-terminated, each mapping to one axis. The cube arrives by necessity, not by design. libtci stays at char **. The third dimension appears when the problem genuinely needs it.

A note on building this

A library is invisible until something uses it. Right now libtci.a is an archive sitting in your working directory. It does nothing you can see.

It is tempting to read through c01 without writing a single function. Reading and building are different skills, and you can mistake one for the other for a surprisingly long time.

The magic is real — it is the result. But you only reach it by writing. Write every function. The tester exists for this reason.

Write the comments while the function is fresh. A comment that explains why tci_memmove copies backwards when dst > src costs thirty seconds to write and saves thirty minutes to rediscover. The compiler discards every byte of it — the binary is identical — so the program pays nothing. But the next time you open that file cold, six months from now, the reason is already there.

The tester tells you whether your function is correct. It does not tell you whether you own it. The bar for ownership: write every function without any external help — no AI, no autocomplete, no reference tab open. Memorisation counts. Looking it up does not.

The clock is a second check. Open a blank file and write a function from whatever you carry in your head:

start=$(date +%s); vim -u NONE tci_strlen.c; elapsed=$(($(date +%s) - start)); printf "%s  %ds\n" "$(date +%Y-%m-%d)" "$elapsed" | tee -a tci_strlen.score

The score is appended to tci_strlen.score alongside the date. Come back a week later and do it again. Read the history for any function:

cat tci_strlen.score
2026-05-12  184s
2026-05-19  76s
2026-05-26  48s

The tester is the correctness check; your times are the ownership check. When you can write the whole library in one sitting, from what you carry in your head, without asking anyone or anything — that is what it means to know it.

Memorising a function and writing it once is better than reading it ten times. Writing it ten times is better than writing it once. Repetition is the mechanism, not a redundancy. Typing speed increases, muscle memory forms, and what currently requires thought becomes automatic. Hours at the keyboard compound.

There is a view that says: primitives do not matter; AI writes those; what counts is speed and architecture. It is wrong in the direction that matters.

You cannot architect what you have never built. Speed in a codebase — knowing where time is spent, knowing what to avoid, knowing when a loop is tight enough — comes from hours of writing the loops. The programmer who has written tci_memmove knows why it exists and when it matters. The one who has only called it does not.

up next

The Voice

The Voice