Search code examples
cpointerspass-by-referenceindirection

Quadruple pointer and memcpy() in C


First of all, I know triple and quadruple pointers are bad practice and are ugly, that's not the point of this question, I'm trying to understand how they work. I'm aware using a struct would be much better.

I am trying to write a function that does some memory operations using memmove() and memcpy() on triple and double pointers that are passed-by-reference (or the C version of that). My memmove() works fine, but the memcpy() yields a SIGSEGV. Here's a minimal example

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

#define UNDO_DEPTH 25


void boardSave(int ***board, int game_sz, int ****history) {
    // Shift history to the right
    memmove(*history + 1, *history, (UNDO_DEPTH - 1) * sizeof(**history));
    // Copy board into history
    for (int row = 0; row < game_sz; ++row) {
        memcpy((*history)[0][row], (*board)[row], game_sz * sizeof((**board)[row]));
    }
}

int main(){
    // Game
    int game_sz = 5;
    // Allocate array for the board
    int **board = calloc(game_sz, sizeof(int *));
    for (int i = 0; i < game_sz; ++i) board[i] = calloc(game_sz, sizeof(int));
    // Allocate array for the history
    int ***history = calloc(UNDO_DEPTH, sizeof(int **));
    for (int i = 0; i < UNDO_DEPTH; ++i) {
        history[i] = calloc(game_sz, sizeof(int *));
        for (int j = 0; j < game_sz; ++j) {
            history[i][j] = calloc(game_sz, sizeof(int));
        }
    }
    board[0][0] = 1;
    boardSave(&board, game_sz, &history);
}

The objective of boardSave() here is to copy board onto history[0]. What am I doing wrong? Why is this causing a segmentation fault?


Solution

  • In the main function you make history point to an array of UNDO_DEPTH pointers, each of which points to a board that has its own allocation. Since memmove moves a contiguous memory blocks, you cannot move the content of all those boards with memmove.

    However, you could move down the pointers in that history array, leaving the board allocations untouched.

    Just doing a single memmove would require you to free memory of the last board shuffled off, and allocate memory for the new board. But you could recycle that memory by moving the last pointer to the start instead.

    Now, there is no need to pass the addresses of board and history to the boardSave function. It just makes your code more complicated for no reason. The simpler version would be:

    void boardSave(int **board, int game_sz, int ***history)
    {
    // Save the last board
        int ** last_board = history[UNDO_DEPTH - 1];
    
    // Shuffle down all the boards
        memmove( &history[1], &history[0], (UNDO_DEPTH - 1) * sizeof history[0] );
    
    // Put the old last board on the front
        history[0] = last_board;
    
    // Copy board into front of history
        copy_board( game_sz, history[0], board );
    }
    
    // Put a prototype for this earlier in the code. I think it makes
    // the boardSave function clearer to use a separate function for this
    // operation, which you might end up using on its own anyway.
    //
    void copy_board( int game_sz, int **dest, int **src )
    {
        for(int row = 0; row < game_sz; ++row)
            memcpy(dest[row], src[row], game_sz * sizeof dest[0][0]);
    }
    

    Personally I'd prefer to avoid memcpy in the last function and just write a simple loop that is obviously correct. The compiler will optimize it to use memcpy anyway, but without the possibility of making an error in the memcpy parameters:

        for(int row = 0; row < game_sz; ++row)
            for (int col = 0; col < game_sz; ++col)
                dest[row][col] = src[row][col];
    

    Similar comments would apply to the use of memmove actually.

    I would also make some use of const in the function signatures, so that a compiler error is generated if I accidentally switched the "dest" and "src" arguments. But I left that out at this stage for simplicitly.

    In main the call would now be:

    boardSave(board, game_sz, history);
    

    If you reeeeealy want to pass pointers for practice then I would "de-point" them at the start of the function:

    void complicated_boardSave(int ***p_board, int game_sz, int ****p_history)
    {  
        int *** history = *p_history;
        int  ** board = *p_board;
    
        // rest of code the same