Here are two different methods of allocating and freeing a dynamically allocated 2D array.
Code:
#include <stdio.h>
#include <stdlib.h>

int **make2D ( size_t rows, size_t cols ) {
    size_t  r;
    int   **result = malloc( rows * sizeof *result );
    for ( r = 0 ; r < rows ; r++ ) {
        result[r] = malloc( cols * sizeof *result[r] );
    }
    return result;
}
void free2D ( int **arr, size_t rows ) {
    size_t  r;
    for ( r = 0 ; r < rows ; r++ ) {
        free( arr[r] );
    }
    free( arr );
}

int **makeAnother2D ( size_t rows, size_t cols ) {
    size_t  r;
    int    *rp;
    int   **result = malloc( rows * sizeof *result );
    result[0] = malloc( rows * cols * sizeof *result[0] );
    for ( r = 0, rp = result[0] ; r < rows ; r++, rp += cols ) {
        result[r] = rp;
    }
    return result;
}
void freeAnother2D ( int **arr ) {
    free( arr[0] );
    free( arr );
}

int main ( ) {
    int **one = make2D( 10, 64 );
    int **two = makeAnother2D ( 10, 64 );
    int r;

    printf( "Number of bytes per row = %ld\n", 64 * sizeof(int) );
    printf( "Start of each row of one\n" );
    for ( r = 0 ; r < 10 ; r++ ) {
        printf( "%p\n", (void*)one[r] );
    }
    printf( "Start of each row of two (these are contiguous addresses)\n" );
    for ( r = 0 ; r < 10 ; r++ ) {
        printf( "%p\n", (void*)two[r] );
    }

    free2D( one, 10 );
    freeAnother2D( two );

    return 0;
}
Advantages of the 2nd method include:
- it takes only 2 mallocs to allocate the whole thing.
- the actual data is contiguous, as it would be for a true 2D array.
- you don't need the number of rows in order to free it at the end.