Skip to content
School of Physics
Home Our Teaching Resources C programming dynamic.html
Back to top

Scientific programming in C
Allocating space

Remember, as far as the computer is concerned a variable, structure or array is just a name for a location in memory.

So far we have used pointers to point to existing structures, arrays and, when using scanf(), variables. But this requires us to know the sizes of our arrays and the number of structures we will need when we write the program. If we write our word processor with enough structures to represent four open documents, what hapens when our user wants to edit five?

For this reason, it is also possible to dynamically allocate memory as we need it. This can then be used like any other memory such as that obtained by declaring structures arrays.

Dynamically allocated memory acts like anonymous (nameless) structures and arrays which we can create and destroy at will.

malloc()

The malloc() function allocates some memory for us to use. Let's first consider allocating memory to be used in the same way as a one-dimensional array:
#include <stdio.h>
#include <stdlib.h>

#define N 100
int
main() {
  float *p = NULL;

  p = malloc(N * sizeof *p);
  if (p == NULL) {
    fprintf(stderr, "Out of memory!\n");
    exit(1);
  }
  p[0] = p[1] = 3.14159;
  /* Use p for something */
  return 0;
}
Notice how we have initialised p to NULL so that if we try to use it then, on most systems, the program will crash immediately.
p above is an ordinary pointer, just like any other. As always, trying to use p without making sure it points to something would be a disaster. Once we have allocated some memory for it we can use a pointer just like an array.

The sizeof operator

For any pointer p the expression sizeof *p returns the number of bytes needed for whatever p points to. It's the ideal argument to malloc! There's also a version sizeof(float) which is less useful. To see why consider the following two statements:
  p = malloc(N * sizeof *p);  // Good
  p = malloc(N * sizeof(float));  // Bad

The first example is always going to allocate enough memory to store N of whatever type p points to. The second example relies on us having got the type of p correct. Every time we look at this code we will have to go back to the declaration of p to check it is a float. Even worse, if we were to change p to point to a double we would have to go through the entire code looking for calls to malloc().

  • If we assumed that p pointed to the space for N doubles but in fact it only pointed to enough space for N floats then that would be a very nasty bug.
  • Bugs split between two parts of the code, where each part looks correct when viewed on its own and its only when we compare the two that we find they are inconsistent can be quite hard to spot. So we avoid the chance of them happening.

Always use the sizeof *p form.

NULL

AS with fopen, NULL is used to indicate that malloc ran out of memory. We must always check that malloc did not return NULL. We have met the exit() function before, does pretty much what it says and by convention, successful programs exit with zero.
Reminder: NULL and exit()are declared in <stdio.h> and <stdlib.h> respectively.

You are welcome to check the result of malloc every time you call it. You are equally welcome to define a function like:

void *xmalloc(size_t n) {
  void *p = malloc(n);
  if (p == NULL) {
    fprintf(stderr, "Out of memory!\n");
    exit(1);
  }
  return p;
}
and to call xmalloc instead of malloc. I know which I prefer to do!

Pointers to pointers

Whilst conceptually simple, pointers to pointers can cause confusion. If you are a little less confident you may wish to treat this indented section as a bit of a recipe.

Arrays of pointers

These work in the obvious way:
#define N 100
#define M 4
int
main() {
  float *p[M];
  int i;

  for (i = 0; i < M; ++i)
    p[i] = xmalloc(N * sizeof *p[i]);

  p[0][1] = 3.14159;
  /* Use p for something */
  return 0;
}

Dynamically allocated '2-D arrays'

Of course, often we won't know how many pointers we will need so we can dynamically allocate them too. The following function allocates memory that can then be used like a two dimensional array (but see note below).
int **
new2dint(int m, int n) {
  int **p;
  int i;

  p = xmalloc(m * sizeof *p);
  for (i = 0; i < m; ++i)
    p[i] = xmalloc(n * sizeof *p[i]);
  return p;
}

int
main() {
  int **p = NULL;
  int n;

  do {
    printf("Size of array (>0)?\n");
    scanf("%i", &n);
  } while (n <= 0);

  p = new2dint(n, n);
  p[0][0] = 17;
  p[n-1][0] = 13;
  p[n-1][n-1] = 10;

  return(0);
}

