The flaw isn't the null termination but the lack of encapsulation.
No string representation is error-proof, if the application programmer open-codes the manipulations of it, taking on the responsibility for maintaining all necessary invariants of the representation, including memory management. If applications take on the responsibility of constructing and maintaining length + data strings, there will be bugs.
Null-terminated strings are actually a very good choice of string representation that is to be directly manipulated by programs, instead of just through opaque handles via library functions.
Null-terminated strings allow for elegant tail recursion like:
const char *my_strchr(const char *str, char c)
{
if (!str)
return NULL;
else if (*str == c)
return str;
else
return my_strchr(str + 1, c);
}
Compare with: (defun findel (list el)
(cond ((null list) nil)
((eql (car list) el) list)
(t (findel (cdr list) el))))
This leverages the fact that if s is a non-empty string, then s + 1 is also a string, which is its suffix. This is super cool and has no equivalent in the length + data representation.Compilers can merge string literals that have a common suffix; if "pet" and "carpet" occur in the same program, they can share storage.
Null terminated strings of byte-wide characters can be transmitted with no byte order issues. Not so when length fields are involved.
Being able to split a null-terminated string in-place according to some delimiter characters, by overwriting them with null bytes, is also handy, and greatly simplifies some programs which open-code this logic rather than relying on string libraries.
Null termination is nice for lexical scanning. Why? Because every string has an accessible byte that is one position past its last character, and has a unique value which can treated as a member of the input alphabet, and thus integrated into the pattern matching logic. Code which scans a null-terminated string for a lexical pattern doesn't have to do anything ugly and extraneous like checking an index against a maximum length; it just contains cases that handle zero as a part of the syntax.
Stream scanning code that looks for and handles EOF return from getc can easily be turned into string scanning code if it just handles zero instead of EOF.