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!
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
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.
To complete this assignment, please complete the quiz for this lab in Canvas.