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

后端 未结 2 1480
有刺的猬
有刺的猬 2020-11-30 15:09

I read somewhere that the following arrays can be passed to the following functions in such a manner, as shown below, however I do not understand what elements within the ar

相关标签:
2条回答
  • 2020-11-30 15:25

    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);
    
    0 讨论(0)
  • 2020-11-30 15:33

    First clear your confusion about arrays and pointers. Always remember that, arrays are not pointers.
    Among all of your declarations, only two of them

    int array[NROWS][NCOLUMNS];  
    int (*array4)[NCOLUMNS];  
    

    are arrays. Rest of them are pointers, not arrays.

    Isn't array1 and array2 an array of pointers?

    No. Never. array1 and array2 are int ** type, i.e, are of type pointer to pointer to integer.

    So when you pass them to the f3, all the pointers get passed?

    No. I explained it above that array1 and array2 are not array of pointers.

    And about array2 when passed to f2, f2 has a normal pointer in formal arguments, but array2 is an array of pointers, so how would you access individual rows and columns when passing array of pointers to f2?

    f2 expects a pointer to int as its first argument. *array2 is a pointer to int. Hence in the call

    f2(*array2, nrows, ncolumns);  
    

    a pointer to int is passed to f2 as its first argument, not an array of pointers.

    And how would u access individual rows and columns when passing array4 which is a pointer to a series of 1D arrays to function f2?

    Since you are passing a pointer to int type argument to f2, you can access only row.

    0 讨论(0)
提交回复
热议问题