Section 5. Pointers


Section materials curated by our head TA Chase Davis, drawing upon materials from previous quarters.

This weekā€™s section exercise delve deep into the details of pointers and memory management in C++. This is an opportunity to get down and dirty in the nitty gritty of things, and to get closer towards gaining ultimate power over your computer! The topics covered in section this week will show up on the next assignment.

Remember that every week we will also be releasing a Qt Creator project containing starter code and testing infrastructure for that week's section problems. When a problem name is followed by the name of a .cpp file, that means you can practice writing the code for that problem in the named file of the Qt Creator project. Here is the zip of the section starter code:

šŸ“¦ Starter code

1) A Series of Unfortunate References

Topics: Pointers and references

What is the output of the following code snippet?

void VFD(int *duncan, int isadora, int& quigley) {
	isadora += *duncan;
	quigley *= isadora;
	(*duncan) = 1;
	isadora = *duncan;
}

int main() {
	int sunny = -6;
	int klaus = 21;
	int violet = 2;
	VFD(&sunny, violet, klaus);
 	cout << sunny << " " << klaus << " " << violet << " " << endl; 
 	return 0;
}

1Ā -84Ā 2


2) Pointed Points about Pointers

Topics: Arrays and pointers

Pointers to arrays are different in many ways from ADTs in how they interact with pass by-value and the = operator. To better understand how they work, trace through the following program. What is its output?

void print(int* first, int* second) {
	for (int i = 0; i < 5; i++) {
		cout << i << ": " << first[i] << ", " << second[i] << endl;
	}
}
void transmogrify(int* first, int* second) {
	for (int i = 0; i < 5; i++) {
		first[i] = 137;
	}
}
void mutate(int* first, int* second) {
	first = second;
	second[0] = first[0];
}
void change(int* first, int* second) {
	first = new int[5];
	second = new int[5];
	for (int i = 0; i < 5; i++) {
		first[i] = second[i] = 271;
	}
}
int main() {
	int* one = new int[5];
	int* two = new int[5];

	for (int i = 0; i < 5; i++) {
		one[i] = i;
		two[i] = 10 * i;
	}

	transmogrify(one, two);
	print(one, two);
	
	mutate(one, two);
	print(one, two);
	
	change(one, two);
	print(one, two);
	
	delete[] one;
	delete[] two;
	return 0;
}

The output of the program is shown here:

0: 137, 0
1: 137, 10
2: 137, 20
3: 137, 30
4: 137, 40
0: 137, 0
1: 137, 10
2: 137, 20
3: 137, 30
4: 137, 40
0: 137, 0
1: 137, 10
2: 137, 20
3: 137, 30
4: 137, 40 

Remember that when passing a pointer to a function, the pointer is passed by value! This means that you can change the contents of the array being pointed at, because those elements aren't copied when the function is called. On the other hand, if you change which array is pointed at, the change does not persist outside the function because you have only changed the copy of the pointer, not the original pointer itself.


3) The Hogwarts School of Pointers and Memory

Topics: Pointer tracing and memory diagrams

Trace through the following function and draw the programā€™s memory at the designated spot. Indicate which variables are on the stack and which are on the heap, and indicate orphaned memory. Indicate with a question mark (?) memory that we donā€™t know the values of.

struct Quidditch {
	int quaffle;
	int *snitch;
	int bludger[2];
};

struct Hogwarts {
	int wizard;
	Quidditch harry;
	Quidditch *potter;
};

Quidditch * hufflepuff(Hogwarts * cedric) {
	Quidditch *seeker = &(cedric->harry);
	seeker->snitch = new int;
	*(seeker->snitch) = 2;
	cedric = new Hogwarts;
	cedric->harry.quaffle = 6;
	cedric->potter = seeker;
	cedric->potter->quaffle = 8;
	cedric->potter->snitch =
	&(cedric->potter->bludger[1]);
	seeker->bludger[0] = 4;
	return seeker;
}

void gryffindor() {
	Hogwarts *triwizard = new Hogwarts[3];
	triwizard[1].wizard = 3;
	triwizard[1].potter = NULL;
	triwizard[0] = triwizard[1];
	triwizard[2].potter =
	hufflepuff(triwizard);
	triwizard[2].potter->quaffle = 4;
	// DRAW THE MEMORY AS IT LOOKS HERE
}

Orphaned memory is represented with dotted lines. For a full walkthrough of the solution, check out: http://tinyurl.com/HogwartsPointers.

Memory diagram, more detailed description to come soon


4) The Notorious RBQ, Revisited (RingBufferQueue.h/.cpp)

Topics: Classes, dynamic memory allocation, pointers

Remember our good friend the RingBufferQueue from last section? Check out the problem definition from last week if you want a quick refresher. Last time we visited the RBQ it had fixed capacity ā€“Ā that is, it couldn't grow in size after it was initially created. How limiting! With our newfound pointer and dynamic allocation skills, we can remove this limitation on the RBQ and make it fully functional!

Add functionality to the class RingBufferQueue from the previous section problem so that the queue resizes to an array twice as large when it runs out of space. In other words, if asked to enqueue when the queue is full, it will enlarge the array to give it enough capacity.

For example, say our queue can hold 5 elements and we enqueue the five values 10, 20, 30, 40, and 50. Our queue would look like this:

index    0    1    2    3    4
      +----+----+----+----+----+
value | 10 | 20 | 30 | 40 | 50 |
      +----+----+----+----+----+
         ^                   ^
         |                   |
       head                tail

