1

I am trying to return a modern 2D dynamic array after allocating memory to it and filling it up with character values. I tried working around some errors but ultimately got stuck at a dead end. I originally had a working 2D dynamic array implementation return in functions using array-of-pointers method from C90 but I am told that has overhead and is out-of-date. So, I decided to try and upgrade using modern C 2D array approach taken from here: How do I work with dynamic multi-dimensional arrays in C?.

Alas, it ended up not working in my implementation so I am now wondering what I may have done wrong. Please see my below code for full reference: (Goal was to make a 2D matrix of a checkerboard within helper functions to main)

char **allocateToBoardC99(const int nthDim)
{
    char(*boardArray)[nthDim] = malloc(sizeof(char[nthDim][nthDim]));
    return (char **)boardArray;
}
char **getCheckeredBoardC99(const int nthDim)
{
    bool startWithWhiteTile = true;

    // C99 version (more elegant):
    char **arr = allocateToBoardC99(nthDim);

    // Make the board checkered by alternating char assignments based on a bool latch
    for (int rowIndex = 0; rowIndex < nthDim; rowIndex++)
    {
        if (startWithWhiteTile)
        {
            for (int columnIndex = 0; columnIndex < nthDim; columnIndex++)
            {
                arr[rowIndex][columnIndex] = (columnIndex % 2 == 0) ? 'W' : 'B';
            }
            startWithWhiteTile = false;
        }
        else
        {
            for (int columnIndex = 0; columnIndex < nthDim; columnIndex++)
            {
                arr[rowIndex][columnIndex] = (columnIndex % 2 == 0) ? 'B' : 'W';
            }
            startWithWhiteTile = true;
        }
    }
    return (char **)arr;
}
int main(int argc, char *argv[])
{
    // Initialize dimension of the checker board:
    int dim = 8;

    // char **boardMatrix = getCheckeredBoardC90(dim); // Works
    char **boardMatrix = getCheckeredBoardC99(dim);

    printf("hello world");
    printf("\n");

    for (int row = 0; row < dim; row++)
    {
        printf("[");
        for (int column = 0; column < dim; column++)
        {
            printf("%c ", boardMatrix[row][column]);
        }
        printf("]\n");
    }

    return 0;
}

If you're curious about the above getCheckeredBoardC90(dim); working, here is the following sequence of code that made it work (while the newer C99 doesn't):

char **allocateToBoardC90(const int nthDim)
{
    // The calloc() function reserves storage space for an array of NUM elements, each of length SIZE bytes:
    // calloc(size_t NUM, size_t SIZE)
    char *values = calloc(nthDim * nthDim, sizeof(char)); // In our 2D board array, we would have nxn (char)elements (8x8=64 elements)

    // Simple way is to allocate a memory block of size row and access its elements using simple pointer arithmetic:
    char **rows = malloc(nthDim * sizeof(char)); // pointer to overall space

    // Iterate through each *row; 0 to nthRow
    for (int rowIndex = 0; rowIndex < nthDim; rowIndex++)
    {
        rows[rowIndex] = values + (rowIndex * nthDim);
    }
    // Returns an array of pointers (array of arrays)
    return rows;
}
char **getCheckeredBoardC90(const int nthDim)
{
    bool startWithWhiteTile = true;

    //**array[rowIndex][columnIndex]
    // char **arr[nthDim][nthDim];

    // C90 version (lotta pointers; be aware of performance overhead):
    char **arr = allocateToBoardC90(nthDim);

    // Make the board checkered by alternating char assignments based on a bool latch
    for (int rowIndex = 0; rowIndex < nthDim; rowIndex++)
    {
        if (startWithWhiteTile)
        {
            for (int columnIndex = 0; columnIndex < nthDim; columnIndex++)
            {
                arr[rowIndex][columnIndex] = (columnIndex % 2 == 0) ? 'W' : 'B';
            }
            startWithWhiteTile = false;
        }
        else
        {
            for (int columnIndex = 0; columnIndex < nthDim; columnIndex++)
            {
                arr[rowIndex][columnIndex] = (columnIndex % 2 == 0) ? 'B' : 'W';
            }
            startWithWhiteTile = true;
        }
    }
    return arr;
}
int main(int argc, char *argv[])
{
    // Initialize dimension of the checker board:
    int dim = 8;

    char **boardMatrix = getCheckeredBoardC90(dim); // Works
    // char **boardMatrix = getCheckeredBoardC99(dim);

    printf("hello world");
    printf("\n");

    for (int row = 0; row < dim; row++)
    {
        printf("[");
        for (int column = 0; column < dim; column++)
        {
            printf("%c ", boardMatrix[row][column]);
        }
        printf("]\n");
    }

    return 0;
}

