CSC 161 Grinnell College Spring, 2009
 
Imperative Problem Solving and Data Structures
 

Laboratory Exercise on Pointers in C

Goals

This laboratory exercise provides practice with basic elements of pointers, addresses, values, and memory allocation in C.

Prerequisites:

Basic control structures, arrays, and strings in C.

Contents:

  1. Printing Memory Addresses
  2. Writing a Swap Function
  3. Allocating and Freeing Memory
  4. Memory Leaks and Other Problems

Steps for this Lab

Part A: Printing Memory Addresses

  1. Write a short C program that declares and initializes (to any value you like) a double, an int, and a string. Your program should then print the address of, and value stored in, each of the variables. Use the format string "%p" to print the addresses in hexadecimal notation (base 16). You should see addresses that look something like this: "0xbfe55918". The initial characters "0x" tell you that hexadecimal notation is being used; the remainder of the digits give the address itself.

  2. Since hexadecimal needs 16 digits (to represent digits 0-15), we use 0-9 and also a-f. Note that a single hex character can express the same values that a 4-bit "nibble" can. How many hex characters are needed to express a single byte? How many bytes are used to store each address? Are your variables located in the first or the second half of the set of all possible memory addresses?

  3. Draw a small memory diagram showing the location of each of the variables in your program. (You need not convert the hex to decimal; just label your drawing in hex.) Are they allocated in the same order that you declared them? Is there any empty space between them?

  4. Modify your program by rearranging the variable declarations and/or changing the length of the string. (In particular, try a string that uses 5 or 7 bytes, including the null terminator.) Does this change the results you got previously?

The take-home message:

Small changes within a program can change how memory is laid out for a given program. The compiler will try to arrange memory for optimal performance, and this may include aligning variables with 4-byte boundaries. For C programmers, this can sometimes mean that a program which appears to work correctly (but in fact overwrites the end of an array), can suddenly stop working due to seemingly innocuous changes -- for example, changing the order in which variables are declared.

Part B: Writing a Swap Function

  1. Write a function that accepts two variables of the same data type and swaps their values. Then add a "driver" function (i.e., main) to test your swap routine. Does your function work as you expected?

  2. Note that the function will not work if you pass the variables themselves. If your function does not work, modify it such that you pass it the addresses of the variables you wish to swap. Using this approach, you should be able to get it to work correctly.

Part C: Allocating and Freeing Memory

You should have a C program, from a previous laboratory, Structures in C, in which you defined a new data type timeinfo_t and wrote some functions that operate on variables of that data type. In this exercise, you will modify that program.

  1. Locate and review that program.

  2. In your program from Step 1, write a function with the following prototype:

       timeinfo_t* create_time(double secs);
    

    Your function should use dynamic memory allocation to allocate the memory needed to store a timeinfo_t variable, load the memory based on the function's argument secs, and return a pointer to that memory. You may find that your previous function convertTime contains useful code for this operation.

    As you probably recognize, you have just written (the C pre-cursor to) a constructor for your timeinfo_t data type. Modify your main function to test create_time.

  3. Recall that you have been warned in the past against allocating memory inside a function and returning a pointer to it. Yet that is what you have been asked to do in this exercise. Why is doing so acceptable now, when it has not been in the past?

In C there is no automatic garbage collection. Therefore, the programmer who allocates memory on the heap (via dynamic memory allocation) is also responsible for freeing that memory when it is no longer needed.

  1. Write a function with the prototype

       void free_time(timeinfo_t* t)
    

    that frees the memory pointed to by t. Your function should state the pre-condition that the memory pointed to by t was, in fact, allocated on the heap.

    In C++, such a function is called a "destructor." Java has no counterpart for this function since the JVM collects "garbage" automatically.

    Once your "destructor" is complete, be sure to call it from your main function to clean up the memory allocated during your testing. Run your program again to make sure all is well.

  2. Write a program that dynamically allocates a chunk of memory large enough to store 6 integers. Then prompt the user the enter 6 integers and store them in your newly-allocated memory. Finally, print the integer values in reverse order. (Recall that the close correspondence between pointers and arrays in C allows you to treat the pointer returned by malloc as the name of a 6-element int array.)

    Did you remember to free the memory you allocated? If not, please add this to your program.

Part D: Memory Leaks and Other Problems

