Introduction
By now we have seen quite a lot of C, enough to write some useful programs. However, up until now, we have seen only two mechanisms for allocating storage, both handled by the compiler: static allocation of global variables (and globally allocated variables of local scope), and automatic (stack-based) variables allocated at entry to a program function/block. In both instances, the size of each allocated data element was fixed at compile time.There are times when this is not good enough. Our matrix assignment is a good case in point: this problem requires you to guess the size of the largest matrix anyone would want to give to your program and allocate space for it. If the matrix is smaller than this, you waste space. If the actual matrix is larger, then your user is out of luck even though there is enough memory available and your program code could handle their data.
It would be nice if you could ask for the space you need at runtime, and you can! It's not very difficult, but C exposes some details and provides no garbage collection — i. e., you must manually allocate storage of the appropriate size, maintain pointers into dynamically-allocated memory yourself, and you must free storage manually when you are done with it (and then you must not attempt to access the storage after you've freed it).
Managing storage manually is very error prone, but it also poses significant challenges for creating abstractions, because storage management issues must be part of an interface. Will the abstraction allocate storage, or must the client allocate and pass in the necessary structures? Is there some storage that persists across calls? Who will free the storage and how? This last question is difficult, because typically the client does not know the details of the structure in question, and therefore cannot perform the necessary traversals. On the other hand, the abstraction exists to provide useful values to the client, and so the implementation cannot know when a value is no longer required.
Allocating Memory
The C runtime environment provides support for explicit
requests for blocks of memory. The library routines for doing
this are malloc()
and calloc()
. You
say how many bytes you need (and here is where the
sizeof
operator come in really handy), and you
either get a pointer to that much memory or
NULL
if there wasn't enough memory available. The
two calls differ only in how you specify the number of bytes to
allocate.
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
size_t
is a system shorthand for an integer type
long enough to hold the system-dependent maximum size of data
structures. malloc()
takes a number of bytes;
calloc()
takes a block size and the number of
blocks you want of that size, i.e., for allocating arrays of
fixed-sized elements. The contents of memory returned by
malloc()
is unspecified; calloc()
initializes the returned memory block to all zeros.
The realloc()
function is used for changing the
size of a dynamically allocated block of data.
void *realloc(void *ptr, size_t size);
The size can increase or
decrease. If the size is increased, it may be necessary to move
the data, in which case new space is allocated, the bytes are
copied from the old data block (the contents of the newly allocated
space are unspecified), and the old memory block is freed. On
successful completion, a pointer to the (potentially different)
memory object is returned. In the case of an error,
realloc()
returns NULL
.
Note the use of void *
here. These functions
are not void
functions: they return a value,
a pointer to void
. C guarantees that all pointer
types can be cast to and from the generic pointer type
void *
without loss of information. Generic
pointers are used when you want to say that there is a pointer
to something but you don't know what. The expectation is
that a void *
will be cast to the required type,
and it is an error to dereference a void *
.
These routines work by keeping a pool of user memory, called the heap and giving out pointers to blocks within the pool. The heap is allocated and grown via calls to the operating system. The library routines keep track of what space within the pool has been allocated for use by the user's program and which is free. Caution: The actual memory associated with an allocation request is typically larger than the requested size. The memory allocater has to guarantee that any data stored in a block will be correctly aligned; but there is also a requirement for certain bookkeeping information. For example, the allocator usually stores the size of a block and some pointers associated with the management of the memory pool in dynamically allocated memory. This information may be stored before and/or after the requested memory. Overwriting this information as a result of incorrect pointer arithmetic or array subscripting can result in horrible bugs that are incredibly difficult to find!
Heap allocated memory is said to be anonymous because there is no variable name for the memory; it can only be accessed via anonymous addresses.
Freeing Memory
When you have finished using memory, you must explicitly
return it to the pool using the free()
function.
The C runtime code does not do garbage collection, so memory
to which you lose a reference before calling free()
is just lost. Situations like this are called memory
leaks and cause nightmares for all C developers. Just hunt
on the web for tools to find memory leaks in C programs, and
you'll see!
void free(void *ptr);
Establishing conventions for allocation and freeing of memory
is a normal part of C software engineering. If a program runs
only for a limited time, worrying about memory leaks may not be
worth the effort. But if you do need to worry about it, a handy
rule of thumb is to write a corresponding free()
whenever you use malloc()
(or its cousins).
It is an error to refer to a pointer after freeing the space
it points to, but you should not assume that an implementation
will detect the error. You may get a segmentation fault, or you
may just get garbage. That is why it is usually good practice
to set a pointer variable to NULL
after you free
it. Freeing data the program still needs is the dual to a
memory leak: it leads to memory corruption.
Examples
Allocating Space for a String
Suppose you are writing a function that is given a string
representing the current path name (no final /
)
called path
. You have read a directory entry
pointed to by direntp
(using
readdir()
), and you want to save away a string
containing the full pathname of the file. You can't know at
compile time how long the strings are. You can allocate space
for the combined string dynamically and then give it a value
like this:
char *fullname;
int size;
...
size = strlen(path) + strlen(direntp->d_name) + 1;
fullname = (char *) malloc(size);
if (fullname == NULL) exit(-1); /* no memory */
strcpy(fullname, path);
strcat(fullname, direntp->d_name);
The call to malloc()
includes enough bytes for all
the non-NUL
characters of path
plus
all the non-NUL
characters of the name of the
directory entry plus a byte for a terminating NUL
character. (Notice NUL
and NULL
are
not the same.)
When you are done with this string (perhaps you are going to process another directory and you will not need to come back to this one again), you will execute:
free(fullname);
This will return these bytes of memory to the heap for future reuse.
Linked Lists
The code inlinked_list.c
implements
Lisp-style lists with the important addition of a mechanism for
destroying a list when it is no longer required. The interface
is in linked_list.h
.
C's type system does not allow for a truly clean generic list package, but don't think people haven't tried. While not type safe, there are a variety of techniques for separating the list capabilities of a structure from the data the list carries.
The Process Address Space
Recall that every process has its own virtual address space. While Unix/Linux present the model of a flat address space, in fact, this space has a structure. In reality, at any given time, the virtual memory system of the OS kernel has only allocated memory for certain address ranges called memory areas.Every process has a text section, which is the memory mapped version of the program code in the executable file. Furthermore, every process has a data section for its initialized global (and static) variables. Unintialized globals are placed in a bss section which has a zero filled page mapped over it (bss stands for "block started by symbol" according to Love, p. 226). There is the user-space stack as well. Of particular interest to us today are the memory areas associated with the heap. Finally there are text, data, and bss sections for every shared library and the loader plus any explicitly memory mapped files or objects.
Only the text and initialized data segments actually need to be stored in an executable file. The executable has to say how much uninitialized data there is and what locations will be used, but the kernel will fill this memory with zeros at load time (or upon first reference to a page in the segment).
If you want to know how much space your program requires for
the text, initialized data, and bss, you can use the
size
program.
The runtime system manages the heap for you, as described
above. When it needs more memory, it uses the
brk()
and sbrk()
system calls, which
ask the kernel to set aside memory areas to expand the heap.
The model is that the heap has an address that defines the last
virtual address that exists in a valid memory area. This
address is called the break, and you ask for more memory
(or surrender memory back to the kernel) by asking the break to
be moved forward or back in memory. You can do this yourself,
too, but at the risk of interfering with the heap structures
maintained by malloc()
. If you call
brk()
and/or sbrk()
be prepared to do
all dynamic memory management yourself!
Why Abandon C's Heap
One reason to want to set the break yourself is to improve
the performance of a program that uses memory in a more
discplined way and hence will allow lower bookkeeping overhead
than that incurred by malloc()
and friends.
C heap support also has a couple of shortcomings. At least
on some Unix-like systems (all?), malloc()
and
friends never return memory to the operating system, so you may
take over memory management yourself to be a better neighbor.
More significantly, C's heap management is subject to
fragmentation, that is, a series of allocations and
deallocations may result in enough memory for a new data object
being free in the heap but not usable because it isn't
coniguous. C does not do compaction, nor is it supported
as a user option. Of course, if you're going to implement a
copying compactor, you might as well implement a whole garbage
collector!
Non-Heap Dynamic Memory
There are two other facilities available for managing dynamic memory that you may find interesting:- One can use a technique called memory mapping. The
idea is to assign a range of virtual addresses that will be
mapped by the virtual memory system to offsets in a file (or
device that looks like a file). The kernel uses this technique
to load your program code into memory: the text section (code)
of your object file is simply mapped into a memory area and then
the virtual memory system will take care of copying the
information from disk as the program counter moves through the
program. Memory mapping comes essentially for free when you
decide to support virtual memory: a segment in a memory area
that is paged out is a memory mapped portion of the swap
space on disk.
You can use this technique to do your own paging if you want, and it is often a useful choice for database applications in which marshalling and unmarshalling the data can be very expensive.
Memory mapping is operating system dependent. Linux/Unix systems support the calls
mmap()
andmunmap()
to implement memory mapped files. - There is a hybrid sort of memory allocation that is
available on some C implementations (i.e., it's not so portable,
but might be handy). The
alloca()
library call is likemalloc()
except that it allocates a block of memory on the stack, which will then be freed automatically at exit from the current program block.gcc
inlines these calls, which just do a stack push. DANGER: returning or storing a pointer to such a block of memory is the same as returning the address of a local variable, which is a very bad idea.
Modified: 28 March 2007