So my goal is to malloc a maze struct that contains a 2D array; however, when I try to allocate memory for each "cell" of the 2D array I can't seem to free it properly afterwards. Is there a way that I could malloc the struct in one line or at least in a way that I can easily free the allocated memory with the free_maze function? I've attached my .c file along with the header file that defines the struct. Additionally, I have attached an example of a maze that is contained within a text file.
#include <stdlib.h>
#include "maze.h"
Maze* malloc_maze(int num_rows, int num_cols){
  Maze* maze = malloc(sizeof(*maze));
  if (maze == NULL){
    free(maze);
    return NULL;
  }
  maze -> cells = malloc(sizeof(maze -> cells)*(num_cols));
  if (maze -> cells == NULL){
    free(maze);
    return NULL;
  }
  for(int i = 0; i < num_cols; i++){
    maze -> cells[i] = malloc(sizeof(*(maze -> cells))*(num_rows));
  }
  maze -> num_rows = num_rows;
  maze -> num_cols = num_cols;
  return maze;
}
void free_maze(Maze* maze){
  free(maze);
}
Maze* read_maze(FILE* fp){
  Maze* maze;
  char c = fgetc(fp);
  int rows = 0;
  int cols = 0;
  int chars = 0;
  while(c != EOF){
    chars++;
    c = fgetc(fp);
  }
  rewind(fp);
  while(c != '\n'){
    cols++;
    c = fgetc(fp);
  }
  rows = chars / cols;
  cols--;
  maze = malloc_maze(rows, cols);
  rewind(fp);
  for(int row_count =0; row_count <= rows; row_count++){
    for(int col_count = 0; col_count < cols; col_count++){
      fseek(fp, (row_count*(cols+1)+col_count), SEEK_SET);
      maze -> cells[col_count][row_count] = fgetc(fp);
    }
  }
  maze -> num_rows = rows;
  maze -> num_cols = cols;
  return maze;
}
bool write_maze(const char* filename, const Maze* maze){
  FILE* ha;
  ha = fopen(filename, "w");
  if(ha == NULL){
    return false;
  }
  rewind(ha);
  int rows = maze -> num_rows;
  int cols = maze -> num_cols;
  for(int i = 0; i < rows; i++){
    for(int j = 0; j < cols; j++){
    fputc(maze -> cells[j][i], ha);
    }
    fputc('\n', ha);
  }
  fclose(ha);
  return true;
}
/////////////////header file//////////////////////////
#ifndef MAZE_H
#define MAZE_H 
#define WALL 'X'
#define PATH ' '
#include <stdio.h>
#include <stdbool.h>
typedef struct _Maze {
   int num_rows; 
   int num_cols; 
   char** cells; 
} Maze;
Maze* malloc_maze(int num_rows, int num_cols);
void free_maze(Maze* maze){
        __attribute__((nonnull));
}
Maze* read_maze(FILE* fp){
        __attribute__((nonnull));
}
bool write_maze(const char* filename, const Maze* maze){
        __attribute__((nonnull));
}
///////////////example maze within .txt file/////////////////////
XXXXX XXX
X       X
X XXX XXX
X X X   X
X X XXXXX
X       X
XXXXX XXX
 
     
     
     
    