Scientific programming in C
Allocating space
Comments and questions to John Rowe.
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.
| Poor | Better
|
|---|
#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.