A 2-dimensional array in C is no larger or smaller than an array of arrays. A 3-dimensional array is an array of arrays of arrays. And so on.
The corresponding section from C99 standard is 6.5.2.1, “Array Subscribers”:
Operators followed by an index denote a multidimensional array element. If E is an n-dimensional array (n ≥ 2) with dimensions i × j ×., × k, then E (used as except for lvalue) is converted to a pointer to an (n - 1) -dimensional array with sizes j ×., × k. If the unary * operator is applied to this pointer explicitly or implicitly as a result of a subscription, the result is a directional (n - 1) -dimensional array, which itself is converted to a pointer if used as other than lvalue. It follows that the arrays are stored in lowercase order (the last index changes faster).
Some confusion is caused by the fact that the index operator is defined in terms of pointer arithmetic. This does not mean that arrays "really point" - and in fact they are very definitely not. Declaring an array object does not create any pointer objects at all (unless, of course, it is not an array of pointers). But an expression that refers to an array usually (but not always) "decomposes" into a pointer to the first element of the array (the value of the pointer, not the pointer object).
Now simple array objects, no matter how they are measured, are quite inflexible. Prior to C99, all array objects should have a fixed size defined at compile time. C99 introduced variable-length arrays (VLAs), but even so, the size of the VLAs is fixed when it is declared (and not all compilers support VLAs, even 12 years after the release of the C99 standard).
If you need something more flexible, the general approach is to declare a pointer to the element type, then select the array with malloc() and point to the first element of the array:
int *ptr = malloc(N * sizeof *ptr); if (ptr == NULL)
This allows you to refer to the elements of the array allocated by the heap using the same syntax that you would use for the declared array object of a fixed size, but in arr[i] expression arr splits to a pointer, whereas in ptr[i] `ptr is already pointer.
The same can be extended to higher dimensions. You can select an array of pointers, and then initialize each pointer to point to the beginning of the selected array.
This gives you something that is very similar to a 2-dimensional (or more) array, but you need to manage the memory yourself; what's the price of more flexibility.
Strictly speaking, this is not a two-dimensional array. A 2-dimensional array, as I said above, is just an array of arrays. It is probably not completely unreasonable to think of it as a two-dimensional array, but this contradicts the use in the C standard; it looks like a link to a linked list as a 1-dimensional array.
comp.lang.c FAQ is a good resource; section 6, which covers arrays and pointers, is especially good.