![]() CEG
433/633:
|
|
Abstract aimed at the student: This article is in support of lectures on "dynamic memory allocation" in class. The lectures and the article are independent of a specific programming language (such as C/ C++), and independent of specific operating systems (such as Unix/ Linux/ Windows). The details are applicable to the dynamic memory allocation portions of the POSIX (portable operating systems interface) standard supported by practically every high-level language and OS. We use C and Unix to be concrete when needed.
A pointer is the address of a memory cell. Nearly all CPUs of the 1990s, deal with memory in units of bytes, store an ASCII character using one byte, and an integer using four consecutive bytes. The pointer itself can be stored. Assuming that an address is no larger than 2^32-1, we can store a pointer value in four consecutive bytes. Obviously, the least value a pointer can be is zero. In C/C++, we refer to the zero-value pointer as NULL, often #defined in an #include file. In other languages, it is also called nil. All machines contain a memory cell whose address is 0. When we use NULL or nil to stand for a pointer value that is "not valid", we are depending on an assumption that our data structures will not ever be stored in a chunk of memory beginning at 0.
Systems programs will, some times, manipulate a pointer as an unsigned integer and vice versa.
The executable code of a function, as compiled, linked, and loaded into the memory, of a computer system is stored in memory. The starting (usually, the lowest) address of this chunk is the address of the function. We call this a pointer to the function. Such a pointer can be stored just as other pointers can be. Systems programs often construct arrays of pointers to such functions.
void * vp;This type (void *) is the least constraining of all pointer types. The variable vp can be assigned any address value.
T * tp;The variable tp is guaranteed to point to an object of type T. The
value of tp is considerably constrained. E.g., it needs to be properly
aligned on a memory address boundary suitable for the type T. While vp
= tp; triggers no warnings from the compiler, tp = vp; is
problematic. Type casting the right hand side as in
tp = (T *) vp;
only silences the compiler on the assurance that you, as the programmer, are assuring that vp has a value that is in fact a pointer of type T *. The type cast generates no extra code that converts the value of vp into a suitable value for tp. The 4-byte long value of vp (assuming that, the pointer value is 4-bytes long) is copied, as-is, into tp.
When an OS loads an executable binary file (named say f.prg) and constructs a process (let us call it P) out of it, the process has the following five address spaces:
text, data, bss, heap, and stack.
We used the Unix names for these address spaces. The "text"
is the executable sequences of CPU instructions of f.prg. The data is the
collection of initialized global variables (declared at the outermost scope of
f.prg, as well as those declared as static), strings, and other
constants (where ever they may have occurred in f.prg). The initial values
for these memory cells is dictated by the f.prg program. The bss is the
collection of uninitialized global variables. Before the process P begins
to execute, the OS sets the bss area to all zeroes. The text, data, and bss
areas do not change their sizes. In modern CPUs, the text area must not
change its content; i.e., the sequence of CPU instructions are not permitted to
be manipulated to become other instructions. Any attempt to change text is
caught as an exception.
The stack contains variables local to each invocation of a function. The stack grows and shrinks, in the discipline of push-pop.
The process P parcels out chunks from this heap in requested sizes to its own code. The heap of one process P has no connection the heaps of other processes, Q, R, ...
These five areas of memory are assigned to process P by the OS. Since both stack and heap can grow dynamically (i.e., during execution), no matter what their initial size of allocation is, an OS must facilitate their increase in size. Obviously, the sum total of such requests to grow from all processes is bounded by the total amount of memory the computer system has. A process should cooperate by returning unused memory to the OS so that another process that needs more memory can be granted its request.
See. Steven's book for more details.
A process can grow or shrink its pointer-based data structures, within its heap, by calling dynamic memory allocation functions. In the C++ language, we do it using operator new; in plain C, we use malloc(uint). The value returned by new or malloc is a pointer to an area of memory allocated from the heap. We return the block of memory so obtained, after it is no longer needed; delete in C++, free (pointer) in C. Do remember that in C++ and in other modern languages (most notably, Java), the new (allocate) and delete (free/ destroy) operations or their equivalent often occur in a hidden (i.e., not directly seen in the source code) way.
The heap management routines within the C run time package (usually called crt.o) deallocate the freed area, and combine it with adjacent free areas so that the result is a list of available areas of memory, each as large as possible.
When the process wishes to enlarge or shrink the heap or stack areas, it does so by calling upon the OS:
#include <unistd.h>
int brk(void *end_data_segment);
void *sbrk(ptrdiff_t increment);
Visit the Malloc/free web site mentioned below.
Read the 2-page handout, based on Knuth, given out in class.
| 10/25/00 01:16:01 PM |
| Open Content Copyright © 2000 pmateti@cs.wright.edu |