trouble understanding what elements are passed when passing multidimensional arrays to functions

What you can percieve as arrays in C are not always arrays in terms of language concepts.

C is an language from the 70’s. It’s very close to machine implementation compared with its more abstract offsprings. So you must take implementation into account if you want to understand what lies beyond confusingly similar syntactic elements.

Both pointers and arrays can be accessed through (square) bracket notation.

Bracket notation is undoubtely useful, but it is the root of all evil as far as confusions between pointers and arrays is concerned.

f[i] will “work” for pointers and arrays as well, although the underlying mechanisms will differ, as we will see.

Relationship between pointers and arrays

Let’s start with variable declarations.

Pointers

float * f simply tells the compiler that the symbol f will someday reference an unknown number of floats.

f is not initialized. It is up to you to decide where the actual data will be and to set f to point to them.

Pointers arithmetics and bracket notation

Keep in mind that when you add/substract a value to a pointer, the unit is the size of the pointed type.

float * f;
float * f3 = f+3; // internal value: f + 3 * sizeof (float)

// these two statements are identical
*f3 = 1;
*(f+3) = 1;

Since writing *(f+i) is awkward when you want to reference contiguous data from a pointer, the bracket notation can be used:

f[3] = 1; // equivalent to *(f+3) = 1;

Regardless of the notation used, the address of f[3] is computed like that:

@f[3] = f +3* sizeof (float)

You could consider f functionally as a (dynamic) array, but as C sees it, it’s still a pointer, referenced through a syntax that makes it look like an array.

Arrays

float f[10] still tells the compiler that f will reference some floats, but it also

  • allocates the requested number of floats at the appropriate location
    • on the stack if f is an automatic local variable
    • in the static data (aka BSS) if f is a global or static variable
  • considers the symbol f as a constant pointer to the first of these float values

Even though array creation syntax might be confusing, an array will always have a fixed size known at compile time.

For instance, float f[] = {2,4,8} declares an array of length 3, equivalent to float f[3] = {2,4,8}. The dimension can be omitted for convenience: the length mirrors the number of initializers without forcing the programmer to repeat it explicitey.

Unfortunately, the [] notation can also refer to pointers in some other circumstances (more on that later).

Bracket notation and arrays

Bracket notation is the most natural way of accessing array contents.

When you reference an array, the compiler knows it’s an array. It can then access the data based on the array first element, like that:

@f[3] = f +3* sizeof (float)

In case of single-dimensional arrays (but in that case only!), you can see that the address computation is exactly the same as for a pointer.

Arrays as pointers

Since an array is also considered a (constant) pointer, you can use an array to initialize a pointer, thought the reverse is obviously false (since an array is a constant pointer and as such its value cannot be changed).

Illustration

void test (void)
{
    float* f1;
    float  f2[10];
    float  f3[];         // <-- compiler error : dimension not known
    float  f4[] = {5,7}; // creates float f4[2] with f4[0]=5 and f4[1]=7

    f1[3] = 1234; // <--- write to a random memory location. You're in trouble
    f2[3] = 5678; // write into the space reserved by the compiler

    // obtain 10 floats from the heap and set f1 to point to them
    f1 = (float *) calloc (10, sizeof(float));
    f1[3] = 1234; // write into the space reserved by you

    // make f1 an alias of f2 (f1 will point to the same data as f2)
    f1 = f2;              // f2 is a constant pointer to the array data
    printf ("%g", f1[3]); // will print "5678", as set through f2

    // f2 cannot be changed
    f2 = f1; // <-- compiler error : incompatible types ‘float[10]’ / ‘float *’
}

Going multidimensional

let’s extend our example to the two-dimensional case:

float    f2[3][10]; // 2d array of floats
float ** f1;        // pointer to pointer to float

f1 = f2; // <-- the compiler should not allow that, but it does!

f2[2][5] = 1234;           // set some array value
printf ("%g\n", f2[2][5]); // no problem accessing it

printf ("%g\n",f1[2][5]);  // bang you're dead

let’s see what happened here

when you declare float f2[3][10], the compiler allocates the 30 required floats as a contiguous block. The first 10 floats represent f[0], the next ten f[1], etc.

When you write f2[2][5], the compiler still knows f is an array, so it can compute the effective address of the required float like this:

