简体   繁体   中英

Explanation how pointers and multidimensional arrays work in C

I am trying to understand the following code.

#include <stdio.h>
#include <stdlib.h>

void print2(int (* a)[2]) {
    int i, j;
    for (i = 0; i < 3; i++ ) {
        for (j = 0; j < 2; j++ ) {
            printf("%d", a[i][j]);
        }
    printf("\n");
    }
}

void print3(int (* a)[3]) {
    int i, j;
    for (i = 0; i < 2; i++ ) {
        for (j = 0; j < 3; j++ ) {
            printf("%d", a[i][j]);
        }
    printf("\n");
    }
}

int main() {
    int a[] = { 1, 2, 3, 4, 5, 6 };
    print2((int (*)[2]) a);
    print3((int (*)[3]) a);
    return 0;
}

Running the code returns following output in console:

12
34
56
123
456

My problem is I don't understand where these numbers come from. I have trouble understanding what is actually going on in this code. More specifically, I'm uncertain of what this means:

int( (* a)[2])

I hope someone can explain this code to me, because I really want to understand how pointers and multidimensional arrays work in C.

void print2(int (*a)[2]) { /*...*/ }

inside the function print2 a is a pointer to arrays of 2 ints

void print3(int (*a)[3]) { /*...*/ }

inside the function print3 a is a pointer to arrays of 3 ints

int a[] = {1, 2, 3, 4, 5, 6};

inside the function main a is an array of 6 ints.
In most contexts (including function call context) a is converted to a pointer to the first element: a value of type "pointer to int".

The types "pointer to int", "pointer to array of 2/3 ints" are not compatible, so calling any of the functions with print2(a) (or print3(a) ) forces a diagnostic from the compiler.

But you use a cast to tell the compiler: "do not issue any diagnostic. I know what I'm doing"

   print3(a); // type of a (after conversion) and type of argument of print3 are not compatible
// print3((cast)a); // I know what I'm doing
   print3((int (*)[3])a); // change type of a to match argument even if it does not make sense

It would be much easier to understand if you break it down and understand things. What if you were to pass the whole array to say a function print4 , which iterates over the array and prints the elements? How would you pass the array to such a function.

You can write it something like

print4( (int *) a); 

which can be simplified and just written as print4(a);

Now in your case by doing print2((int (*)[2]) a); , you are actually designing a pointer to an array of 2 int elements. So now the a is pointer in array of two elements ie every increment to the pointer will increase the offset by 2 int s in the array a

Imagine with the above modeling done, your original array becomes a two dimensional array of 3 rows with 2 elements each. That's how your print2() element iterates over the array a and prints the int s. Imagine a function print2a that works by taking a local pointer to a and increments at each iteration to the point to the next two elements

void print2a(int (* a)[2]) {
    int (* tmp)[2] = a;
    for( int i = 0; i < 3; i++ ) {
        printf("%d%d\n", tmp[0][0], tmp[0][1] );
        tmp++;
    }
}

The same applies to print3() in which you pass an pointer to array of 3 int s, which is now modeled as a 2-D array of 2 rows with 3 elements in it.

TL;DR

This code contains incorrect and meaningless hacks. There is not much of value to learn from this code.

Detailed explanation follows.


First of all, this is a plain 1D array that gets printed in different ways.

These lines are strictly speaking bugs:

print2((int (*)[2]) a);
print3((int (*)[3]) a);

In both cases there is an invalid pointer conversion, because a is of type int[6] and a pointer to the array a would have to be int (*)[6] . But the print statements are wrong in another way too, a when used in an expression like this "decays" into a pointer to the first element. So the code is casting from int* to int(*)[2] etc, which is invalid.

These bugs can in theory cause things like misaligned access, trap representations or code getting optimized away. In practice it will very likely "work" on all mainstream computers, even though the code is relying on undefined behavior.


If we ignore that part and assume void print2(int (*a)[2]) gets a valid parameter, then a is a pointer to an array of type int[2] .

a[i] is pointer arithmetic on such a type, meaning that each i would correspond to an int[2] and if we had written a++ , the pointer would jump forward sizeof(int[2]) in memory (likely 8 bytes).

Therefore the function abuses this pointer arithmetic on a[i] to get array number i , then do [j] on that array to get the item in that array.


If you actually had a 2D array to begin with, then it could make sense to declare the functions as:

void print (size_t x, size_t y, int (*a)[x][y])

Though this would be annoying since we would have to access the array as (*a)[i][j] . Instead we can use a similar trick as in your code:

void print (size_t x, size_t y, int (*a)[x][y])
{
  int(*arr)[y] = a[0];
  ...
  arr[i][j] = whatever; // now this syntax is possible

This trick too uses pointer arithmetic on the array pointer arr , then de-references the array pointed at.

Related reading that explains these concepts with examples: Correctly allocating multi-dimensional arrays

The code seeks to reinterpret the array int a[6] as if it were int a[3][2] or int a[2][3] , that is, as if the array of six int in memory were three arrays of two int (in print2 ) or two arrays of three int (in print3 ).

While the C standard does not fully define the pointer conversions, this can be expected to work in common C implementations (largely because this sort of pointer conversion is used in existing software, which provides motivation for compilers to support it).

In (int (*)[2]) a , a serves as a pointer to its first element. 1 The cast converts this pointer to int to a pointer to an array of two int . This conversion is partially defined C 2018 6.3.2.3 7:

  • The behavior is undefined if the alignment of a is not suitable for the type int (*)[2] . However, compilers that have stricter alignment for arrays than for their element types are rare, and no practical compiler has a stricter alignment for an array of six int than it does for an array of two or three int , so this will not occur in practice.
  • When the resulting pointer is converted back to int * , it will compare equal to the original pointer.

The latter property tells us that the resulting pointer contains all the information of the original pointer, since it must contain the information needed to reconstruct the original pointer. It does not tell us that the resulting pointer is actually pointing to the memory where a is.

As noted above, common C implementations allow this. I know that Apple's versions of GCC and Clang support this reshaping of arrays, although I do not know whether this guarantee was added by Apple or is in the upstream versions.

Given that (int (*)[2]) is passed to print2 as its a , then a[i][j] refers to element j of array i . That is, a points to an array of two int , so a[0] is that array, a[1] is the array of two int that follows it in memory, and a[2] is the array of two int after that. Then a[i][j] is element j of the selected array. In effect, a[i][j] in print2 is a[i*2+j] in main .

Note that no aliasing rules are violated as no arrays are accessed by a[i][j] : a is a pointer, a[i] is an array but is not accessed (it is automatically converted to a pointer, per footnote 1 below), and a[i][j] has type int and accesses an object with effective type int , so C's aliasing rules in C 2018 6.5 7 are satisfied.

Footnotes

1 This is because when an array is used in an expression, it is automatically converted to a pointer to its first element, except when it is the operand of sizeof , is the operand of unary & , or is a string literal used to initialize an array.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM