CSC 101 Lecture Notes Week 9
Pointers and Dynamic Memory Allocation
More on Programming in the Large

Relevant Reading: Chapter 14 Sections 14.1 and 14.2; Chapter 13 (again)





  1. Review of Pointer Uses So Far

    1. The beginning of Chapter 14 has a summary of how pointers have been used so far in CSC 101.

    2. Function output parameters

      1. A formal output parameter is declared as a pointer, using *.

      2. The actual parameter in a call to the function uses the address-of operator &.

      3. For an example of this, see the discussion of the exchange function in Notes Week 8, in particular the picture of memory in Notes 8, Figure 2

    3. Array variables, including strings, are pointers to blocks of memory.

      1. An array can be declared with a fixed size, with the name of a the array being a pointer to a piece of memory of that size.

      2. An array function parameter can be declared without a fixed size, which means it's a pointer to the memory supplied in the actual array parameter.

      3. For and example, see the discussion of the read_values function Notes Week 6, in particular the picture of memory in Notes Week 6, Figure 1.

    4. File access

      1. A variable declared as FILE * is a pointer to file information.

      2. File functions like fscanf use these pointers.

      3. See the lab 5 writeup and solutions to array_math.c and read_strings.c .


  2. Pointers to Dynamically Allocated Memory

    1. The final topic related to pointers is the use of dynamically allocated memory.

      1. This allows a program to allocate amounts of memory that are not static when the program is written, but rather dynamically determined when the program is run.

      2. For example, instead of allocating an array of a fixed size for computing statistics of up to, say 10000 numbers, a program can instead do this:

        1. Ask the user at the beginning of a program how many numbers to compute statistics for.

        2. Allocate an array that's big enough to hold that many numbers, no more and no less.

    2. There are two functions in C to allocate memory dynamically -- malloc and calloc, which stand for memory allocate and contiguous memory allocate.

    3. The function malloc allocates new memory.

      1. It works for any type of value.

      2. The general form of a to call malloc is this:
        (type) malloc(sizeof(type))

      3. E.g.,
        int* nump;
        char* str;
        Planet* p;
        
        nump = (int*) malloc(sizeof int);
        str = (char*) malloc(sizeof char);
        p = (Planet*) malloc(sizeof Planet);
        

    4. Dynamically allocated arrays with calloc.

      1. The function calloc allocates arrays of memory.

      2. The general form is:
        (type)calloc(sizeof(n, type))

        where n is the number of array elements.


  3. Examples of Dynamic Memory Allocation

    1. As an example of where dynamic memory allocation can be particularly useful, let's look at a program that reads names from a file.

      1. It uses malloc to allocate strings of particular dynamic sizes.

      2. It also uses calloc to allocate the array that holds a list of strings, where the size of the array is determined dynamically.

    2. Here's the example, from the file 101/examples/pointers-malloc/read-names.c ; the comments in the code further explain and motivate the use of dynamic memory allocation; the key parts of the code are shown in green text.
      /****
       *
       * This program illustrates the dynamic allocation of string memory.  It uses
       * malloc to allocate strings of a proper size.  It uses calloc to allocate an
       * array big enough to hold the number of strings that the program reads in.
       *
       * The program takes two command-line arguments: the number of names to be read
       * and the file from which to read the names.  Each line of the file is
       * considered to be a separate name.  The length of a name can be up to 80
       * characters.  Any name longer than 80 characters is truncated to 80
       * characters.  Avoiding this truncation would require additional logic that
       * would overly complicate this example.
       *
       */
      
      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      
      #define MAX_NAME_LEN 81
      
      int main(int argc, char** argv) {
          int num_names;              /* Number of names to read */
          char* filename;             /* Name of file to read from */
          FILE* file;                 /* File to read from */
          int i;                      /* Loop index variable */
          char name[MAX_NAME_LEN];    /* One name read from the file */
          char* name_sized;           /* Dynamically sized name string */
          char** names;               /* Array of all names read from the file */
          char* fstatus;              /* return value of fgets */
      
          /*
           * Check that there are two command-line args.
           */
          if (argc < 2) {
              printf("The program requires two arguments -- number and file.\n");
              return 1;
          }
      
          /*
           * Get the number of names to read and the name of the file to read from.
           * These are supplied by the program user in the first and second
           * command-line arguments.  Since argv is a string array, we need to call
           * atoi on argv[1], to convert it from a string to an int.  The filename in
           * argv[2] is good as a string.  This kind of command-line arg processing
           * was done in lab 7.
           */
          num_names = atoi(argv[1]);
          filename = argv[2];
      
          /*
           * Open the file, exiting of not openable.
           */
          if ((file = fopen(filename, "r")) == NULL) {
              printf("Cannot open file %s.\n", filename);
              return 1;
          }
      
          /*
           * Allocate a string array big enough to hold the number of names to be
           * read from the file.  This allocation is one of the main ideas of this
           * example.  It allows us to use an array of exactly the right size, rather
           * than declaring an array that tries to be big enough for any
           * eventuality.
           */
          names = (char**) calloc(num_names, sizeof(char*));
      
          /*
           * Read names from file, up to num_names or EOF, whichever occurs first.
           */
          for (i = 0, fstatus = fgets(name, MAX_NAME_LEN, file);
               i < num_names && fstatus != NULL;
               i++, fstatus = fgets(name, MAX_NAME_LEN, file)) {
      
              /*
               * Allocate a string just the right size for name.  This allocation is
               * the other main idea of this example.  It allows us to allocate
               * string storage that's exactly the size we need.  What we avoid is
               * having every name stored in a string of size MAX_NAME_LEN, when that
               * would be quite wasteful of space in most cases.
               */
              name_sized = (char*) malloc(strlen(name + 1));
              strcpy(name_sized, name);
      
              /*
               * Store the sized name in the names array.
               */
              names[i] = name_sized;
          }
      
          /*
           * Update the value of num_names to be the actual number of names read in,
           * in case it's different than the command-line arg value.
           */
          num_names = i;
      
          /*
           * Print out array to confirm that names were properly read.
           */
          for (i = 0; i < num_names; i++) {
              printf("%s", names[i]);
          }
      
          return 0;
      }
      

    3. It can be instructive to compare this code with a version of the program that does not use dynamic memory allocation, in 101/examples/pointers-malloc/read-names-non-dynamic.c ; differences with the dynamic version are shown in red.
      /****
       *
       * This is a version of the read-names program that does not use dynamic memory
       * allocation.  It's provided for comparision purpose, to illustrate the
       * difference between static and dynamic allocation of arrays.
       *
       * The program takes two command-line arguments: the number of names to be read
       * and the file from which to read the names.  Each line of the file is
       * considered to be a separate name.  The length of a name can be up to 80
       * characters.  Any name longer than 80 characters is truncated to 80
       * characters.  Avoiding this truncation would require additional logic that
       * would overly complicate this example.
       *
       */
      
      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      
      #define MAX_NAME_LEN 81
      #define MAX_NAMES 1000
      
      int main(int argc, char** argv) {
          int num_names;              /* Number of names to read */
          char* filename;             /* Name of file to read from */
          FILE* file;                 /* File to read from */
          int i;                      /* Loop index variable */
          char name[MAX_NAME_LEN];    /* One name read from the file */
          char names[MAX_NAMES][MAX_NAME_LEN];
                                      /* Array of all names read from the file */
          char* fstatus;              /* return value of fgets */
      
          /*
           * Check that there are two command-line args.
           */
          if (argc < 2) {
              printf("The program requires two arguments -- number and file.\n");
              return 1;
          }
      
          /*
           * Get the number of names to read and the name of the file to read from.
           * These are supplied by the program user in the first and second
           * command-line arguments.  Since argv is a string array, we need to call
           * atoi on argv[1], to convert it from a string to an int.  The filename in
           * argv[2] is good as a string.  This kind of command-line arg processing
           * was done in lab 7.
           */
          num_names = atoi(argv[1]);
          filename = argv[2];
      
          /*
           * Open the file, exiting of not openable.
           */
          if ((file = fopen(filename, "r")) == NULL) {
              printf("Cannot open file %s.\n", filename);
              return 1;
          }
      
          /*
           * Check that the number of names to be read is <= MAX_NAMES.  If it's not,
           * set it to MAX_NAMES.  Compare this code to the use of calloc in
           * read-names.c.
           */
          if (num_names >= MAX_NAMES) {
              printf("Of the %d names requested, only the first %d will be read.\n",
                  num_names, MAX_NAMES);
              num_names = MAX_NAMES;
          }
      
          /*
           * Read names from file, up to num_names or EOF, whichever occurs first.
           */
          for (i = 0, fstatus = fgets(name, MAX_NAME_LEN, file);
               i < num_names && fstatus != NULL;
               i++, fstatus = fgets(name, MAX_NAME_LEN, file)) {
      
              /*
               * Copy the name just read into the ith array element.  Compare this
               * code to the use of malloc in read-names.c.
               */
              strcpy(names[i], name);
          }
      
          /*
           * Update the value of num_names to be the actual number of names read in,
           * in case it's different than the command-line arg value.
           */
          num_names = i;
      
          /*
           * Print out array to confirm that names were properly read.
           */
          for (i = 0; i < num_names; i++) {
              printf("%s", names[i]);
          }
      
          return 0;
      }
      


  4. Memory Pictures Comparing Dynamic Versus Static Names Arrays

    1. Suppose a text file called "names" has the following contents:
      Able
      Jones
      Smith
      Wallace
      

    2. Figure 1 shows the memory layout of the dynamically allocated names array created by the read-names.c program.


      Figure 1: Dynamically allocated memory.



    3. In contrast, Figure 2 shows the memory layout of the statically allocated names array created by the read-names-non-dynamic.c program.


      Figure 2: Statically allocated memory.





    4. There's definitely a ton of wasted space in the static memory layout compared to the dynamic one. (OK, so "ton" is not a particularly accurate measure of memory space. How about a "boatload"? Doesn't cut it either?? OK, how about up to MAX_NAME_LEN * MAX_NAMES - strlen("Able") - strlen("Jones") - strlen("Smith") - strlen("Wallace") - 4 - 4 * sizeof(char*) = 80943 bytes, where the "up to" qualification accounts for any extra space that malloc may return beyond the specific number of characters requested.)


  5. Another Pointer Example

    1. Lecture Notes Week 7 has an example program that uses C structures to define Planet and SolarSystem data types.

    2. That program can be improved by using pointers to structures within the SolarSystem array, and for function parameters of type Planet and SolarSystem.

    3. The code is in the examples/pointers-malloc directory, in the same set of files that are defined without pointers in the examples/structs directory.

    4. It's instructive to compare the two implementations, with and without pointers.

    5. It's also instructive to note that this program is a good example of C "programming in the large".

      1. This particular example is not all the large in size.

      2. However, it does illustrate how large C programs are typically organized:

        1. .h header files contain program type definitions and function prototypes

        2. .c implementation files include the header files and define the function implementations

        3. -test.c testing files define a main function, plus additional functions that test the implementation

    6. Here are the files:
      • planet.h -- the type definition for Planet plus prototypes for functions that operate on planets
      • planet.c -- the implementation of the functions declared in planet.h
      • planet-test.c -- a testing program that has a main function that calls the functions defined in planet.c

      • solar-system.h -- the type definition for SolarSystem plus prototypes for functions that operate on solar systems
      • solar-system.c -- the implementation of the functions declared in solar-system.h
      • solar-system-test.c -- a testing program that has a main function that calls the functions defined in solar-system.c and planet.c

    7. We will go over some details of these files during lecture in class.