CSCI 262 Data Structures

Spring 2018

Lab 5: Memory

(Back to Labs)

Goals


Overview


This lab will walk you through some exercises concerning pointers, arrays, and dynamically allocated memory. As usual, we ask that you work with a partner or a team. If you end up solo, please raise your hand and we'll pair you with someone else. You are welcome to change partners throughout the semester!


Instructions


Step 0: Go ahead and create a project and source file for this lab. Include the iostream library, and create a main() function. You'll be pasting or typing in fragments of code and running them as we go. You don't need to save the code or turn it in at the end, so consider this project your playground for exploring and learning about pointers, arrays, and dynamically allocated memory!

When you are ready to build and run your code, you can compile it using:

    g++ -o lab05 lab05.cpp

(assuming your source file is called lab05.cpp). You can then run your program using

    ./lab05

Step 1: pointers and arrays

First, create an array of 5 integers, e.g.

    int arr[5];

Use a for loop (the usual indexing into arr will work, as will a range-based for loop) to print out the contents of the array. Was the result what you expected?

Write a for loop to set the values in the array to 0, 1, 2, 3, and 4.

Next, create a variable which is a pointer to int and assign to it the array variable (recall from lecture that this is correct C++ code, as array variables and pointers are mostly interchangeable - we are about to explore some small ways in which they are not identical):

    int* p = arr;

Use a for loop to print out the contents of the array using p rather than arr. Note that a range-based for loop will not work on your pointer, as the compiler doesn't "know" that your pointer is pointing to a fixed-size array, and thus it has no way of determining the begin and end points of the memory pointed to. Instead, treat your pointer as an array whose size you know to be 5, e.g, use p[i] to get the ith element of the array.

Finally, print out the addresses represented by your array and your pointer, e.g.:

    cout << arr << endl;
    cout << p << endl;

So the above work was just to demonstrate and reinforce what we discussed in lecture, that array variables are secretly pointers and that pointers can be treated as array variables (assuming they are actually pointing to an array!) We've seen one difference between them already, though - you can't use range-based for loops on the pointer like you can on the array variable.

Next, let's use the sizeof operator to see how much memory is being used by your array. sizeof is a unary prefix operator, but most people write it as if it were a function; either way, it evaluates to an unsigned integer giving the size of the variable in bytes. Print out the size of your array in memory:

    cout << sizeof(arr) << endl;

Was this result what you expected? Remember that each int takes up multiple bytes in memory. Using what you know about the size of the array, how large (in bytes) must an int be? You can confirm the size of an int, by the way, by using the sizeof operator this way:

    cout << sizeof(int) << endl;

What happens if you use sizeof to ask the size of your pointer? Is this what you expected?

Now take a moment to play with the following snippets of code. These all do the exact same thing, which you can confirm in your project:

    for (int i = 0; i < 5; i++) arr[i] = i;
    for (int i = 0; i < 5; i++) p[i] = i;
    for (int i = 0; i < 5; i++) *(p + i) = i;

The last one is taking advantage of pointer arithmetic. Try out this code, then answer question 2:

    p = arr;
    p++;
    cout << arr << endl;
    cout << p << endl;
    cout << (p - arr) << endl;

Were the results what you expected? It is important to remember the rules of pointer arithmetic; adding n to a pointer does not modify the underlying address by n, but rather by the amount that would be required to move the pointer n times in an array of the type of the pointer. Conversely, when we compare two pointers, as in the last line above, C++ takes into account the size of the type being pointed to.

Finally, find out the theoretical (maximum) size of memory for your computer program. First, find out the size of a pointer:

    cout << sizeof(p) << endl;

This gives the size of a pointer in bytes. Since a pointer is an unsigned value, your program can theoretically address a maximum of 2(8*sizeof(p)) bytes. This number is unwieldy to work with, so typically we divide by some power of 1024 (210) to get a value in kilobytes, megabytes, gigabytes, etc. (Actually, the prefixes are somewhat ambiguous, since some use these to mean powers of 1000 instead of powers of 1024. There are alternate prefixes which refer specifically to powers of 1024, although these are not as commonly used in everyday speech. See https://en.wikipedia.org/wiki/Binary_prefix for more info.)

Step 2: dynamic allocation

In step 1, we only worked with a static array and pointers. When the amount of memory we need is only determined at runtime, we cannot use a static array, and instead must use dynamic allocation with the new operator.

For your first task, you are going to estimate how much memory you can actually use from inside your C++ program, on your current computer, given your current compiler and memory architecture. First, note that you can easily allocate n bytes of memory by simply using new to create a dynamically allocated array of char of size n (since chars are 1 byte large in C++). To start with, consider the following program:

    size_t n;
    cout << "How many gigabytes do you want? ";
    cin >> n;
    
    char* p = new char[n * 1024L * 1024L * 1024L];
    cout << "Success at " << n << " gigabytes!" << endl;
    delete[] p;

Try using this program to estimate the amount of memory you can address, in gigabytes.

If you find typing and guessing tedious, then rewrite the program above to keep trying to allocate increasing amounts in a loop, printing out successes as it goes. Just make sure you deallocate your memory using delete[] each time within the loop, or your calculation will be off!

If you don't want your program simply crashing (in this case, due to an uncaught exception - since we don't cover exceptions in this class) you can instead write your code using the std::nothrow constant passed as a parameter to the new operator. With this usage, the returned pointer will be NULL if the requested memory cannot be allocated: just make sure you test the returned pointer!

    size_t n;
    cout << "How many gigabytes do you want? ";
    cin >> n;
    
    char* p = new(std::nothrow) char[n * 1024 * 1024 * 1024];
    if (p != NULL) {
        cout << "Success at " << n << " gigabytes!" << endl;
        delete[] p;
    } else {
        cout << "Failed at << n << " gigabytes!" << endl;" 
    }   

Step 3: arrays of objects

Add the following class declaration to your program:

    class foo {
    public:
        int x, y;
    };

Using what you know now of sizeof, find out how big an object of type foo is, and how big an array of 10 foo objects is. Does the answer meet your expectations?

Now, try this class:

    class bar {
    public:
        int x, y;
        char c;
    };

How big do you think a bar object or array should be? Test it using sizeof. Was the answer what you were expecting?

If you are confused, here is the explanation: modern computer memory is set up so that memory operations work most efficiently when the memory being operated on is aligned along word boundaries. A word in computers is a processor-dependent number of bits, usually the number of bits the processor can work on in one operation. (Typically this is larger than one byte.) In the processors most of us use, the word size is a bit muddled, as the processor can efficiently operate on 64-bit words, but can also work on more than one 32-bit word at a time. So, for our purposes, let's say the word is 32-bits (4 bytes).

What the compiler does for us, then, is pad our bar object with just enough unused bytes to make sure that bar objects are some integer multiple of a word in size, so that we can efficiently operate on, say, arrays of bar objects.


Wrapping Up


To complete this assignment, please complete the quiz for this lab in Canvas.