If the helper functions return correctly, then main's output should look something like this:

hello world
[W B W B W B W B ]
[B W B W B W B W ]
[W B W B W B W B ]
[B W B W B W B W ]
[W B W B W B W B ]
[B W B W B W B W ]
[W B W B W B W B ]
[B W B W B W B W ]

Specific help on this would be greatly appreciated, thank you!

NOTE: Originally with this dynamic 2D array modern C code, I was trying to return the 2D array from an allocator function (really a reference to it), pass that 2D array's reference to an element-initializer function to fill up the checkerboard like usual, and then finally pass the finalized 2D array (as a reference) to the main function to check the contents of the 2D array through terminal output.

What ended up happening was a lot of trial and error to resolve pointer-to-object issues, pointer casting, etc. This only led (from what I believe is improper C-pointer casting) to an end-game "Segmentation Fault" in the getCheckeredBoardC99 function.

  • 4
    You don't have a char ** (and don't want one) so casting to one & returning it is a mistake. – Avi Berger May 17 '23 at 06:23
  • 1
    You're needlessly circumventing the type system, and as a result you're masking your own bugs. You do not need pointer casts in this code. So (1) Get rid of the casts (2) Fix your type errors and warnings (3) *Then* see if you still have a problem. Posting for help before fixing your type errors and warnings is premature. As it is now, you have incompatible pointer types, so of course your code doesn't work. You're missing pointers-to-pointers and pointers-to-arrays, and when the compiler complained, you added type casts to mask it. Rip off the bandaid and instead treat the wound. – Tom Karzes May 17 '23 at 06:31
  • You might start by thinking about and then improving on: `void allocateToBoardC99(const int nthDim, char (*boardArray)[nthDim]) { boardArray = malloc(nthDim * nthDim); } ` I think that needs work, but I have to sleep, so I'll leave it there. – Avi Berger May 17 '23 at 06:33
  • 2
    @AviBerger No that won't work since `boardArray` is a local pointer in the function. You have to pass a pointer by reference, one more level of indirection. – Lundin May 17 '23 at 07:00
  • 1
    As for the cast, my rule of thumb is that unless you are a C veteran (>5 years of full-time C programming experience), you should never use a cast in _any_ circumstance. Since at least 90% of the use of casts we see here on SO is plain wrong. – Lundin May 17 '23 at 07:05
  • @Lundin, yes, realized I messed that up after I was in bed. – Avi Berger May 17 '23 at 15:23
  • @TomKarzes The type errors are the problem I am struggling with and the casts were a desperate attempt to fix the wound. I did post in my original problem that I admitted to possible misuse of casting. Nonetheless, I will take your advice and show the original wound next time, thank you. – Brendan Sting May 18 '23 at 00:57

3 Answers3

3

A char** can only be used to point at the first item in an array of char*. Except that a pointer look-up table can be used for 2D array-like syntax, char** has literally nothing to do with 2D arrays what-so-ever.

Consider returning the array through a parameter instead:

void allocateToBoardC99 (size_t n, char(**boardArray)[n] )
{
  *boardArray = malloc(sizeof(char[n][n]));
}

int main (void)
{
  size_t n=5;
  char (*arr)[n];
  allocateToBoardC99(5, &arr);
}
Lundin
  • 195,001
  • 40
  • 254
  • 396
1

The type char** is not a 2D array so you cannot return 2d array this way. Due to some peculiarities of Variably Modified Types (i.e. VLA and pointers to VLA) they cannot be declared at file scope. So functions cannot return VMT because their return type is visible at file scope. In a case of 2D array there is a workaround in form of returning a pointer to incompleted array type e.g. int (*)[]. This pointer cannot be dereferenced because it points to incompleted type so one must assign it to a complete and compatible array type before use.


// function returning a pointer to an array

char (*allocateToBoardC99(const int nthDim))[]
{
    char(*boardArray)[nthDim] = malloc(sizeof(char[nthDim][nthDim]));
    return boardArray;
}

... usage ...

char (*arr)[n] = allocateToBoardC99(n);

Returning a complex type from a function requires a bit clunky syntax but it can be made a lot prettier with popular typeof extension (a feature in C23).


typeof(char[]) * allocateToBoardC99(const int nthDim);

