present dayThe convention as it stands
A string in C is a run of bytes followed by a zero. Nothing records its length. To find the end you walk it, and to know whether a write will fit you have to already know something the string itself does not carry.
The alternative was available and in use at the time. Several languages of the period put a count in front of the characters, which makes the length a lookup rather than a loop.
1993, describing 1972Ritchie, on the development of C
The clearest account of why the terminator was chosen comes from the language's author, writing two decades later. He describes the predecessor languages, the machine the work was done on, and the trade-off as it looked then.
This is a retrospective by a participant, which is a specific kind of evidence: authoritative about intent, and written long after the decision, by somebody who knew how it turned out.
Arithmetic on the alternative
A single byte of count limits a string to 255 characters. That was a real limit, and languages that used it carried it for decades.
Spending two bytes lifts the limit and costs two bytes on every string in the program, including the very short ones, of which there are many. On a machine whose entire address space was measured in tens of kilobytes, that was not a rounding error.
A terminator costs exactly one byte at any length, and it needs no decision about how wide the count should be.
1972The machine the decision was made on
The context that makes the trade-off legible is the size of the machine. Address space was measured in tens of kilobytes, and the operating system, the compiler and the program had to share it.
In that setting a per-object overhead is not a detail. A program handling several thousand short strings pays the count on every one of them, and short strings are the common case: field names, file names, messages, the pieces of a parsed line.
A terminator has the same cost per string regardless of length, which means it is cheapest exactly where strings are most numerous.
1972The instruction that made it comfortable
There is a second reason the convention felt natural, and it is about the hardware rather than the memory budget. The machine had addressing modes that advance a pointer as a side effect of using it, so walking a sequence until a zero appears compiles to a very short loop.
Checking a counter each time round requires holding the count somewhere and decrementing it, which on a register-poor machine is a real cost. The terminated form suited the instruction set, and code written that way looked and ran better than the alternative.
That is the honest shape of most decisions in this section: not one reason but three small ones pointing the same way, none of which survives into the present.
1970s onwardsWhere the alternative survived
Length-prefixed strings did not disappear; they simply lost the language that spread furthest. Several languages of the same era kept the count, and the practical differences are visible today in any system that mixes them: a length is available for free on one side of an interface and has to be computed on the other.
Modern languages have almost all returned to carrying the length, and several carry a capacity as well. The convention that won in 1972 did not win on merit for the following fifty years; it won on the constraints of a machine that was retired long ago.
present dayThe interface problem it created
Because one enormous body of code assumes a terminator and another assumes a count, every boundary between them needs a conversion, and conversions are where the mistakes live: a string containing a zero byte is truncated in one direction, and a string without one runs past its end in the other.
That failure mode is not a bug in either convention. It is the predictable cost of two reasonable answers to the same question meeting at an interface neither anticipated.
1988 onwardsWhat the choice took away
Two consequences follow directly and are still being paid for.
Finding the length is a loop, so operations that look constant are linear, and code that repeatedly asks for a length turns an inner loop into a quadratic one without anything in the source suggesting it.
And the string carries no bound, so a copy has nothing to check against. The family of failures that follows from that has its own entry in the part on mistakes, and the first widely-noticed instance was in 1988.
What we cannot verify
How much of this was deliberate design and how much was inherited from the predecessor language without re-examination is not settled by the record. The author's account is the best source available and is still a recollection. No contemporaneous document sets out the reasoning at the moment of the decision, and confident claims that it was purely about saving a byte cite none.
In short
- A C string is a run of bytes and a zero, and carries no length.
- Length-prefixed strings existed at the time and were the obvious alternative.
- One byte of count caps a string at 255; two bytes cost every string in the program.
- A terminator costs one byte at any length and needs no decision about width.
- The cost is that length is a loop and a write has nothing to check against.
- The author's account is a retrospective, and no contemporaneous document states the reasoning.