Others have already answered most of the question, but I thought I would add some illustrations...
When you want an array-like object, i.e., a sequence of consecutive elements of a given type T
, you use a pointer to T
, T *
, but you want to point to objects of type T
, and that is what you must allocate memory for.
If you want to allocate 10 T
objects, you should use malloc(10 * sizeof(T))
. If you have a pointer to assign the array to, you can get the size from that
T * ptr = malloc(10 * sizeof *ptr);
Here *ptr
has type T
and so sizeof *ptr
is the same as sizeof(T)
, but this syntax is safer for reasons explained in other answers.
When you use
T * ptr = malloc(10 * sizeof(T *));
you do not get memory for 10 T
objects, but for 10 T *
objects. If sizeof(T*) >= sizeof(T)
you are fine, except that you are wasting some memory, but if sizeof(T*) < sizeof(T)
you have less memory than you need.

Whether you run into this problem or not depends on your objects and the system you are on. On my system, all pointers have the same size, 8 bytes, so it doesn't really matter if I allocate
char **string_array = malloc(sizeof(char **) * 10);
or
char **string_array = malloc(sizeof(char *) * 10);
or if I allocate
int **int_matrix = malloc(sizeof(int **) * 10);
or
int **int_matrix = malloc(sizeof(int *) * 10);
but it could be on other architectures.
For your third solution, you have a different problem. When you allocate
int **int_matrix2 = malloc(sizeof(int *));
you allocate space for a single int
pointer, but you immediately treat that memory as if you had 10
for (int i = 0; i < 10; i++)
{
int_matrix2[i] = malloc(sizeof(int));
}
You can safely assign to the first element, int_matrix2[0]
(but there is a problem with how you do it that I get to); the following 9 addresses you write to are not yours to modify.

The next issue is that once you have allocated the first dimension of your matrix, you have an array of pointers. Those pointers are not initialised, and presumably pointing at random places in memory.

That isn't a problem yet; it doesn't do any harm that these pointers are pointing into the void. You can just point them to somewhere else. This is what you do with your char **
array. You point the first pointer in the array to a string, and it is happy to point there instead.

Once you have pointed the arrays somewhere safe, you can access the memory there. But you cannot safely dereference the pointers when they are not initialised. That is what you try to do with your integer array. At int_matrix[0]
you have an uninitialised pointer. The type-system doesn't warn you about that, it can't, so you can easily compile code that modifies int_matrix[0][0]
, but if int_matrix[0]
is pointing into the void, int_matrix[0][0]
is not an address you can safely read or write. What happens if you try is undefined, but undefined is generally was way of saying that something bad will happen.

You can get what you want in several ways. The closest to what it looks like you are trying is to implement matrices as arrays of pointers to arrays of values.

There, you just have to remember to allocate the arrays for each row in your matrix as well.
#include <stdio.h>
#include <stdlib.h>
int **new_matrix(int n, int m)
{
int **matrix = malloc(n * sizeof *matrix);
for (int i = 0; i < n; i++)
{
matrix[i] = malloc(m * sizeof *matrix[i]);
}
return matrix;
}
void init_matrix(int n, int m, int **matrix)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
matrix[i][j] = 10 * i + j + 1;
}
}
}
void print_matrix(int n, int m, int **matrix)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
int main(void)
{
int n = 3, m = 5;
int **matrix = new_matrix(n, m);
init_matrix(n, m, matrix);
print_matrix(n, m, matrix);
return 0;
}
Here, each row can lie somewhere random in memory, but you can also put the row in contiguous memory, so you allocate all the memory in a single malloc
and compute indices to get at the two-dimensional matrix structure.

Row i
will start at offset i*m
into this flat array, and index matrix[i,j]
is at index matrix[i * m + j]
.
#include <stdio.h>
#include <stdlib.h>
int *new_matrix(int n, int m)
{
int *matrix = malloc(n * m * sizeof *matrix);
return matrix;
}
void init_matrix(int n, int m, int *matrix)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
matrix[m * i + j] = 10 * i + j + 1;
}
}
}
void print_matrix(int n, int m, int *matrix)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
printf("%d ", matrix[m * i + j]);
}
printf("\n");
}
}
int main(void)
{
int n = 3, m = 5;
int *matrix = new_matrix(n, m);
init_matrix(n, m, matrix);
print_matrix(n, m, matrix);
return 0;
}
With the exact same memory layout, you can also use multidimensional arrays. If you declare a matrix as int matrix[n][m]
you will get what amounts to an array of length n
where the objects in the arrays are integer arrays of length m
, exactly as on the figure above.
If you just write that expression, you are putting the matrix on the stack (it has auto scope), but you can allocate such matrices as well if you use a pointer to int [m]
arrays.
#include <stdio.h>
#include <stdlib.h>
void *new_matrix(int n, int m)
{
int(*matrix)[n][m] = malloc(sizeof *matrix);
return matrix;
}
void init_matrix(int n, int m, int matrix[static n][m])
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
matrix[i][j] = 10 * i + j + 1;
}
}
}
void print_matrix(int n, int m, int matrix[static n][m])
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
int main(void)
{
int n = 3, m = 5;
int(*matrix)[m] = new_matrix(n, m);
init_matrix(n, m, matrix);
print_matrix(n, m, matrix);
int(*matrix2)[m] = new_matrix(2 * n, 3 * m);
init_matrix(2 * n, 3 * m, matrix2);
print_matrix(2 * n, 3 * m, matrix2);
return 0;
}
The new_matrix()
function returns a void *
because the return type cannot depend on the runtime arguments n
and m
, so I cannot return the right type.
Don't let the function types fool you, here. The functions that take a matrix[n][m]
argument do not check if the matrix has the right dimensions. You can get a little type checking with pointers to arrays, but pointer decay will generally limit the checking. The last solution is really only different syntax for the previous one, and the arguments n
and m
determines how the (flat) memory that matrix
points to is interpreted.