@f2[2][5] = f + (2* 10 +5) * sizeof (float)

You can also access pointers through multiple brackets, provided the pointer has the proper number of reference levels:

When referencing the pointer, the compiler simply applies pointer arithmetics in succession:

float h = f1[2][5];

is equivalent to:

float * g = f1[2]; // equivalent to g = *(f1+2)
float   h = g[5];  // equivalent to h = *(g +5)

f1[2][5] is handled by the compiler as *(*(f1+2)+5).
The final address will be computed like this:

@f1[2][5] = *(f +2* sizeof (float *)) +5* sizeof (float)

You’ve asked for it, you got it

Beyond the same bracket notation lie two very different implementations.

Clearly, when trying to access f2 data through f1, the results will be catastrophic.

The compiler will get the 3rd float from f2[2], consider it as a pointer, add 20 to that and try to reference the resulting address.

If you write some value through this kind of wrongly initialized pointer, consider yourself lucky if you get an access violation instead of silently corrupting some random four bytes of memory.

Unfortunately, even though the underlying data structure cannot be accessed properly unless the compiler is aware that f2 is an array, f2 is still considered a constant float** pointer.

In an ideal world it should not, but in C (alas!), it is.

It means you can assign a pointer to an array without the compiler complaining about it, even though the result makes no sense.

Function calls

Both arrays and pointers can be passed as parameters to functions.

However, to avoid such catastrophic misinterpretations as in the previous example, you must let the compiler know whether what you are passing to the function is an array or a pointer.

Here again, due to the compiler considering an array as a constant pointer, you will be allowed to do silly things like declaring an array and passing it to a function like a pointer.

Empty square brackets

To make things worse, the syntax of function parameter declaration allows to use brackets in a way that makes confusion between arrays and pointers even more likely.

void f (float f1[]);

is handled exactly as

void f (float * f1);

even though the variable declaration

float f1[];

will yield an error instead of considering it as an alternate way of declaring float * f.

You could say that the [] notation is allowed to designate pointers, but only in function parameters.

Why it has not be allowed as a variable declaration might be open to debate (among other things, it would be ambiguous with the float f[] = { ... } initialized array declaration syntax), but the net result is that function parameter decalaration introduce a notation that adds another layer of confusion.

For instance, the famous argv parameter can be declared any odd way:

int main (int argc, char ** argv)
int main (int argc, char * argv[])
int main (int argc, char argv[][])

On the other hand, provided you are fully aware of the difference between pointers and arrays, the empty brackets are somewhat more convenient than the pointer notation, especially in that case:

void fun (float f[][10]); // pointer to arrays of 10 floats

the equivalent pointer syntax forces you to use brackets:

void fun (float (* f)[10]);

which you can’t avoid when declaring such a variable:

float (* f)[10]; // pointer to array of 10 floats
float f[][10];   // <-- compiler error : array dimension not known

Conclusion

As far as function prototypes are concerned, you have the choice between syntactic variants, but if the variable you pass to the function doesn’t match the prototype, it will all end in tears.

float      ** var1;           // pointer to pointer to float
float       * var2[10];       // array of pointers to float
float      (* var3)[10];      // pointer to array of floats (mind the brackets!)
float         var4[10][10];   // array of arrays of floats (2d array of floats)

// using empty brackets notation
void fun1 (float f[  ][  ]);
void fun2 (float f[10][  ]);
void fun3 (float f[  ][10]);
void fun4 (float f[10][10]);

// using same syntax as for variables declaration
void fun1 (float ** f);
void fun2 (float * f[10]);
void fun3 (float (* f)[10]); // <-- [] notation (arguably) easier to read
void fun4 (float f[10][10]); // must always use square brackets in that case

// even more choice for multiple level pointers
void fun1 (float * f[]);

// any funI (varJ) call with I != J will end up in tears

A final word of advice

It is of course a matter of personal taste, but I would recomend the use of typedef as a way of getting a bit more abstraction and limit the use of C syntactic oddities to a minimum.

// type definition
typedef float (* tWeirdElement)[10];
typedef tWeirdElement (* tWeirdo)[10]; // pointer to arrays of 10 pointers
                                       // to arrays of 10 floats 

// variable declaration
tWeirdo weirdo;

// parameter declaration
void do_some_weird_things (tWeirdo weirdo);

Leave a Comment