Curiously the article doesn't mention two-dimensional arrays and they're curios because they bring a certain asymmetry with them. It always tripped me over the most in C because I otherwise find the language very "symmetrical". It often feels like in design of this language the beauty of expressing certain things took priority over readability or safety which I admire in a way. But somehow not in the case of the two-dimensional arrays.
If you see a[i][j] it could mean two completely different things:
1) "a" is a continuous chunk of memory of N*M bytes, so it behaves as char*; a[i][j] == *(a + i*M + j)
2) "a" is an array of char* pointers that point to N completely distinct memory chunks of size M, so it behaves as char**; a[i][j] == *(*(a + i) + j)
With flat arrays the difference between an array as a variable and a pointer to the first element is literally negligible because you won't even see the difference in the assembly. This is why the automatic decay-to-pointer makes a lot of sense.
But that breaks completely with multiple dimensions. You definitely see the difference in the assembly because the memory layout is so different.
C array types are weird because C doesn't really need arrays. It's not what C was about.
But if you designed a language in the era where Fortran, THE array language, reigned supreme, nobody would use your language. The mindshare Fortran had is difficult to convey now, half a century later.
Think of it like making a chatbot today and not mentioning AI or LLMs, that's what making a language without arrays would have felt like in 1970.
The real lack is that C doesn't have slices. Slices can do most of what pointers into arrays can do, with sane semantics. Slices were invented surprisingly late. They were implementable in the 1970s, but didn't really show up until the 1990s.
Now that we have slices, the demand for pointers into the middle of an array has much decreased.
I had a go at retrofitting C with slices over a decade ago.[1] Too much political hassle.
And while the (*array_ptr)[3] notation take a moment to get used to, it is very logical. If you have a pointer to an array, you dereference it first and then indx into it. Again, useful for bounds checking: https://godbolt.org/z/ao1so9KP7
In C declaration syntax, there is a "stem" called declaration specifiers consisting of specifiers and qualifiers. That's where int can appear. After that, there is a declarator. In some cases, multiple declarators separated by a comma, which share the same "stem".
int a, b, *c; // one stem consisting of "int", three declarators.
The * is declarator syntax for deriving a pointer type. It never appears such that a type specifier would come after it somewhere to the right.
Some languages have extended the C declaration syntax such that the type derivators can be moved from the declarator part to the "stem". For instance, as an alternative to:
int a[10];
you can write
int[10] a;
This is how we could get
**int[3]
as a declarator stem indicating an array of 3 pointers to pointers to int. But it's not in C.
And if you want 'int **arr[a][b]', it's a value that when you say 'x = **arr[m][n]', will evaluate to an int and assign it to x. Postfix has higher precedence than prefix.
There is a history to it; in one of the predecessor languages, like B, Ritchie actually had arrays that had a hidden pointer to their start. The "array to pointer decay" was actually a real operation that loaded an address from memory, and it was possible to twiddle the bits to relocate an array. One problem with it was no way to initialize such a pointer field that would allow an array to live in dynamically allocated storage (no constructors in the language).
So in short, the bad design (array values produce pointers) was informed by conceptual compability with an earlier design in which that was literally happening.
Not just this it is important to remember that there was no "aha!" moment where C was created whole-cloth by writing the first compiler in B then cross-compiling.
The language B was evolved in-place by adding new features, then editing the compiler source to make use of those new features, then repeating. They simply started calling it "New B". At some point the language had evolved sufficiently that they decided to call it C.
The semantics of arrays were inherited from B and simply never changed. Part of me suspects this was also because it was seen as "clever" at the time. Look ma, we let arrays turn into pointers! Isn't that clever?
When you look at pre-ANSI C function prototypes you wonder "where are the parameter types?" because there are none. The compiler didn't bother to check. Part of that was perhaps for implementation reasons but a big part of that was the feeling or culture inherited from B: in that language you just had words of memory. You were free to interpret any word of memory as any data type you liked. So duh of course it is up to you to decide how many parameters your function received and of what type. If the caller supplied a different number or different types? Don't do that.
If you are coming from that sort of world clever tricks like arrays decaying to pointers or automatically converting between data types and sizes seems perfectly natural. Anything C offers above and beyond that is an improvement from B after all.
This is one of the things that I feel is an inappropriate abstraction that is around for historical reasons. When I do FFI to call C from rust, I usually wrap the generated API (Which is pointer based) into rust's &[] array syntax. Arrays/lists/Vecs etc in most non-C languages feel like an abstraction over a collection of items; I feel like C's exposing the pointer directly is taking a low-level memory/MMIO operation and inserting it into business logic. Conceptually, I like to keep them separate; pointers for writing drivers, accessing registers, writing to flash memory etc. Arrays/lists/vecs for higher level operations on collections.
Tangent: I have a pet theory that part of Zig's raison d'etre is to fix some of the problems with C, while accommodating its pointer-based data structures, and the resulting patterns.
Learning to program with pointers is enormously useful. It's simply bad software engineering to not use typing to enforce constraints on access to pointers (or addresses, or however you'd like to term them)
IIRC that talk of about using indices (u32) to represent data in an array. That is orthogonal to representing that information in the type system since you can just type the index
At your service! D fixed it, and I'm sorry C users have suffered as the array-to-pointer decay blasted their kingdom. Fixing it in C is easy and should be the #1 priority.
In C a[i] is converted to *(a+i) internally. i[a] is converted to *(i+a). Array names also act as pointers in c. so (a+i) or (i+a) give an address (using pointer arithmetic) that is dereferenced using
Embedded programming is still in C for a lot of micro controllers and whatnot. If you’re programming with limited resources it’s essential to understand pointers and arrays. Likely you won’t be doing anything useful without them
If you see a[i][j] it could mean two completely different things:
1) "a" is a continuous chunk of memory of N*M bytes, so it behaves as char*; a[i][j] == *(a + i*M + j)
2) "a" is an array of char* pointers that point to N completely distinct memory chunks of size M, so it behaves as char**; a[i][j] == *(*(a + i) + j)
With flat arrays the difference between an array as a variable and a pointer to the first element is literally negligible because you won't even see the difference in the assembly. This is why the automatic decay-to-pointer makes a lot of sense.
But that breaks completely with multiple dimensions. You definitely see the difference in the assembly because the memory layout is so different.
But if you designed a language in the era where Fortran, THE array language, reigned supreme, nobody would use your language. The mindshare Fortran had is difficult to convey now, half a century later.
Think of it like making a chatbot today and not mentioning AI or LLMs, that's what making a language without arrays would have felt like in 1970.
I had a go at retrofitting C with slices over a decade ago.[1] Too much political hassle.
[1] https://www.animats.com/papers/languages/safearraysforc43.pd...
https://godbolt.org/z/PzcjW4zKK
And while the (*array_ptr)[3] notation take a moment to get used to, it is very logical. If you have a pointer to an array, you dereference it first and then indx into it. Again, useful for bounds checking: https://godbolt.org/z/ao1so9KP7
Not sure why, maybe it doesn't feel like C anymore, maybe it feels hacky?
typically if you're passed an array you'd want to get more anyway, so you'd get passed a struct. Not sure.
Some languages have extended the C declaration syntax such that the type derivators can be moved from the declarator part to the "stem". For instance, as an alternative to:
you can write This is how we could get as a declarator stem indicating an array of 3 pointers to pointers to int. But it's not in C.So in short, the bad design (array values produce pointers) was informed by conceptual compability with an earlier design in which that was literally happening.
The language B was evolved in-place by adding new features, then editing the compiler source to make use of those new features, then repeating. They simply started calling it "New B". At some point the language had evolved sufficiently that they decided to call it C.
The semantics of arrays were inherited from B and simply never changed. Part of me suspects this was also because it was seen as "clever" at the time. Look ma, we let arrays turn into pointers! Isn't that clever?
When you look at pre-ANSI C function prototypes you wonder "where are the parameter types?" because there are none. The compiler didn't bother to check. Part of that was perhaps for implementation reasons but a big part of that was the feeling or culture inherited from B: in that language you just had words of memory. You were free to interpret any word of memory as any data type you liked. So duh of course it is up to you to decide how many parameters your function received and of what type. If the caller supplied a different number or different types? Don't do that.
If you are coming from that sort of world clever tricks like arrays decaying to pointers or automatically converting between data types and sizes seems perfectly natural. Anything C offers above and beyond that is an improvement from B after all.
Tangent: I have a pet theory that part of Zig's raison d'etre is to fix some of the problems with C, while accommodating its pointer-based data structures, and the resulting patterns.
https://www.hytradboi.com/2025/05c72e39-c07e-41bc-ac40-85e83...
But in other news most don't know that a[3] == 3[a]
https://stackoverflow.com/a/16163840
In C a[i] is converted to *(a+i) internally. i[a] is converted to *(i+a). Array names also act as pointers in c. so (a+i) or (i+a) give an address (using pointer arithmetic) that is dereferenced using
https://www.tiobe.com/tiobe-index/ada/