Return to lecture notes index

September 30, 2010 (Lecture 11)

Dynamic Memory

We've talked a lot this semester about variables, pointers, and memory. But, there is one bridge that we haven't crossed. What if we don't know what we need until after the program is running. For a trivial example, imagine that we want to sort a list of numbers supplied by the user -- we'll need to create array after the program starts running and can get the user input. We can't allocate that array at compile-time because it isn't known and might change from run to run. Or, for those of you who might happen to be familiar with linked lists -- we generally create the nodes as we go.

Solving this type of problem is dynamic memory -- memory that can be allocated and freed as needed. In java, we did it by instantiating objects via the "new" operator and releasing references to them such that Java could "garbage" collect them.

C's version of "new" is a libary function called malloc(), short for "memory allocate". Unlike Java, C is not garbage collected, so we'll need to explicitly free() memory once we are done.

In order to ask for memory, we just tell malloc() how much we'll need and cast it to the right pointer type. When we free memory, we pass the pointer to free() and it deallocates the space. We'll also set the poiner to NULL. This will cause an error if we try to re-use it after freeing it -- which is a somewhat common error more.

Here are a few examples:

  #include <stdio.h>
  #include <string.h>
  #include <stdlib.h>

  int main  () {

    int *number;
    float  *tenFloats;
    char *word;

    number = (int *) malloc (sizeof(int));
    *number = 5;

    tenFloats = (float *) malloc (10 * sizeof(int));
    tenFloats[5] = 4.7;

    word = (char *) malloc (5);
    strcpy (word, "Greg");

    free (number);
    number = NULL;

    free (tenFloats);
    number = NULL;

    free (word);
    number = NULL;

    return 0;
  }
  

In addition to malloc(), there is a convenience function, calloc(). It is short for "clear and allocate". It zeros the memory before giving it to us. This raises an interestng point. Any even half-decent general purpose operating system will zero memory before allowing a program to have it -- this is the only way to prevent one program's old data from being read by another program. But, the memory we get from malloc() isn't necessarily zeroed. This is for two reasons.

Embedded systems, and other types of specialized systems may not have privacy concerns -- so they may not zero the memory. And, when we call free(), the memory almost never actually gets returned to the OS. Instead the "user level memory allocator", otherwise known as the "malloc library" that is part of our program gets it back. The next time we ask for memory, it can slice it, and dice it, and give it back. And, since programs most often initialize memory to values before using it, malloc()s don't usually bother to zero it.

So, calloc() zeros it for us, should we choose to use it instead of malloc(). It also does the multiply for us, taking the number of elements and the size of each element as two separate arguments:

  int *numbers = (int *) malloc(100*sizeof(int));
  memset (number, 0, 100*sizeof(int));

  int *numbers = (int *) calloc(100, sizeof(int));
  

Memory: A Quick Review, Plus the Heap

The figure below shows a summary of a computer's memory. Notice the "stack" that grows downward. Recall that this is where the "automatic" variables, arguments, and other "per call" function state is stored. We "push" the stack frames with this information onto the stack (at the bottom) when a function is called -- and "pop" it off upon the function's return.

At the very bottom of the picture are the things we read in from the executible file: the program code, itself (the "text") and initialized global and "static local" variables. Above that are those variables that live as long as the program, but that are not statically initialized, also including global and "static local" variables.

Above that is the "heap". It grows in the opposite direction as the stack. The "user level memory allocator", a.k.a. malloc, gets its memory from the heap via system call, brk(). This call asks the operating system to adjust the "brk point", the dividing line between the heap and the unallocated space above. By "raising the brk point", malloc can get more memory that it can then use to satisfy malloc() and calloc() calls.

When this memory is freed, it goes back to malloc, which can then give it back later on to satisfy anotehr request. Although in theory malloc could "lower the brk point" and give memory back to the OS, this almost never happens. As you'll learn in 213, malloc consists of some data structure, usually a series of lists, which keep track of the free space. Since memory is allocated and freed in an arbitrary order in arbitrary amounts, it is time consuming to coalesce these small pieces back into whole pages, and even then, only rarely is the top one completely free -- and so, these days, it is hardly worth the effort. It wastes processor with no real gain. to c

Memory Errors

We'll talk a lot more about memory-related programming mistakes. But, for now, I just want to identify a few of the really big ones:

For now, let me suggest that, although there are some tools that can help to find these problems, the best solution is to program in ways that minimize the exposure -- defensive programming. We'll talk about those as the semester rolls on. For now, let me make a few quick suggestions, just to give you a small taste of the flavor:

struct

The C Language supports complex data types that are composed of several individual pieces of data. One classic example of this type of complex data type is the "student record" which might be composed of a student's name, birthdate, identification number, and perhaps another complex data type, a transcript of courses. Another classic example, is the price tag, which might contain both the name of the product and the price.

This type of complex data is sometimes called structured data and for this reason it is supported in the C Language with a constrcut known as the struct. This type of structured data is also sometimes known as a record, nomenclature that dervies from databases. For this reason, some languages call the analagous construct the record.

