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!

Step 1: Pointers and References

In step 1 we verify some things we learned about pointers in class. The code below will guide you through working with the address-of operator (&) and the dereference operator (*). We'll also take a side trip into reference parameters.

Start by creating the function below - remember to put it above main(), or make a prototype for it:

    void doit() {
        double d;
        cout << d << endl;
    }

Call this function from main and see what prints out. Is it what you expected? Depending on your compiler and some other factors, it is possible that you saw a zero, but more likely you saw some random value. This happens because C and C++ do not initialize base type variables with any standard value; instead, they simply assign the next available memory in the stack to the variable. If you do not initialize the variable yourself, then whatever garbage was already in the stack at that memory location is still there!

Next, try the following (in main() or wherever you want - you can delete the previous code):

    int x = 0;
    int* p = &x;

    cout << "&x: " << &x << endl; 
    cout << "p: " << p << endl;
    cout << "x: " << x << endl;
    cout << "*p: " << *p << endl << endl;

    x = 10;

    cout << "&x: " << &x << endl; 
    cout << "p: " << p << endl;
    cout << "x: " << x << endl;
    cout << "*p: " << *p << endl << endl;

    *p = 20;

    cout << "&x: " << &x << endl; 
    cout << "p: " << p << endl;
    cout << "x: " << x << endl;
    cout << "*p: " << *p << endl << endl;

Observe how p can be dereferenced to tell us what x is, and to assign to x. Also observe that p (and the address of x) remain unchanged through all of this. Take some time and experiment with this code, e.g, by adding a variable y, and having p point to x some of the time and y some of the time. Ask if you still have any questions about what a pointer is!

Step 2: pointers and arrays

You can again discard the previous code if you wish. Now, create an array of 5 integers, e.g.

    int arr[5];

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!)

    int* p = arr;

Use a for loop to print out the contents of the array using p rather than arr. (Use p[i] to get the ith entry in 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!)

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? Answer questions 1 and 2 in the quiz for this lab in Canvas.

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:

    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 an integer 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 (and answer question 3):

    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 3: 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;" 
    }   

Tell us how much memory (approximately, in gigabytes) your program can access (question 4)!

Step 4: pointers, dynamic allocation, and 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 (answer question 5). 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? 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.

Finally, practice with dynamic allocation and the pointer operator (->) using foo or bar - play around with the following to build your intution and understanding:

    // make a dynamically allocated foo object
    foo* f = new foo;  

    f->x = 42;     // equivalent, recall, to (*f).x = 42
    f->y = 17;

    cout << f->x << ", " << f->y << endl;

    foo g = *f;    // copies foo into a locally allocated foo object
    cout << g.x << ", " << g.y << endl;

Wrapping Up


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