If the client tries to enqueue a sixth element of 60, your improved queue class should grow to an array twice as large:

index    0    1    2    3    4    5    6    7    8    9
      +----+----+----+----+----+----+----+----+----+----+
value | 10 | 20 | 30 | 40 | 50 | 60 |    |    |    |    |
      +----+----+----+----+----+----+----+----+----+----+
         ^                        ^
         |                        |
       head                     tail

The preceding is the simpler case to handle. But what about if the queue has wrapped around via a series of enqueues and dequeues? For example, if the queue stores the following five elements:

index    0    1    2    3    4
      +----+----+----+----+----+
value | 40 | 50 | 10 | 20 | 30 |
      +----+----+----+----+----+
              ^    ^
              |    |
            tail head

If the client tries to add a sixth element of 60, you cannot simply copy the array as it is shown. If you do so, the head and wrapping will be broken. Instead, copy into the new array so that index 0 always becomes the new head of the queue. The picture will look the same as the previous one with the value 60 at index 5.

Write up the implementation of the new and improved RingBufferQueue. It should have the same members as in the previous problem, but with the new resizing behavior added. You may add new member functions to your class, but you should make them private.


RingBufferQueue.h


#pragma once

#include <iostream>
using namespace std;
class RingBufferQueue {
public:
    RingBufferQueue();
    ~RingBufferQueue();
    bool isEmpty() const;
    bool isFull() const;
    int size() const;
    void enqueue(int elem);
    int dequeue();
    int peek() const;
private:
    int* _elements;
    int _capacity;
    int _size;
    int _head;
    friend ostream& operator <<(ostream& out, const RingBufferQueue& queue);
    void enlarge();
};

RingBufferQueue.cpp


#include "RingBufferQueue.h"

static int kDefaultCapacity = 5;
RingBufferQueue::RingBufferQueue() {
    _capacity = kDefaultCapacity;
    _elements = new int[_capacity];
    _head = 0;
    _size = 0;
}
RingBufferQueue::~RingBufferQueue() {
    delete[] _elements;
}
void RingBufferQueue::enqueue(int elem) {
    if (isFull()) {
        enlarge();
    }
    int tail = (_head + _size) % _capacity;
    _elements[tail] = elem;
    _size++;
}
int RingBufferQueue::dequeue() {
    int front = peek();
    _head = (_head + 1) % _capacity;
    _size--;
    return front;
}
int RingBufferQueue::peek() const {
    if (isEmpty()) {
        throw "Can't peek at an empty queue!";
    }
    return _elements[_head];
}
bool RingBufferQueue::isEmpty() const {
    return _size == 0;
}
bool RingBufferQueue::isFull() const {
    return _size == _capacity;
}
int RingBufferQueue::size() const {
    return _size;
}
void RingBufferQueue::enlarge() {
    int *larger = new int[_capacity * 2];
    for (int i = 0; i < _size; i++) {
        larger[i] = _elements[(_head + i) % _capacity];
    }
    delete[] _elements;
    _elements = larger;
    _capacity *= 2;
    _head = 0;
}
ostream& operator <<(ostream& out, const RingBufferQueue& queue) {
    out << "{";
    if (!queue.isEmpty()) {
        // We can access the inner ā€˜_elementsā€™ member variables because
        // this operator is declared as a friend of the RingBufferQueue class
        out << queue._elements[queue._head];
        for (int i = 1; i < queue.size(); i++) {
            int index = (queue._head + i) % queue._capacity;
            out << ", " << queue._elements[index];
        }
    }
    out << "}";
    return out;
}

5) Min Heap

Topics: Heaps

We have implemented the Priority Queue ADT using a binary min-heap. Draw a diagram of the heapā€™s tree structure that results from inserting the following priority values in the order given: 25, 37, 28, 12, 30, 3

This image contains a table with 2 rows and three columns, for a total of 6 entries. These entries read as follows (left to right, top to bottm): 1) Diagram after inserting 25 2) Diagram after inserting 37 3) Diagram after inserting 28 4) Diagram after inserting 12 5) Diagram after inserting 30 6) Diagram after inserting 3


6) Max Heap

Topics: Heaps

You have a PriorityQueue class which treats higher (larger integer value) priority elements as frontmost. The internal implementation of the class is a binary max-heap stored in an unfilled array. The initial allocation of the array is for capacity 5 and the array capacity is doubled when asked to enqueue to a queue which is full. You are going to trace the operation of enqueuing and dequeuing elements from the priority queue. You can sketch as a tree if you prefer when working it out, but your final answer should be based on a representation of the underlying array that supports the heap.

(a) Show the contents of the internal array after these elements are enqueued to an empty priority queue in this order. Each element has a string value and a priority in parenthesis.

Red(8), Blue(33), Green(29), Purple(42), Orange(20), Yellow(22), Indigo(10), Teal(21)

(b) Dequeue is called twice on the priority queue. Which two values are removed?

(c) Show the contents of the internal array after the above two elements have been dequeued

(a) Color names are abbreviated

elements: P(42) B(33) G(29) T(21) O(20) Y(22) I(10) R(8) ? ?
index:  0   1   2   3   4   5   6   7   8   9 

(b) Purple(42) and Blue(33)

(c)

elements: G(29) T(21) Y(22) R(8) O(20) I(10) ? ? ? ?
index:  0   1   2   3   4   5   6   7   8   9