Consider the example below. It illustrates the definition of a struct. Please notice the keyword struct, followed by the struct's identifier. It is important to realize that this does not create an actual instance -- just defines the struct and gives the new type of struct the name "person_t".

Next comes the struct's body -- the individual fields. These are the individual types that combine to form this complex, structured, type.

Please pay careful attention to the ;-semicolon after the }-closing-squiggly. This ;-semicolon is very important -- and very easy to leave out, especially for Java programmers who aren't accustomed to ending class definitions with one. If it is left out, in many cases, the C compiler will choke and produce all sorts of seemingly meaningless error messages.

  struct person_t {
    char fname[256];
    char lname[256];
    unsigned int age;
  };
  

If we actually want to declare an instance of this struct type, we can do that as below. Please notice that we must use the keyword "struct" as part of the declaration. Regardless, the line below creates an actual "struct variable" that we can use.

  struct person_t somePerson;
  

It is also possible to contract the definition and the declaration into one statement as below:

  struct person_t {
    char fname[256];
    char lname[256];
    unsigned int age;
  } somePerson;
  

Regardless, we access the fields of the struct using the "." operator as follows:

  struct person_t somePerson;

  strcpy (somePerson.fname, "Greg");
  strcpy (somePerson.lname, "Kesden");
  somePerson.age = 65;
  

When passing structs to functions, we often pass them by reference, even if we don't intend to change them within the function. We do this to avoid the overhead of copying the struct by value -- it is much faster to only copy the pointer. Soon, we'll learn about hints we can give the compiler if we don't intend to change it.

This means that we'll spend a lot of time accessing struct's via pointers. One way of doing this is exactly as one might expect. We dereference the pointer to get to the variable and then use the .-dot operator.

  int someFunction (struct person_t *somePerson) {
    strcpy ((*somePerson).fname, "Greg");
    strcpy ((*somePerson).lname, "Kesden");
    (*somePerson).age = 65;

    return 0;
  }
  

But, this synax is ugly -- especially for something so common. One goal of any programming language is to make the common case convenient. And, in this case, the C Language does exactly that. It defines an arrow operator formed by combining a -dash and >-greater than sign: >-. This notation is nothing more than shorthand for the star-and-dot notation. Some compilers implement the arrow-operator as a first-class language feature, whereas others use the preprocessor to, before compilation-proper, convert the "shortcut" arrow notation to the star-and-dot notation. By way of example, the code below is exactly equivalent to the prior example above:

  int someFunction (struct person_t *somePerson) {
    strcpy (somePerson->fname, "Greg");
    strcpy (somePerson->lname, "Kesden");
    somePerson->age = 65;

    return 0;
  }
  

typedef

The C Language also provides a mechanism for creating new type names from old ones. This is done using a typedef as in the example below:

  typedef unsigned int uint;
  uint number1;
  unsigned int number2;
  number1 = number2;
  

"typedef" is a keyword and begins the definition. The last string on the line is the identifier, a.k.a name, of the new type. Everything in between is the description of the type using its old defintion. So, the example above defines a "uint" to be the same thing as an "unsigned int".

"typedef" is very useful in symplifying the use of structs. Notice that in every declaration of a "struct", whether as a variable or parameter, we were required to use the keyword "struct". Consider again one of the examples from above. Notice the presence of the keyword "struct". We declared the paramter to be "struct person_t *somePerson", not just "person_t *somePerson":

  int someFunction (struct person_t *somePerson) {
    strcpy (somePerson->fname, "Greg");
    strcpy (somePerson->lname, "Kesden");
    somePerson->age = 65;

    return 0;
  }
  

We can use a typedef to clean this up by defining a new type name for the struct. Consider the following definition:

  struct person_t {
    char fname[256];
    char lname[256];
    unsigned int age;
  };

  typedef struct person_t person;
  

We can now just use the type "person", in place of the type "struct person_t". Consider the revised example below:

  int someFunction (person *somePerson) {
    strcpy (somePerson->fname, "Greg");
    strcpy (somePerson->lname, "Kesden");
    somePerson->age = 65;

    return 0;
  }
  

We can actually revise our definition of the struct to contract the typedef and the struct definition as below:

  typedef struct person_t {
    char fname[256];
    char lname[256];
    unsigned int age;
  } person;
  

It is really ugly, but notice that this exactly follows the sytax of a typedef that we've seen before. To see this more easily, we can rewrite it with the meaningless whitespace formatted differently -- the "typedef" keyword, followed by the type definition, followed by the new type name.

  typedef struct person_t { char fname[256]; char lname[256]; unsigned int age; } person;
  

But, putting aside the funny spacing, we can actually simplify the definition a bit more. Since we'll be using the new type name, "person", we won't actually need to refer to it as a "struct person_t". As a result, we can use an anonymous struct within the typedef. Basically, the syntax stays the same -- except the struct identify, person_t, can go away:

  typedef struct {
    char fname[256];
    char lname[256];
    unsigned int age;
  } person;