Differences from true multi-dimensional arrays

Executive summary: the things above may look like ordinary two-dimensional arrays but they're not

At first sight p above looks just like a "real" array, in that their elements are accessed using the same source-level notation:

  int array[M][N];
  int **p = NULL;

  p = new2dint(M, N);

  p[1][7] = 17;
  array[1][7] = 17;
However, p and array are different things. p is a pointer to M pointers to N ints. By contrast, array points to a single block of M*N ints.

They are dereferenced as follows:


p[i][j] => *( *(p+i) + j)  
Double deference: deference (p+i), add j and dereference the result.
array[i][j] => *(array + (N*i + j))
Single deference: add N*i+j to array and dereference that.

This is why for functions which have arguments which are multidimensional arrays we must tell the function the size of the array (which the left-most size being optional):

int oldstylefun(float array[][N]);
int newstylefun(int n, float array[][n]);
int three_d_fun(int m, int n, float array[][m][n]);

Allocating structures

A major use of malloc() is to dynamically create structures. The mechanics work just as we would expect:

struct thingy *p = NULL;
p = xmalloc(sizeof *p);
In practice however it's a very good idea to define a function whose job is to allocate the space for a structure and do any initialisation:

Notice how str is a pointer not an array. In order to use it we must make it point at something.

#include <stdlib.h>

typedef struct wotsit {
  float val;
  char *str;
  int length;
} Wotsit;


Wotsit *newwotsit(int len) {
  Wotsit *w = xmalloc(sizeof *w);

  w->length = len;
  w->val = 0;
  w->str = xmalloc(w->length);
  return w;
}

int main() {
  Wotsit *william = newwotsit(100);
  /* Blah, blah, blah */
}

In practice structures are nearly always dynamically allocated, seldom statically.

Freeing memory

When we've finished with some memory we should get rid of it so it can be reused. If we fail to do this our program will gradually use more and more memory until the computer runs out. (When our program finishes it will automatically free any memory it was still using.)

The absolute rule is that we can't use memory after we've freed it and we can free it only once. To reuse the above example we might define a function freewotsit():

void freewotsit(Wotsit *w) {
  free(w->str);
  free(w);
}
The above version is correct. Had we written:
  free(w);
  free(w->str);

Pointers that point to something that no longer exists are referred to as dangling.

It would have been very, very wrong.

We would have the same problem if we tried to defererence w after we had freed the memory it pointed to or another pointer had the same value as w and we tried to derefence that. In practice it is only safe to free() some memory if we are careful to set any pointers that point to it to NULL.

"Reallocating" memory with realloc()

C's dynamic memory allocation has another useful trick: memory allocations can be "resized". Provided the value of p is something that has been returned by a previous called to malloc() (or realloc()) then we may write:

  p = alloc(oldsize * sizeof *p);
  ...

  p = realloc(p, newsize * sizeof *p); // This changes the value of p

This allocates the new amount of memory, copies the value of the old memory to the new one (up to the minimum of the old and new sizes), frees the old memory and returns a pointer to the new memory. Note that:

  • The old allocation does not get magically extended: it's just a convenient way of doing "allocate new", "copy old", "free old" all in one.
  • AS with free(), we could have serious problems if some other pointers pointed to the old (now freed) memory.
  • The first argument must be an actual value previously returned by malloc() or realloc(). It cannot point to somewhere "inside" some allocated memory only to the very start.
  • The new size may be larger or smaller than the old.

Example of a bug

  p = alloc(oldsize * sizeof *p);
  q = p;
  ...

  p = realloc(p, newsize * sizeof *p); // q is now dangling

Advantages and disadvantages of using malloc()

malloc() is quite slow

One not very obvious disadvantage of malloc() is that because it's so general it's also quite slow. This means that for, say, short arrays that will be used within a function (and the functions it calls) and then thrown away at the end of the function it is better to use automatic ("normal") arrays and structures if we can.

PoorBetter
#include <stdlib.h>

void poorfun(int n) {
  double *x;

  x = xmalloc(n * sizeof *x);
  // .. Do some stuff with x

  free(x);
}
 
void betterfun(int n) {
  double x[n];

  // .. Do some stuff with x
}
 