tstanisl
  • 13,520
  • 2
  • 25
  • 40
  • Returning a pointer to an incomplete array means you lose the type safety advantage of passing the dimensions as parameters. In theory the compiler should be able to warn in case something like `static void func (int x, int y, int arr[x][y])` is used but a different sized array is passed. At least some static analyzers have that ability. – Lundin May 17 '23 at 06:51
  • Nice trick with `typeof` though! So much better than hiding the array pointer behind a typedef. – Lundin May 17 '23 at 06:53
  • @Lundin, yes. Probably the best way would be wrapping dimensions and a pointer to a structure, return it by value. Like `struct s { int rows, cols; char (*data)[]; }`. – tstanisl May 17 '23 at 07:06
  • Or return it through a parameter, in which case you preserve the "strong typing" of pointers to array. As of today I don't think it matters unless you actually have a habit of using external static analysers, since the mainstream compilers don't do much in the way of error checking here. – Lundin May 17 '23 at 07:52
0

Taking from @Lundin's answers and switching approaches to the array parameter approach (and its pros), I reached the following code solution to get the desired output in modern C:

1.) First, changing my allocation helper function to the pass by parameter structure:

void allocateToBoardC99(size_t nthDim, char (**boardArray)[nthDim])
{
    *boardArray = malloc(sizeof(char[nthDim][nthDim]));
}

2.) Next, initiating the allocating in the main code before filling up the board (declare the array here and then proceed to pass the buck):

int main(int argc, char *argv[])
{
    // Initialize dimension of the checker board:
    size_t dim = 8;

    // char **boardMatrix = getCheckeredBoardC90(dim); // Works
    char(*boardMatrix)[dim];
    allocateToBoardC99(dim, &boardMatrix);

    // NOTE: rest of the main code hidden for now...
}

3.) Now we are ready to start filling up the checkered board with values. Pass the board matrix as a parameter just like before to reference destination for the subscript assignments. Also, change the return type to void since it's just performing element assignment operations:

void getCheckeredBoardC99(size_t nthDim, char (*boardArray)[nthDim])
{
    bool startWithWhiteTile = true;

    // C99 version (more elegant):

    // Make the board checkered by alternating char assignments based on a bool latch
    for (int rowIndex = 0; rowIndex < nthDim; rowIndex++)
    {
        if (startWithWhiteTile)
        {
            for (int columnIndex = 0; columnIndex < nthDim; columnIndex++)
            {
                boardArray[rowIndex][columnIndex] = (columnIndex % 2 == 0) ? 'W' : 'B';
            }
            startWithWhiteTile = false;
        }
        else
        {
            for (int columnIndex = 0; columnIndex < nthDim; columnIndex++)
            {
                boardArray[rowIndex][columnIndex] = (columnIndex % 2 == 0) ? 'B' : 'W';
            }
            startWithWhiteTile = true;
        }
    }
}

Note that in step 3, we only pass the declared incomplete array type pointer(credit to @tstanisl for this type's explanation) and not the double pointer version in allocateToBoardC99. This is because getCheckeredBoardC99 isn't trying to refer to a block (like malloc), but rather to single elements in the board. I am trying to word this carefully as I have recognized and would like to echo @Lundin and @tstanisl that double pointer types like char** does NOT equal/mean 2D array.

4.) Call the element assignment function after the allocation function in the main code to complete this mini-process, as well as also printing the modern 2D board array's elements to verify it matches the desired output:

int main(int argc, char *argv[])
{
    // Initialize dimension of the checker board:
    size_t dim = 8;

    // char **boardMatrix = getCheckeredBoardC90(dim); // Works
    char(*boardMatrix)[dim];
    allocateToBoardC99(dim, &boardMatrix);

    // --SEE REST OF CODE BELOW THAT COMPLETES THE BOARD PROCESS--

    getCheckeredBoardC99(dim, boardMatrix);

    printf("hello world");
    printf("\n");

    for (int row = 0; row < dim; row++)
    {
        printf("[");
        for (int column = 0; column < dim; column++)
        {
            printf("%c ", boardMatrix[row][column]);
        }
        printf("]\n");
    }

    return 0;
}

After debugging, I got the following correct output:

(a 2D representation of the board with alternating tiles; B = Black, W = White)

PS Drive:\path> '...'
hello world
[W B W B W B W B ]
[B W B W B W B W ]
[W B W B W B W B ]
[B W B W B W B W ]
[W B W B W B W B ]
[B W B W B W B W ]
[W B W B W B W B ]
[B W B W B W B W ]

If there's anything I missed or didn't explain correctly, feel free to comment (that is, if this question thread isn't locked down; I am new to posting on Stack Overflow so bear with me).

Thank you to everyone who helped me reach a solution to my initial problem as well as also pointing out my C programming misconceptions so I don't get myself into a similar pitfall again.