The array itself is always a continuous chunk of memory, no matter how many dimensions it has and what the array item type is. The array item type just happens to be a pointer. But a pointer is also just a bunch of bits in memory, like any other type.
I rather would say it works nicely in auto-generating the complex indexing operation for n-dimensional arrays which makes it a lot more convenient and less error-prone to write such code. The compiler may also flatten a loop.
The array of pointer hack used previously to similate 2d arrays using an array to pointers to arrays should not be used outside of special algorithms, as it is error prone and slow.
Array memory is on the stack. The size of that array is actually not known at run time, its only known at compile time, where any reference to that length gets resolved by the compiled.
If your 2d array sits on the stack, then inferring memory layout is pretty easy. If you are dealing with pointer that was passed to a function, then you can't assume anything about data size or limits, which is why many functions that take pointers take a size parameter as well.
Right, but 2d arrays come into this picture with their own quirks again. You're not just passing the size as the parameter, you can pass it as a "special" parameter that influences how the compiler will interpret other parameters. E.g. in C99 you can do this:
void do(size_t x, size_t y, int a[][y]);
Here "y" plays the critical role because it will be used to compute offsets in the a[i][j] expression. For 1d arrays this doesn't happen.Of course it's still generalizable as "all but the outermost dimensions should be known" and for 1d array the outermost dimension is the only dimension. Still, this whole thing always felt a bit odd to me.
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
Current: All postfix
*ptr[3] ptr[3]* // indexed access, then deref
(*ptr)[3] ptr*[3] // deref, then indexed access*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.
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.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 "restrict" keyword was invented to solve this but it still has weaker semantics than original Fortran arrays. It can still solve a big share of problems, but it never got proper adoption and never even made it into C++.
i used fortran recently to see how "slow" python is, i did matrix multiplies by hand in .c, and .py. Now i didn't write the fortran, the AI did, but i remember enough that i verified what it did was sane, also the other two i wrote did agree with results.
fortran 1 unit of time
C 1.7 unit of time
python 2.2 unit of time
for the same matmuls.anyhow, 1996-ish. crazy.
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.
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...
[1] https://www.open-std.org/jtc1/sc22/wg14/www/wg14_document_lo...
But yes, I was thinking about making a custom-allocator version of vec.
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
Most modern language start with fixing C's warts (good) but then at some point turn into 'tech manifestos' (for lack of a better word). C is refreshengly devoid of opinion and that's what makes it so extremely flexible and timeless.
Compared to what?
Anyway I find it hard to believe enabling suicide is a good thing
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.