In our last chapter we worked with arrays using automatic allocation and saw the problems that come with automatic arrays such as wasted memory, sub-optimal performance and sometimes as an indirect result, buffer overflow. In chapter four we now use dynamic memory allocation to solve the same problems of the previous chapter. We will compare and contrast automatic allocation vs. dynamic allocation and discuss the gains we make in convenience, efficiency and performance.
Recall our stale pointer example from chapter two. We returned from a function and brought back the address of a variable that was local to that function. The compiler even issued a warning. We all agreed that the variable at that address was no longer valid. It's space had been deallocated and as a result its value was no longer guaranteed. Variables that are declared in between a pair of {}s are automatic variables. Automatic variables may appear as local variables in a function or even variables in a if/do/while loop block. A pair of braces defines a scoping block. A variable inside a scoping block has a life span starting at the line were it was declared and ending at the closing }. When the closing } is reached that scoping block is deallocated /destroyed. By destroyed we mean its memory has become available for re-use by other scoping blocks or stack frames (we will explain stack frames in just a moment). This deallocation or destruction of memory is a very simple and fast process that is done in constant time. The deallocated memory is not wiped clean or overwritten. That would take time proportional to the size of the chunk of memory being deallocated. Instead, a lazy deletion is done. A special address marker known as the TOP of the stack is reset back to previous position, thus marking everything above the stack as available for reuse in the next function call.
Consider the following program as we illustrate the call stack. The call stack is the region of memory where all automatic variables such as a function's local variables are stored. The call stack grows every time a function is called and shrinks as functions return because each function call pushed a chunk of memory on top of the call stack to hold that function's local variables and incoming parameters. What we are illustrating is the mechanism by which space for automatic variables is allocated and deallocated using a very simple mechanism of keeping a marker to the end (top) of the stack. This simple mechanism is very similar to using a counter to mark the end of an array. A simple increment or decrement of the counter indicates addition or deletion of the last element. A stack is like an array that can only grow or shrink by one element at a time on the end. In the case of the call stack the array is growing/shrinking in units of a chunk that represents the local variables and parameters of a function.
void baz( int x,y )
{
int z = x + y;
}
void bar( int x,y )
{
int z = x - y;
baz(x,z);
}
void foo( int x,y )
{
int z = x * y;
bar(x,z);
}
int main()
{
int x=5,y=10;
foo(x,y);
return EXIT_SUCESS;
}
We now look at the mechanism by which the space used by a function is allocated when it is called and then deallocated when that function returns. For our purposes we will illustrate the concepts in a very simplified manner, omitting details of representation that may differ from OS to OS or compiler. We start with the call stack.
We start by assuming main is executing but has not called foo() yet. The top of the stack is at the highest address of main's stack frame. In some systems the stack may grow toward low memory. Which direction it grows is not important.
main calls foo() and foo's stack frame is pushed onto the stack and the top of the stack is advanced to the end of foo's frame.
foo() calls bar() and bar's stack frame is pushed onto the stack and the top of the stack is incremented.
bar calls baz() and baz's stack frame is pushed onto the stack and the top of the stack is incremented.
baz makes no function calls and returns to bar.
bar finishes its execution and returns to foo. The stack's top is decremented (popped) back to its position prior to calling bar.
foo finishes its execution and returns to main. The stack's top is decremented (popped) back to its position prior to calling foo.
It is important to see that automatic memory is reclaimed in constant time by simply popping the latest block off the stack with a simple change in the value of the stack's top address. Any data beyond the top is now considered garbage and its integrity no longer guaranteed because that memory is now available for the next function call to overwrite it with the new function's data. Just like the operation of deleting the last value in an array is done simply by decrementing the counter. Data beyond the top gets overwritten as soon as the next frame is pushed onto the stack. This method of automatic reclamation of memory is a example of a lazy deletion or shallow deletion. The compiler wastes no time scrubbing or re-initializing that reclaimed memory. It is Russian roulette to read or write memory beyond the current top of the stack. Unfortunately pointer variables give us the power to save an address that is no longer valid (dangling/stale pointer). To dereference such a pointer is a memory error that can produce unpredictable behavior from which there is no recovery.
In case you are wondering if a function's code is stored in the stack, the answer is no. Since only one function can be executing at a time there is no need to store the code in the stack. Only the data is stored in the stack frame. The code for the currently executing function is stored separately from the stack and only one function can be executing at a time. In that currently executing code, all references to variables are bound to the data values in the top stack frame which is the frame for the currently executing function. There are however some circumstances where data declared in a function, ends up getting stored not in the stack but with the code it was declared in. The compiler does this in order to make data immutable. This area of memory where the code is stored is protected memory and it cannot be written to. It makes sense to "protect" code since you don't want program code to ever be overwritten. Putting it in protected memory is how the compiler enforces this. We saw an example of such data when we declared pointers to char and initialized them to a string literal: char * name ="Johnny";
this declaration declares a string constant that gets stored in protected memory with the code it was declared in so as to protect it from modification. If you attempt to modify any of the characters in the string pointed to by name, you will get a segfault (segmentation fault) which will crash your program. One other case where data declared in a function does not get stored in that function's stack frame is a variable that is declared as static. A variable declared static is done so with the intention that its value is not destroyed when the declaring function exits. Suppose you wanted to keep a counter of how many times a function is called. It makes sense that the best place to put it is inside the function, however, scoping rules tell us that our counter gets destroyed and recreated each time the functions is exits and is called again. To override the scoping lifespan such a local variable would be declared as static and the compiler would store that variable in some place other than the stack frame. Exactly where these static values are stored is up to the compiler and is not strictly mandated by the ANSI standard for the C language.
When you delete a file under Windows environment that deletion does not destroy the contents of the file. Instead it "marks" that chunk of disk space as being available to anyone who wants to use it to write another file in that space. In old DOS the marking mechanism was trivial. It simply went into the directory table and changed the first char of the filename to a ? mark. This mark told the operating system that the space was no longer to be considered in-use and files that want to write to this place on the disk are free to do so. This simple marking operation could be done in constant time. This reclamation strategy is sometimes called "passive" or "lazy" deletion. As long as no one came along and over wrote any of the sectors that file was sitting in - the data was still there. However as soon as other files were edited and saved, more and more segments of that old file got overwritten. Peter Norton (Norton Utilities and now Symantec) created an industry in the 80s writing effective software to recover data from deleted files.
int arr[] = { 9,4,6,1, 7 };
int cap=5, cnt = 5;
arr: [9][4][6][1][7]
^
|
end of array
--cnt; /* delete/deallocate/reclaim last elem in the array */
arr: [9][4][6][1][7]
^
|
end of array
Note how this simple operation of decrementing the cnt effects a deletion of the last element and the space occupied by the 7 has been reclaimed. Overwriting it with some special value is unnecessary. The cnt marks the end of the array. A critical property of a deallocation/recycling scheme is that (when possible) it does not require time proportional to the size of the object being deallocated.
Before we demonstrate how to allocate dynamic memory it will be helpful to compare and contrast the advantages of dynamic over automatic allocation. Once you understand the advantages of dynamic memory we will show you how to use it.
Automatic | Dynamic |
---|---|
dimension of array must be decided at compile time | dimension decision can be deferred till runtime at the moment of need |
array's memory is not reclaimed until its name goes out of scope | array is immune to scope and programmer controls exactly when array's memory is destroyed/reclaimed |
array (or variable) has a name that stays with it for life | array (or variable) has no name - so you better have saved its address into a pointer variable when you alloc'ed it |
array is stored in the stack | array is stored in separate region called the heap |
The first point above about dynamic memory is probably the most important advantage over automatic. You do not have to make guesses when you are writing your program as to how big of an array you will need. Dynamic memory allows you to defer that allocation decision until the program is running. Thus for example, you can query the file system as to how big a file is so you know how much array you need to read in all the values from that file. The second point is also a tremendous win. Dynamic memory does not get reclaimed until you explicitly cause it to be. This means you can recycle that memory as soon as you are done with the array instead of having to wait until some scoping block is exited. You completely control memory usage and can maximize efficiency.
One of the highlights of this chapter is the combining of these two advantages to solve the buffer overflow problem with dynamic allocation.
Dynamic memory in C is allocated from the heap at runtime using malloc() or one of its sibling functions: calloc() and realloc(). Dynamic memory is deallocated or recycled to the heap by the free() function. Let's see what the man pages have to say about malloc() and its siblings.
bash-2.05b$ man malloc MALLOC(3) Linux Programmer's Manual MALLOC(3) NAME calloc, malloc, free, realloc - Allocate and free dynamic memory SYNOPSIS #include <stdlib.h> void *calloc(size_t nmemb, size_t size); void *malloc(size_t size); void free(void *ptr); void *realloc(void *ptr, size_t size); DESCRIPTION calloc() allocates memory for an array of nmemb elements of size bytes each and returns a pointer to the allocated memory. The memory is set to zero. malloc() allocates size bytes and returns a pointer to the allocated memory. The memory is not cleared. free() frees the memory space pointed to by ptr, which must have been returned by a previous call to malloc(), calloc() or realloc(). Other wise, or if free(ptr) has already been called before, undefined behaviour occurs. If ptr is NULL, no operation is performed. realloc() changes the size of the memory block pointed to by ptr to size bytes. The contents will be unchanged to the minimum of the old and new sizes; newly allocated memory will be uninitialized. If ptr is NULL, the call is equivalent to malloc(size); if size is equal to zero, the call is equivalent to free(ptr). Unless ptr is NULL, it must have been returned by an earlier call to malloc(), calloc() or realloc(). RETURN VALUE For calloc() and malloc(), the value returned is a pointer to the allo- cated memory, which is suitably aligned for any kind of variable, or NULL if the request fails. free() returns no value. |
We now look at our first demonstration of allocation, initialization and deallocation of dynamic memory.
Dynamic memory is anonymous memory. It has no name. For this reason we don't refer to a pointer to a dynamic chunk (array) as the name of the array. Contrast this with automatic variables which have a name bound to them for life. Every time we call malloc() we must have a pointer variable ready to store the address value returned by malloc()or we have just allocated memory that is inaccessible. This situation is called a memory leak (a.k.a. creating garbage). Another common way to leak memory is to store the address of the array in the pointer, but later overwrite the value in that pointer before you call free on the pointer (which frees what it points to).
malloc() takes only one arg value - an unsigned long that represents the number of bytes you want allocated. Notice however that when we malloc an array, we pass an expression to malloc that is the number of elements requested times the size in bytes of each element. The product of the two factors is the total bytes requested. This is the common practice as it is self documenting and clearer to programmer and reader. The memory elements allocated by malloc() are guaranteed contiguous just like automatic arrays. If there is not enough continuous memory in the heap for the request - then malloc will return a NULL. If the allocation was successful then a pointer to the start of the memory is returned. In either case malloc() always returns a pointer to void which assign into out pointer variable.
In our demo we scanf() into our dynamic string using the same syntax as if reading into an automatic array variable. All of C's string functions are agnostic to whether the incoming string is dynamic or automatic. After initializing and echoing the string - we call free() on the pointer var and then assign a NULL into the pointer.
Question #1: Although many programmers don't do it consistently - Why is it a good idea to assign NULL into a pointer that has just been freed?Now that you know how to allocate dynamic memory there are dangers to be understood and avoided. Our first illustration is a caveat that is the inverse of our dangling reference or stale pointer example in the previous chapter. This common misuse of dynamic memory is called a memory leak.
char *ptr; ptr = malloc( 5 );
In the above scenario we declare a pointer variable ptr that lives in the stack, then malloc a chunk of 5 chars in the heap and store it's address into ptr. We then malloc a new chunk of another 5 bytes in the heap using malloc and store this new chunk's address in ptr.
ptr = malloc( 5 );
What happens to the memory allocated at address 1000? Nothing happens to it - and nothing ever will because we have overwritten our only pointer to that memory. There is no garbage collection in C and so this memory is still marked as in-use and cannot be given out to any other calls to malloc. The chunk is wasted. This is a memory leak. Leaking memory is not a memory error per-se. It is however a bad practice that might cause your program to run out of memory. Garbage collection is a function of the runtime environment that watches for cases where memory is allocated but no pointer variable has its address. In this case the runtime environment realizes that this memory is inaccessible and will de allocate that chunk of memory at its earliest convenience.
It is natural to ask: "Why doesn't C implement garbage collection? It sure would save a lot of careless programmers from running out of memory." The reason is efficiency. Some languages, like Java, do have garbage collection. However the performance hit can be significant. In fact this performance hit can make the difference between whether a language is suitable for some application or not. Consider a program that must react in strict real-time to rapidly changing physical events. It is quite possible that time required by garbage collection could occur just at a moment when the program needs to react to an event. Garbage collection from the heap is not a constant time operation. The actual collection operation requires time proportional to the amount of memory being reclaimed. Making the program vulnerable to unexpected interruptions of indeterminate length for a garbage collection algorithm to complete could cause a mission critical failure for a hard real-time application. This is the extreme case against garbage collection. In general having garbage detection running and watching in the background simply slows the program since that garbage detection code has to run every so often just to look for garbage even before an expensive reclamation operation is triggered.
Our second demo program declares a pointer in main, but wants to initialize that pointer in a function. This requires that the address of the pointer be passed into the function. This is where things get interesting again because the receiving function must prototype a pointer to pointer - the address of the pointer var in main. This introduces the ** notation.
Our demo2 above accomplishes the same work as demo1 - except- it modifies main's word pointer in a function - rather than in main where the pointer was declared. The data type of the incoming pointer is thus a pointer to a pointer and its prototype is **. Once this is understood we then apply our same rules as passing the address of an int. We simply dereference the local parameter and use it as a synonym for the value of the original pointer in main. When passing the address of any variable just add a star to the prototype of the receiving parameter. Then, once inside the function, dereference that parameter with a single star as a synonym for the original variable from main that you are trying to modify.
Now that we know how to allocate a dynamic string, let's look at pointer arithmetic on automatic and dynamic strings. An important concept to be seen in the following examples is that the string functions and all the syntax used on strings and arrays does not care or make distinction between whether the array or string being operated on is dynamic or automatic. Both types of arrays are treated with the identical syntax. The only differences are that an automatic array's name is not a variable and produces its true dimension when given to the sizeof operator. Other than that, an automatic array's name and a pointer to an array can be dereferenced, indexed and have pointer arithmetic performed on them in identical manner. One last thing. It is not really correct to refer to a pointer to a dynamic array as the array's name. Only an automatic array really has a name. That name is bound to the array's memory for the scope of the array. When the name goes out of scope - so does the array's memory. Dynamic memory is anonymous and it is up to the programmer to retain's the array's address in a pointer at all times until it is freed.
Our third demo declares an array of pointers, and passes that array into a function that will malloc strings to those pointers. The array of pointers itself is automatic storage but the strings hanging off the pointers are dynamic. Note this scheme still suffers from unused rows (pointers) but the length of each string is just enough to store that string.
Note that when we free the memory associated with this structure we free only the strings and not the array itself since the array is automatic. Only the strings are dynamic.
Our fourth demo allocates a dynamic array of pointers to the exact dimension that will be used, then mallocs dynamic strings to each of the pointers. Note this scheme solves both horizontal and vertical storage waste problems.
The memory allocated in demo four looks like this:
Note that we draw an arrow out of the wordArray variable box to the beginning of the array. This is just to emphasize that wordArray is a variable not a const. Furthermore the array itself is dynamic as well as the strings hanging off the array. When we free the memory - notice that we free the strings first and the array itself second.
Question #2: WHY: do we free the strings first and the array after? What happens if we do it in reverse order?
#define MAX_WORDS 10 #define MAX_WORDLEN 15 int main() { char wordArray[MAX_WORDS][MAX_WORDLEN]; /* assume only 5 words ("horses", "cats", "rats", "dogs", "bats") were read into the array */ } |
The disadvantages of the 2D char array are:
If we assume an input file as follows:
5 horses cats rats dogs bats
The memory allocated for wordArray now looks like this
The advantage to the dynamic array is obvious. We dimension it at runtime as soon as we read the expected wordCount from the file.
FYI: Your Lab #3 assignment will NOT have a number at the top of the input files to tell you how to dimension the dynamic array of pointers. Instead - Lab3 requires that you initialize your array to some default dimension, then whenever your array gets full, you must resize your array by doubling its capacity, copy the old pointers into the new array and then free the old array.
We will now use dynamic memory to solve our buffer overflow vulnerability when reading strings. We want a solution that reads an entire line of text with no fear of overflow or truncation. The strategy is simple to explain. Allocate a small dynamic string (array of char). Use fgets() to read as much as will fit in the buffer up to the newline (end of line) or end of file (EOL or EOF). If neither EOL nor EOF has been reached then we know our read was truncated because our array was not big enough. If this is the case we allocate a bigger (doubled in size) array and copy all the chars from our smaller array into the new array. Once we have copied the chars to the new array we free the old array. Now we can read again from the file right where we left off. It is important that not only do we resume reading from the file where the last read left off - we also must store our next read into the array at the place (index) in the array where we left off putting characters. This process repeats until we encounter EOL or EOF.
Here is an illustration of a simplified version of an algorithm that does not trim whitespace or do any kind or error testing/recovery. It just starts with a small buffer and does repeated fgets() reads from the file until it reaches EOL or EOF. After each read attempt it allocates an array twice as big and tries again where it left in the file and the array until EOL or EOF is encountered.
int buffSiz = 10; char *buffer = malloc(buffSiz); 1 2 01234567890123456789012345678 inFile: [one two three four five six \n] ^ | read cursor ------------------------------------------ READ #1: fgets( buffer, buffSiz, inFile); and now the buffer looks like: buffer[0] 'o' buffer[1] 'n' buffer[2] 'e' buffer[3] ' ' buffer[4] 't' buffer[5] 'w' buffer[6] 'o' buffer[7] ' ' buffer[8] 't' buffer[9] '\0' and the read cursor on the input file is pointing at the 'h' in "three" 1 2 01234567890123456789012345678 inFile: [one two three four five six \n] ^ | read cursor Our first read did NOT store a newline, nor have we reached EOF, so we know the input may have been truncated and we need to read again. We must realloc our string to double its size buffSiz *= 2; buffer = realloc( buffer, buffSiz ); and now the buffer looks like: buffer[0] 'o' buffer[1] 'n' buffer[2] 'e' buffer[3] ' ' buffer[4] 't' buffer[5] 'w' buffer[6] 'o' buffer[7] ' ' buffer[8] 't' buffer[9] '\0' buffer[10] '?' buffer[11] '?' buffer[12] '?' buffer[13] '?' buffer[14] '?' buffer[15] '?' buffer[16] '?' buffer[17] '?' buffer[18] '?' buffer[19] '?' ------------------------------------------ READ #2: fgets( buffer+strlen(buffer), buffSiz-strlen(buffer), inFile); Whatever chars we read in get stored starting over top of the terminating null of the last read and now the buffer looks like: buffer[0] 'o' buffer[1] 'n' buffer[2] 'e' buffer[3] ' ' buffer[4] 't' buffer[5] 'w' buffer[6] 'o' buffer[7] ' ' buffer[8] 't' buffer[9] 'h' buffer[10] 'r' buffer[11] 'e' buffer[12] 'e' buffer[13] ' ' buffer[14] 'f' buffer[15] 'o' buffer[16] 'u' buffer[17] 'r' buffer[18] ' ' buffer[19] '\0' and the read cursor on the input file is pointing at the 'f' in "five" 1 2 01234567890123456789012345678 inFile: [one two three four five six \n] ^ | read cursor Our second read did NOT store a newline, nor have we reached EOF so we know the input may have been truncated and we need to read again. We must realloc our string to double its size buffSiz *= 2; buffer = realloc( buffer, buffSiz ); and now the buffer looks like: buffer[0] 'o' buffer[1] 'n' buffer[2] 'e' buffer[3] ' ' buffer[4] 't' buffer[5] 'w' buffer[6] 'o' buffer[7] ' ' buffer[8] 't' buffer[9] 'h' buffer[10] 'r' buffer[11] 'e' buffer[12] 'e' buffer[13] ' ' buffer[14] 'f' buffer[15] 'o' buffer[16] 'u' buffer[17] 'r' buffer[18] ' ' buffer[19] '\0' buffer[20] ? buffer[21] ? buffer[22] ? buffer[23] ? buffer[24] ? buffer[25] ? buffer[26] ? buffer[27] ? buffer[28] ? buffer[29] ? buffer[30] ? buffer[31] ? buffer[32] ? buffer[33] ? buffer[34] ? buffer[35] ? buffer[36] ? buffer[37] ? buffer[38] ? buffer[39] ? ------------------------------------------ READ #3: fgets( buffer+strlen(buffer), buffSiz-strlen(buffer), inFile); Whatever chars we read in get stored starting over top of the terminating null of the last read buffer[0] 'o' buffer[1] 'n' buffer[2] 'e' buffer[3] ' ' buffer[4] 't' buffer[5] 'w' buffer[6] 'o' buffer[7] ' ' buffer[8] 't' buffer[9] 'h' buffer[10] 'r' buffer[11] 'e' buffer[12] 'e' buffer[13] ' ' buffer[14] 'f' buffer[15] 'o' buffer[16] 'u' buffer[17] 'r' buffer[18] ' ' buffer[19] 'f' buffer[20] 'i' buffer[21] 'v' buffer[22] 'e' buffer[23] ' ' buffer[24] 's' buffer[25] 'i' buffer[26] 'x' buffer[27] ' ' buffer[28] '\n' buffer[29] '\0' buffer[30] ? buffer[31] ? buffer[32] ? buffer[33] ? buffer[34] ? buffer[35] ? buffer[36] ? buffer[37] ? buffer[38] ? buffer[39] ?
Our third read DID store a newline so we know we got the whole line. We are done. Even if the input file only contained this one line and there was no newline at end we would have detected EOF and still concluded that we are done reading this line.