There are several ways to do this:
If you know how big the dimensions need to be at compile time, and you want the rows to be contiguous in memory, do the following:
#define ROWS 2
#define COLUMNS 5
...
int (*student)[COLUMNS] = malloc( sizeof *student * ROWS );
...
student[i][j] = some_value;
If you don't know how big the dimensions need to be at compile time, and you want the rows to be contiguous in memory, and your version of C supports variable-length array syntax (C99 or later):
size_t rows;
size_t columns;
// get rows and columns somehow
int (*student)[columns] = malloc( sizeof *student * rows );
...
student[i][j] = some_value;
Strictly speaking, this will invoke undefined behavior since sizeof on a VLA is evaluated at runtime, and student hasn't been initialized to point anywhere meaningful yet. However, I and others argue that this is a weakness in the wording of the language standard, and I've never had this not work as expected. But, be aware, this isn't guaranteed to work everywhere all the time.
If you don't know how big the dimensions need to be at compile time, and you need the rows to be contiguous in memory, and you don't have variable-length arrays available:
size_t rows;
size_t columns;
// get rows and columns;
int *student = malloc( sizeof *student * rows * columns );
Yeah, this is a 1D array; you'll have to map "rows" and "columns" onto this 1D structure:
student[i * ROWS + j] = some_value;
If you don't know how big your dimensions need to be at compile time, and you don't care whether rows are contiguous or not, you can do this:
size_t rows;
size_t columns;
// get rows and columns;
int **student = malloc( sizeof *student * rows );
if ( student )
{
for ( size_t i = 0; i < rows; i++ )
{
student[i] = malloc( sizeof *student[i] * columns );
}
}
...
student[i][j] = some_value;
For the first three methods, cleanup is easy:
free( student );
For the last method, you'll have to deallocate each "row" individually:
for ( size_t i = 0; i < rows; i++ )
free( student[i] );
free( student );