Search code examples
arrayscmultidimensional-arraysegmentation-faultdynamic-arrays

Seg faulting with 4D arrays & initializing dynamic arrays


I ran into a big of a problem with a tetris program I'm writing currently in C. I am trying to use a 4D multi-dimensional array e.g.

uint8_t shape[7][4][4][4]

but I keep getting seg faults when I try that, I've read around and it seems to be that I'm using up all the stack memory with this kind of array (all I'm doing is filling the array with 0s and 1s to depict a shape so I'm not inputting a ridiculously high number or something).

Here is a version of it (on pastebin because as you can imagine its very ugly and long). If I make the array smaller it seems to work but I'm trying to avoid a way around it as theoretically each "shape" represents a rotation as well. https://pastebin.com/57JVMN20

I've read that you should use dynamic arrays so they end up on the heap but then I run into the issue how someone would initialize a dynamic array in such a way as linked above. It seems like it would be a headache as I would have to go through loops and specifically handle each shape?

I would also be grateful for anybody to let me pick their brain on dynamic arrays how best to go about them and if it's even worth doing normal arrays at all.


Solution

  • Even though I have not understood why do you use 4D arrays to store shapes for a tetris game, and I agree with bolov's comment that such an array should not overflow the stack (7*4*4*4*1 = 448 bytes), so you should probably check other code you wrote.

    Now, to your question on how to manage 4D (N-Dimensional)dynamically sized arrays. You can do this in two ways:

    1. The first way consists in creating an array of (N-1)-Dimensional arrays. If N = 2 (a table) you end up with a "linearized" version of the table (a normal array) which dimension is equal to R * C where R is the number of rows and C the number of columns. Inductively speaking, you can do the very same thing for N-Dimensional arrays without too much effort. This method has some drawbacks though:

      • You need to know beforehand all the dimensions except one (the "latest") and all the dimensions are fixed. Back to the N = 2 example: if you use this method on a table of C columns and R rows, you can change the number of rows by allocating C * sizeof(<your_array_type>) more bytes at the end of the preallocated space, but not the number of columns (not without rebuilding the entire linearized array). Moreover, different rows must have the same number of columns C (you cannot have a 2D array that looks like a triangle when drawn on paper, just to get things clear).
      • You need to carefully manage the indicies: you cannot simply write my_array[row][column], instead you must access that array with my_array[row*C + column]. If N is not 2, then this formula gets... interesting
    2. You can use N-1 arrays of pointers. That's my favourite solution because it does not have any of the drawbacks from the previous solution, although you need to manage pointers to pointers to pointers to .... to pointers to a type (but that's what you do when you access my_array[7][4][4][4].

    Solution 1

    Let's say you want to build an N-Dimensional array in C using the first solution. You know the length of each dimension of the array up to the (N-1)-th (let's call them d_1, d_2, ..., d_(N-1)). We can build this inductively:

    • We know how to build a dynamic 1-dimensional array
    • Supposing we know how to build a (N-1)-dimensional array, we show that we can build a N-Dimensional array by putting each (N-1)-dimensional array we have available in a 1-Dimensional array, thus increasing the available dimensions by 1.

    Let's also assume that the data type that the arrays must hold is called T. Let's suppose we want to create an array with R (N-1)-dimensional arrays inside it. For that we need to know the size of each (N-1)-dimensional array, so we need to calculate it.

    • For N = 1 the size is just sizeof(T)
    • For N = 2 the size is d_1 * sizeof(T)
    • For N = 3 the size is d_2 * d_1 * sizeof(T)

    You can easily inductively prove that the number of bytes required to store R (N-1)-dimensional arrays is R*(d_1 * d_2 * ... * d_(n-1) * sizeof(T)). And that's done.

    Now, we need to access a random element inside this massive N-dimensional array. Let's say we want to access the item with indicies (i_1, i_2, ..., i_N). For this we are going to repeat the inductive reasoning:

    • For N = 1, the index of the i_1 element is just my_array[i_1]
    • For N = 2, the index of the (i_1, i_2) element can be calculated by thinking that each d_1 elements, a new array begins, so the element is my_array[i_1 * d_1 + i_2].
    • For N = 3, we can repeat the same process and end up having the element my_array[d_2 * ((i_1 * d_1) + i_2) + i_3]

    And so on.

    Solution 2

    The second solution wastes a bit more memory, but it's more straightforward, both to understand and to implement.

    Let's just stick to the N = 2 case so that we can think better. Imagine to have a table and to split it row by row and to place each row in its own memory slot. Now, a row is a 1-dimensional array, and to make a 2-dimensional array we only need to be able to have an ordered array with references to each row. Something like the following drawing shows (the last row is the R-th row):

    +------+
    |  R1 -------> [1,2,3,4]
    |------|
    |  R2 -------> [2,4,6,8]
    |------|
    |  R3 -------> [3,6,9,12]
    |------|
    | .... |
    |------|
    |  RR -------> [R, 2*R, 3*R, 4*R]
    +------+
    

    In order to do that, you need to first allocate the references array (R elements long) and then, iterate through this array and assign to each entry the pointer to a newly allocated memory area of size d_1.

    We can easily extend this for N dimensions. Simply build a R dimensional array and, for each entry in this array, allocate a new 1-Dimensional array of size d_(N-1) and do the same for the newly created array until you get to the array with size d_1.

    Notice how you can easily access each element by simply using the expression my_array[i_1][i_2][i_3]...[i_N].

    For example, let's suppose N = 3 and T is uint8_t and that d_1, d_2 and d_3 are known (and not uninitialized) in the following code:

    size_t d1 = 5, d2 = 7, d3 = 3;
    int ***my_array;
    
    my_array = malloc(d1 * sizeof(int**));
    for(size_t x = 0; x<d1; x++){
        my_array[x] = malloc(d2 * sizeof(int*));
        for (size_t y = 0; y < d2; y++){
             my_array[x][y] = malloc(d3 * sizeof(int));
        }
    }
    
    //Accessing a random element
    size_t x1 = 2, y1 = 6, z1 = 1;    
    my_array[x1][y1][z1] = 32;
    

    I hope this helps. Please feel free to comment if you have questions.