The next several exercises give you some experience with common coding mistakes and the error messages that result.

  1. Add a second call to free_time in your main function. Run the program to see the result, then remove the offending call.

    In main allocate a timeinfo_t variable statically (i.e., on the stack). Then allocate a timeinfo_t* and point it at your statically-allocated variable. Finally, try to free the statically-allocated memory by calling free_time with your new pointer (and then remove the offending call).

  2. Consider the following program.

    #include <stdio.h>
    #include <stdlib.h>
    
    #define FALSE 0
    #define TRUE  1
    
    int main() {
      int done = FALSE;
      int j=0;
      
      while (!done) {
        int n = 10000000;
        int* a = (int*)malloc(n * sizeof(int));
    
        int i;
        for (i=0; i < n; i++)
          a[i] = i;
        
        j++;
        printf("%d\n", j);
      }
    
      return 0;
    }
    
    1. What is wrong with it? What do you expect it to do when run?

    2. Now copy the program and run it. On my machine, it prints numbers up to around 80 before it crashes. How about yours? Do you understand why it crashes?

    3. Add the following code immediately after the malloc call to confirm your understanding. The library function perror(), declared in stdio.h, prints a message regarding the most recent error that occurred in any system or C library call. Thus, with this placement, perror will print any error that may occur related to malloc. (We will discuss system calls later in the course.)

      
          if (!a) {
            perror(NULL);
            exit(1);
          }
      

      If you still are not sure why the error occurred, please ask.

In the next few exercises, you will experiment with a (non-GNU) Linux tool named Valgrind that can detect and report on several types of errors related to dynamic memory management. Actually, Valgrind is a suite of debugging tools; the specific Valgrind tool we will use is called Memcheck. According to the documentation at http://valgrind.org, Valgrind is pronounced with a short i (like grinned), and the origins of the name are related to Norse mythology.

  1. Modify your program from the previous exercise so that it allocates (and fails to free) only ten arrays or so. Then build your program with a command like the following. Note that the -g option is necessary; Valgrind needs the debugging information it adds to the executable code.

    
      gcc -Wall -g myprog.c
    

Valgrind is a "virtual machine", which means that you will run Valgrind, and it will invoke your executable code line by line. This allows it to monitor your use of memory and report related errors. It also adds a lot of overhead, so you may notice that it runs slowly.

  1. Run your program with Valgrind, using a command like the following. (For future reference, if your program takes command-line arguments, you can simply add these to the end of the command line.)

      valgrind --leak-check=yes ./a.out
    
    

    Your output should include some header information about Valgrind, then the output of your program, and then some diagnostic information about the memory leak.

    Do not be misled by the line that says "ERROR SUMMARY: 0 errors from 0 contexts". This apparently relates to specific error types. Continue reading, and you should see "malloc/free: 10 allocs, 0 frees" and also the following.

      ==22813== LEAK SUMMARY:
      ==22813==    definitely lost: 0 bytes in 0 blocks.
      ==22813==      possibly lost: 400,000,000 bytes in 10 blocks.
    
  2. Modify your code from the previous exercise to free the memory you have allocated. Note that you will need a call to free in each loop iteration, so that you can free the memory before you lose the pointer to it!

    Now rebuild your code, and run it with Valgrind to see the improved output message.

  3. In this exercise, you will experiment with a few more memory-related errors Valgrind can catch.

    1. Add an extra call to free() somewhere in your program. Then rebuild your program and take a look Valgrind's output. (After you have done so, remove the offending call again.)

    2. Another common error that Valgrind can catch is accessing memory after it has been freed. To test this, you can add statements such as the following immediately after your call to free(). Go ahead and try it, noting that Valgrind tells you the line numbers where the errors occur, and then remove the offending code.

        a[0] = 5;
        printf("a[0]=%d\n", a[0]);
      
    3. Valgrind can also tell you when you access elements that are out-of-bounds of an allocated memory block. Modify your program to test this, noting what information Valgrind gives you about the error. (Then remove the error afterwards.)

      Unfortunately, Valgrind can not detect out-of-bounds errors with statically allocated arrays. It can only do this for dynamically-allocated memory.

  4. Optional: For those with extra time, please take a look at the on-line documentation for Valgrind: http://valgrind.org. In particular, I suggest reading quickly through the "Quick Start" information, and also Sections 4.1 and 4.3 in the "User Manual".


This document is available on the World Wide Web as

     http://www.walker.cs.grinnell.edu/courses/161.sp09/lab-pointers.shtml

created 27 March 2007 by Marge Coahran
revised 1 April 2008 by Marge Coahran
revised 11 April 2008 by Henry M. Walker
last revised 25 January 2009 by Henry M. Walker
Valid HTML 4.01! Valid CSS!
For more information, please contact Henry M. Walker at walker@cs.grinnell.edu.