The first version has more ways to go wrong, we might forget to call free(), and is also slower. (However, until C99 programmers had no choice as array sizes had to be declared at compile time.) It's not unknown for the speed of some programs to be dominated by calls to malloc().

Optional background: automatic variables and the stack

Automatic variables have an important simplification compared to using malloc(): they are always deallocated in the reverse order to being allocated (albeit several may be deallocated at the same time); when their containing function returns, a principle known as last in, first out (LIFO).

Such collections are known as stacks as items can be thought of as being stacked on top of each other with items only ever been added to, and removed from, the top of the stack, never the middle.

This leads to a particularly simple implementation where automatic variables are stored sequentially starting from address zero (or in practice a slight offset to allow for program start up and static variables). The address of the top of the stack (the first byte not being used) is called the stack pointer. When a variable or array is allocated its address is just the current value of the stack pointer and the stack pointer is then incremented by the size of that variable or array.

Of course, if we are foolish enough to leave a pointer pointing to a piece of memory above the current value of the stack pointer we will be in serious trouble when that piece of memory gets reused by another function.

When a function calls another function, the current value of the stack pointer, within the calling function, is saved. Thus the second function's memory sits on top of the first one's. When the second function returns the calling function's saved stack pointer is restored and nothing needs to be done: the memory previously used by the called function is automatically now available for re-use.

Example

In the following example, we assume that static variables take up 200 bytes. main() is therefore called with an initial stack pointer value of 200 (Point 0 in the code). After it has allocated x (800 bytes) the stack pointer has the value 1000, which is the initial value of the stack pointer for function1().

function1() then allocates y (also 800 bytes), thus increasing the stack pointer to 1800 which is the initial value of the stack pointer for function2().

function2() then allocates 800 bytes for 9 (also ), increasing the stack pointer to 2600, its peak value for this program.

When function2() returns the stack pointer reverts to its previous value within function1(), 1800. function1() then returns so the stack pointer reverts to its value within main(), 1000.

Thus the stack grows (as functions are called) and shrinks as they return.

 

 

 

 

 

Program Stack pointer
int main() {
  /* Point 0 */
  double x[100]; 
  /* Point 1 */
  function1();
  /* Point 5 */
  return 0;
}

void function1() {
  double y[100]; 
  /* Point 2 */
  function2();
  /* Point 4 */
}

void function2() {
  double z[100]; 
  /* Point 3 */
}

Point 0 Point 1 Point 2 Point 3 Point 4 Point 5
2600

function2()
800 bytes

1800

function1()
800 bytes

1800

function1()
800 bytes

1800

function1()
800 bytes

1000

main()
800 bytes

1000

main()
800 bytes

1000

main()
800 bytes

1000

main()
800 bytes

1000

main()
800 bytes

200

static, etc

200

static, etc

200

static, etc

200

static, etc

200

static, etc

200

static, etc

Time

This stack mechanism is so useful that it is usually used to store the "function calling" information, including the saved value of the stack pointer. Thus on my system the difference in the values of the stack pointer between Point 0 and Point 1 is 816 bytes, not 800 which probably corresponds to the saved values of the stack pointer and the instruction pointer (the location of the instruction to be executed after the called function returns). And the stack grows downwards from 140736935706880!

malloc() and the heap

This simplification isn't available to malloc() and free() as we are able to free memory in any order we like. The memory managed by malloc() and free() therefore has holes it it where previously-used memory has been freed and is now available for re-use. This is usually referred to as the heap. Thus malloc() has to search for a free chunk of memory of the right size rather than just expanding the heap every time it is called.

Advantages of malloc()

The obvious advantage of malloc() is that its memory is valid after the function returns. Another, much less obvious one, is that many systems have limits on how large the stack can grow which are much larger than the heap. This may mean that it's wise to obtain very large items via malloc() even when they could have been allocated on the stack.

Summary

  • Dynamic allocation allows us to change the number of "things" we have as the program runs.
  • We must free memory when we are finished and not try to use it after we have freed it.
  • Pointers to pointers can be confusing and are not the same as two-dimensional arrays.
  • malloc() can be slow.
                                                                                                                                                                                                                                                                       

Validate   Link-check © Copyright & disclaimer Share
Back to top