CSC 161 Grinnell College Fall, 2011
 
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. Arrays are Pointers too
  4. Allocating and Freeing Memory
  5. 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 "%u" to print the addresses as unsigned (32-bit non-negative) integers.

    Hint: Remember that you can use the & character to find addresses.

    Reminder: 1 byte = 8 bits, and a 32-bit integer requires the space of 4 bytes.

  2. Draw a small memory diagram showing the location of each of the variables in your program. Are they allocated in the same order that you declared them? Is there any empty space between them?

  3. 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 (by value) of the same data type of your choice (e.g. int or double) and tries to swap 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: Arrays Names are Pointers

  1. Copy the program getObstaclePercentPointers.c and open it in an editor.

  2. Read the documentation for the getObstaclePercent function (stated above the function itself), and complete the function so it operates as specified.

    * Remember: The name of an array is simply a pointer to the first element of the array.

Part D: Allocating and Freeing Memory

You should have a C program from Laboratory Exercise on Insertion Sort, in which you worked with the pixel data type Pixel and wrote a function to sort pixels in a two dimentional pixel array. In this exercise, you will modify a slightly more efficient version of the pixel sorting program which uses a quick sort rather than insertion sort: sort-picture.c.

Recall that in the insertion sort program from step 5 of the insertion sort lab, you wrote Pixels to an array from a Picture * using the rGetPicturePixel function. In this implementation, the pixelArray is pre-allocated on the program stack. This works for small pictures. However, a more practical implementation is to dynamically allocate memory off the heap for the pixel array. Because of the limited, and operating system dependent, size of a process's stack, allocating memory allows the program to scale and work on much larger pictures.

  1. Retrieve a copy of the quick sort picture program, sort-picture.c, and place it in your working directory.

    * Note this is the same program you wrote but it uses quick sort instead of insertion sort. Can you guess why...

  2. In your program from Step 9, change the prototype for picToPixArray so that it returns a pointer to a dynamic memory location instead of taking in a pointer to a pre-allocated array.

    Pixel * picToPixArray(Picture * pic);

    Your function should use dynamic memory allocation to allocate the memory needed to store the entire pixel array, load the memory based on the function's argument pic *, and return a pointer to that memory. Your previous function should still do the same thing but create its own array rather than use one that is passed in.

    As you probably recognize, you have just written (the C pre-cursor to) a constructor for a pixel array. Modify your main function to incorporate the new implementation.

  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, that is the program getting rid of dynamic memory that was allocated to the heap, but is not used anymore. 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. Modify the program sort-picture.c so that it deallocates all dynamic storage before it exits. Remember that the memory pointed to by p was allocated on the heap.

    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. Finally, notice at the bottom the function pixelCompare takes two Pixels and compares their value returning true or false depending on the result. Imagine a world where Pixels were very large types and it would be cumbersome to pass two of them by value to pixelCompare. Alter the function to operate on pointers to Pixels (Pixel *) instead.

    Note: You might want to use the -> shortcut.

    Reminder in using structs and pointers:

    • Recall with MyroC, a Pixel is defined as
         typedef struct Pixel {
            unsigned char R;
            unsigned char G;
            unsigned B;
         } Pixel;
      
    • Now suppose variable Pix is defined as a pointer to a Pixel:
         Pixel * pix;
      
    • Let's assume pix has been initialized.

      Expression Meaning
      pix a pointer to a Pixel struct
      *pix the Pixel struct itself
      (*pix).R the R field of the Pixel struct
      pix->R an alternate way to reference the R field of the Pixel struct

  3. 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 from part c that the close correspondence between pointers and arrays in C. This correspondence 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.

Extra Credit — Turn this in within 2 weeks to receive extra credit!

Part E: 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_pixArray in your main function. Run the program to see the result. What happens and why?.

    Remove the second call so the program functions normally again.

    In main allocate a Pixel variable statically (i.e., on the stack). Then allocate a Pixel * and point it at your statically-allocated variable. Finally, try to free the statically-allocated memory by calling free_pixArray with your new pointer. Run the program and explain what happens.

    After you have an explanation, remove the bad instructions so your program operates normally once again.

  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 -o myprog -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 ./myprog

    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. For those interested: 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".

Reminder: Complete Evaluation Form

When you have finished this lab, be sure to fill out its evaluation form in the "Lab Evaluation" section for CSC 161 on Pioneer Web.