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.