Rex Jaeschke is an independent computer consultant, author and seminar leader. He participates in both ANSI and ISO C Standards meetings and is the editor of The Journal of C Language Translation, a quarterly publication aimed at implementers of C language translation tools. Readers are encouraged to submit column topics and suggestions to Rex at 2051 Swans Neck Way, Reston, VA, 22091 or via UUCP at uunet!aussie!rex.
The C run-time library has long had a family of routines that enable a programmer to allocate and free memory at run-time, at his pleasure. This capability is a powerful one and was adopted (and somewhat expanded) in ANSI C.
Oftentimes you define an array of elements (necessarily of fixed size) only to find that, in most cases, you don't use all the elements or that, in some cases, you need just a few more. What you need is the ability to have variable sized arrays. However, according to the definition of C, the dimension of an array in a definition must be a compile-time integer constant. That is, the C language does not support such constructs. (Note that the Numerical C Extensions Group, of which I am the convener, is investigating the possibility of adding such a construct.) However, this idea can be implemented using the memory allocation routines in the standard library.
The beauty of these allocation routines is twofold: the programmer determines just when space is allocated and exactly how long it is kept, and, if the program is written correctly, you can change the manner in which the space is allocated and freed, transparently. Let's discuss the second point further.
ANSI C defines the term storage duration by saying "An object has a storage duration that determines its lifetime. There are two storage durations: static and automatic." I prefer to also add a third duration, dynamic. An object having dynamic storage duration is one allocated by the programmer using the library. (For the purposes of this discussion, the address space from which dynamic objects are allocated will be referred to as the heap. This term is widely used for this purpose but is not used in the ANSI C Standard.)
Consider the following example:
#include <stdlib.h> void f() { char c1[100]; static char c2[100]; char *c3; c3 = malloc(100); c1[10] = 'a'; c2[10] = 'a'; c3[10] = 'a'; }Ignoring the possibility of malloc() failing to allocate memory, c1, c2, and c3 can be used to designate the automatic, static, and dynamic arrays, respectively. Since the notation for referencing all three arrays is identical, the executable code can be ignorant of the object's storage duration. You can change from automatic to dynamic, from dynamic to static, etc., with no real impact on the code, if you design it appropriately to begin with.The allocation functions somehow magically change the address space of our program at run-time. The way in which this is done is specific to an implementation and may vary widely. In any case, an understanding of such details is unnecessary to use the allocation functions effectively. All you need know is that if they succeed, the requested space is allocated contiguously and you are given a base address.
The Parent Header
In the not too distant past, there were only four or five "standard" headers. Apart from those, there was a wide variation as to which functions were provided and in which header (if any) they were declared. ANSI C requires the allocation functions to be declared in the header stdlib.h. Many implementations currently declare them in malloc.h as well as, or instead of, stdlib.h. I have also seen quite a lot of old code that contained explicit declarations for these functions, presumably because no header in their implementation contained them.As a result of ANSI C, the declarations of these functions has changed both with regard to return as well as argument types. ANSI C adopted the concept of a void pointer from C++. This solved two important issues: it provided a bridge for porting code across byte and word (and other) architectures where different pointer types may actually have different physical representations, and secondly, it provided a way to represent a generic pointer, one that simply contained an address of some (unknown) object type.
Since the allocation routines are not given any information about the type of object a programmer wishes to store in the allocated space, the pointers used and returned by these functions were prime candidates for type void *. A consequence of this is that the returned value no longer need be explicitly cast. For example in the following case:
int *pi; pi = (int *)malloc(10 * sizeof(int)); pi = malloc(10 * sizeof(int));the assignments are equivalent since a void pointer is assignment-compatible with all other pointer types. (Historically, it was common to see such casts even though they generally were not needed. That is, strict pointer assignment-compatibility checking was not enforced as is now required by ANSI.)If some of your code explicitly declares the allocation functions as having return types of char *, without such casts you will get errors when compiling in strict ANSI mode if the target of the assignment has type other than char *. The best solution to this is to remove the explicit declaration and include stdlib.h instead.
With ANSI's adaptation of function prototypes from C++, stdlib.h now describes the allocation routines' argument type information as well. Again, all pointer types here have type void * but this is of no consequence since any "real" pointer type is compatible with void * and, as such, objects of such type can be passed in.
ANSI C has invented the type size_t, the type of a sizeof expression. This type is typedefed in numerous standard headers including stdlib.h and is used in various library function prototypes (including the allocation functions) for the type of sizes and counts. Since sizes and counts can never be negative, size_t is an unsigned integer type. However, the underlying type of size_t is implementation-defined and may be unsigned int or unsigned long. Historically, descriptions of the allocation functions stated that sizes and counts had type unsigned int.
The Allocation Functions
calloc
#include <stdlib.h> void *calloc(size_t nmemb, size_t size);calloc() allocates contiguous space for nmemb objects, each of whose size is size.The space allocated is initialized to all-bits-zero. Note that this is not guaranteed to be the same representation as floating-point zero or the null pointer constant NULL.
free
#include <stdlib.h> void free(void *ptr);free() causes the space (previously allocated by calloc(), malloc(), or realloc()) pointed to by ptr to be freed.If ptr is NULL, free does nothing. Otherwise, if ptr is not a value previously returned by one of these three allocation functions, the behavior is undefined.
The value of a pointer that refers to space that has been freed is indeterminate, and such pointers should not be dereferenced.
Note that free() has no way to communicate an error if one is detected.
On some systems, most noticeably MS-DOS, freed space may not actually be given back to the operating system. (It likely will, however, be available for future allocations within that program.) It might only be really released when the program terminates. One consequence of this is that if you try to execute another program from within a running program that has freed up memory using free(), there still might not be sufficient physical memory available to start the new program.
malloc
#include <stdlib.h> void *malloc(size_t size);malloc() allocates contiguous space for size bytes. The space allocated has no guaranteed initial value.
realloc
#include <stdlib.h> void *realloc(void *ptr, size_t size);realloc() changes the size of the space pointed to by ptr to have size size.If ptr is NULL, realloc() behaves like malloc(). Otherwise, if ptr is not a value previously returned by calloc(), malloc(), or realloc(), the behavior is undefined. The same is true if ptr points to space that has been freed.
size is absolute, not relative. If size is larger than the size of the existing space, new uninitialized contiguous space is allocated at the end; the previous contents of the space are preserved. If size is smaller, the excess space is freed; however, the contents of the retained space are preserved.
If realloc() cannot allocate the requested space, the contents of the space pointed to by ptr remain intact.
If ptr is non-NULL and size is 0, realloc() acts like free().
Whenever the size of space is changed by realloc(), the new space may begin at an address different from the one given it, even when realloc() is truncating. Therefore, if you use realloc() in this manner, you must beware of pointers that point into this possibly-moved space. For example, if you build a linked list there and use realloc() to allocate more (or less) space for the chain, it is possible that the space will be "moved," in which case the pointers now point to where successive links used to be, not where they are now. You should always use realloc() as follows:
ptr1 = realloc(ptr, new_size); if (ptr1 != NULL) { ptr = ptr1; ... }This way, you never care whether the object has been relocated since you always update ptr each call, to point to the (possibly new) location.
General Comments
The way in which a heap is physically organized can vary widely. On some systems, the stack and the heap (and possibly even the static data area) share the same address space. On others, each may have its own address space. Some MS-DOS implementations provide both near and far heaps.Historically, many C implementations have permitted the allocation of zero bytes to be successful. That is, a non-NULL pointer is returned. Since ANSI C does not permit zero-sized objects to be defined, this practice was hotly debated during X3J11 deliberations. As a compromise, if you attempt to allocate zero bytes, it is implementation-defined whether a null pointer or a unique pointer is returned.
We are told that if an allocation attempt fails, NULL is returned. The common approach I've seen to this is to display some error message and call exit(). However, most applications I have seen could ill-afford to actually do this since it would leave either disk files and/or shared memory data areas compromised. For example, if you cannot get more dynamic space, you may have quite some work to undo your current situation before you can gracefully terminate or continue. On the other hand, failure to allocate more memory when doing an in-memory sort can simply be handled by writing the sorted tree to disk, freeing the memory, and starting on the next set of strings. In such cases, the failure to allocate memory is not fatal. In cases where it is, you must consider the ramifications of receiving a NULL return at design time, not during maintenance when the first failure occurs.
When heap allocation fails it might well be useful to find out how much you can get. Unfortunately, ANSI C does not provide this capability.
Several implementations (including Microsoft's) do provide some help in this area. Either they can tell you how much is available now in one allocation or, how many allocations you can make of a given size. (The two need not add up to the same number of bytes since each time you request bytes, extra bytes may also be fetched to help manage the space allocated.)
Similarly, ANSI C provides no help in debugging heap-related problems by "walking the heap links" and the like. Again, it's up to the quality of the implementation.
On some systems (VAX/VMS, for example) the cost of allocating memory dynamically can be somewhat expensive. As such, a caching approach may be taken. That is, when you free memory the larger of the freed block and the cache currently held, will be kept. The idea is that if you alternately allocate and free, each new allocation will have some chance of getting memory from the freed cache.
ANSI C guarantees that any non-NULL address returned by the allocation functions will be aligned appropriately so it can be dereferenced via any pointer type. On systems that require object alignment, this means that space is allocated in multiples of some cluster value (such as machine words, for example.) On such systems, more memory may actually be allocated than you requested. If your program contains a bug and copies (slightly) beyond the end of allocated memory, the bytes overwritten may be those extra ones and no error occurs. However, if you change the request to a few extra bytes, the bug may manifest itself. The most common example I see is as follows:
char name[30]; getname(name); pc = malloc(strlen(name)); strcpy(pc, name);Here, strcpy() adds a null character to the destination but no space was allocated for it. If the length was odd and malloc() allocates an even number anyway, the problem will not be observed. However, with even length names it may well appear.It is considered good style to explicitly free allocated memory when you are done with it. Presumably, if you don't, this is done when your program terminates (although this is not so stated by ANSI C.) Note that if you "forget" where your allocated space resides (by overwriting the pointer value returned by malloc(), for example), there is no way of getting that address back. One relatively easy way of having this happen is to use:
ptr = realloc(ptr, new_size);If realloc() fails, you have lost the address of the original area.An alternate memory allocation system also exists in many systems. It usually involves using sbrk(). The two schemes are incompatible and must not be used in the same program. ANSI C does not include this alternate scheme.
Transparent Heap Usage
It is possible that your program calls the allocation routines even if you don't call them yourself. For example, some library routines might need dynamic space to efficiently handle variable size amounts of local information.Many systems have a fixed limit on the number of open files they support. However, others do not. They can achieve this by building a linked list of FILE objects using the allocation routines. They may even include stdin, stdout, and stderr in this list, in which case, the program startup code may contain calls to malloc(), etc. Compile and link an empty main() program and look at the linker map to see if these library functions are called at startup.
Multi-Dimensional Arrays
Occasionally, it may be necessary to allocate a multi-dimensional array on the heap. This can be done just as easily as for single-dimensioned arrays once you master the required pointer declaration. For example,
double (*pd)[10]; pd = malloc(50 * sizeof(double)); pd[3][2] = 1.234;By declaring pd to be a pointer to an array of 10 doubles, pd can be subscripted to two levels. pd[3] designates the fourth row of 10 elements and pd[3] [2] designates the third column in that row. (If you are confused about the difference between a pointer to double and a pointer to an array of double, you will have to wait for a future column.)