Class 11 Slides Compile-time versus Run-time memory allocation Allocating and referring to dynamically allocated memory Referencing data members and methods of structs and class objects Pointer assignment and comparison De-allocating memory Linked lists A structure definition for a list element Adding an item to an existing list Add new first item Add new last item Add new middle item Find the proper position for a new item in the middle of the list Initializing a list Lab: Delete from list Implementing stacks or queues with a linked list JTStack header file JTStack method implementations: Constructors JTStack method implementations: Destructor, Assignment JTStack method implementations: top(), isEmpty(), length(), push(item) JTStack method implementations: pop(), pop(item), makeEmpty() Other types of linked lists -------------------------------------------------------------------------------- Compile-time versus Run-time memory allocation When you declare a variable, you request that memory be allocated for that variable at the time your program starts running. The memory remains allocated until the program ends, at which point it's returned to the pool of memory available to other programs. It is also possible to request that memory be allocated after the program has started running. You would want to do this when you don't know at compile time just how much memory you'll need. This is called dynamic memory allocation. All five of the AP classes use dynamic memory allocation in their implementations, in one form or another. If you want to allocate memory dynamically, you will need some way to reference the memory that's been allocated. To do this, you use a pointer. A pointer is essentially the address of a location in memory, stored in a memory location of its own. Here's the difference: int iVar; int *iPtr = NULL; iPtr = new int; iVar = 5; *iPtr = 5; The use of the '*' operator is called pointer indirection, or dereferencing. -------------------------------------------------------------------------------- Allocating and referring to dynamically allocated memory Just declaring a pointer variable does not allocated memory. The pointer variable is initially undefined. To allocate dynamic memory, use the new operator. It takes one argument, which is the type of value to allocate memory for. This can be a simple type, like int or char, but is more likely to be a struct or class type. int *intPtr = new int; double *doublePtr = new double; apstring *stringPtr = new apstring; apqueue *queuePtr = new apqueue; Calling new on a class type automatically calls the class's default constructor. You can also call other class constructors; for example, apstring *namePtr = new apstring("Amy"); The memory that gets allocated is uninitialized. To set a value in the memory location now pointed to (or "referenced") by the pointer variable, you must use the dereference operator *. *intPtr = 62; intPtr = 50; // Wrong! Changes the address stored in intPtr! *doublePtr = 3.1415 * *intPtr; *stringPtr = *stringPtr + " Ann"; You can use the constant value NULL to indicate explicitly that a pointer isn't pointing to anything. intPtr = NULL; if (intPtr != NULL) cout << *intPtr << endl; -------------------------------------------------------------------------------- Referencing data members and methods of structs and class objects If I have a pointer to an apqueue object, how do I call its member methods? To refer to the apqueue object pointed to, I need to use the dereference operator '*'. To call a method of the class, I use the '.' operator. So I must combine the two in order to call a member method of a referenced class object. Maybe this way... *queuePtr.dequeue(); Unfortunately, this doesn't quite work, because the '.' operator happens to have higher precedence than the unary * operator. So the computer tries to evaluate: *(queuePtr.dequeue()); But since queuePtr is not of type apqueue, but of type pointer-to-apqueue, the '.' operator fails. Instead, I must write: (*queuePtr).dequeue(); Since this is pretty awkward for a quite common construct, the language provides an easier way of writing it. This line is the exact equivalent of the one above: queuePtr->dequeue(); The upshot is, when you have a class object, use the '.' operator, and when you have a pointer to a class object, use the '->' operator. Note: this also applies to structs! -------------------------------------------------------------------------------- Pointer assignment and comparison One way to give a value to a pointer variable is to call new. Another way is to assign it the value of another pointer variable. Doing so would mean you have more than one pointer pointing to the same location in memory. int *p, *q; p = new int; *p = 25; q = p; Now p and q point to the same memory location. What happens when I do this now? *q = 32; Note that there is a difference between comparing the values of two pointers and comparing the values they point to. For instance, what will be the output here? p = new int; q = new int; *p = 7; *q = *p; if (p == q) cout << "p and q are equal" << endl; if (*p == *q) cout << "*p and *q are equal" << endl; -------------------------------------------------------------------------------- De-allocating memory Just as you should close an input or output file when you're done with it, you should explicitly deallocate any memory you no longer require. If p is a pointer to a value of a given type, then to reclaim the space used for that value, write: delete p; It is up to you to make sure that any other pointers pointing to the same location in memory don't get used after that memory has been freed. These are called dangling pointers -- they point to dereferenced space. For instance: int *p, *q; p = new int; *p = 10; q = p; delete p; *q = 8; // Dangling pointer! You must also be careful not to lose your last reference to a memory location before it's been deallocated. If that happens, you won't be able to reclaim the space. int *p = new int, *q; *p = 10; q = p; p = new int; q = NULL; // Oops! Lost last pointer to location of the 10! -------------------------------------------------------------------------------- Linked lists A linked list is a data structure which uses dynamic memory allocation to grow and shrink the amount of memory it takes up, never using more than it needs. Granted, apvectors do this to some extent, and are more appropriate for some applications, but there are other applications where linked lists win out. Resizing of apvectors is somewhat awkward - you wouldn't want to have to resize every time you added or removed an element from the array. With linked lists, you don't have to. Here's a basic picture of a linked list. This type is called a singly-linked list, for obvious reasons. Each element is linked to the next in the list through a pointer. There is one distinguished pointer, which points to the first element in the list. I've also shown a second distinguished pointer, which points to the last element in the list. This pointer isn't always necessary (it depends on what you're going to use the list for), but often convenient. -------------------------------------------------------------------------------- A structure definition for a list element Each element of the list illustrated has two values: a data value (of type apstring) and a pointer to the next item. This can be represented as a struct type, with the following definition: struct ListItem { apstring word; ListItem *next; }; ListItem *first, *last; These two pointers are all I need to keep track of the list. All the rest of the links are 'internal'. Suppose I want to print all the words in the list, in order. The following code will do the job: ListItem *p = first; while (p != NULL) { cout << p->word << endl; p = p->next; } Note the assignment of pointer value to pointer value, which has the effect of pointing p to the next item in the list. This is the key to working with lists. Also notice the end-of-list test. -------------------------------------------------------------------------------- Adding an item to an existing list Suppose we want to add a word to our alphabetical list. There are three possible scenarios: the word belongs at the beginning of the list, the word belongs at the end, or the word belongs somewhere in the middle. We'll work through each of the possibilities in diagrams and code. For all three cases, we first start by creating a new ListItem. ListItem *newItem = new ListItem; apstring newWord; cin >> newWord; newItem->word = newWord; newItem->next = NULL; The code framework for inserting the new word is: if (newWord <= first->word) { add new first item } else if (newWord >= last->word) { add new last item } else { add new item somewhere in the middle } If you type in your code as we develop it on these slides, you'll be all set for the lab on Slide 14, and probably have a more accurate, readable version than if you try and write the code down. -------------------------------------------------------------------------------- Add new first item Add "BOB" to the beginning of this list: In code: -------------------------------------------------------------------------------- Add new last item Add "SOB" to the end of this list: In code: -------------------------------------------------------------------------------- Add new middle item Assume you have a pointer insertAfter pointing to the item to insert the new value after. Add "LOB" to the middle of this list: In code: -------------------------------------------------------------------------------- Find the proper position for a new item in the middle of the list Basically, we need to implement linear search for a linked list. The other two searches we know aren't applicable, because linked lists aren't random-access (their biggest liability). Here's our array-based linear search routine: int pos; int N = Numbers.length(); for (pos = 0; pos < N; pos++) { if (Numbers[pos] == x) return pos; if (Numbers[pos] > x) return -1; } return -1; How do we write it for linked lists? Keep in mind: we know the new item goes in the middle, not first or last. -------------------------------------------------------------------------------- Initializing a list We've been assuming a list with some elements already in it. How do we set up an empty list? first = last = NULL; That was simple. Do we have to make any changes to our insertion code in order for it to work even if the list is empty? Yes, but not a very big change. If we insert a word into an empty list, it automatically becomes the first and last item in the list, without having to be compared to any other words. So we just add one more condition to the beginning of our cascading if-else: if (first == NULL) { first = last = newItem; } else if ... Was that so bad? -------------------------------------------------------------------------------- Lab: Now that we can insert items into a list, how about deleting items? Start with code that inserts several words into a singly-linked list. Then allow the user to enter a word to be deleted. If the word doesn't appear in the list, leave the list as is, otherwise delete the word, and print out the words in the modified list. Use diagrams to help you develop your code. You will have three cases, just as with insertion. Be careful! Don't destroy your last pointer to an item! Use breakpoints and watches to help debug. Remember to use delete to deallocate the memory used by the item you're deleting. Only do this when you no longer need the pointer to this item. -------------------------------------------------------------------------------- Implementing stacks or queues with a linked list As mentioned, linked lists aren't always appropriate. They're not very efficient for searching or sorting, since they're not random access (however, you could perform an insertion sort fairly well, with a little coding effort). But they are appropriate for implementing stacks or queues, because: Stacks and queues can grow or shrink at run-time, making dynamic memory allocation appropriate. For a stack, the only available data-changing operations are: push onto top of stack, which can be implemented as "insert at the front of the list" pop from top of stack, which can be implemented as "delete from the front of the list" For a queue, the only available data-changing operations are: enqueue at back of queue, which can be implemented as "insert at the back of the list" dequeue from front of queue, which can be implemented as "delete from the front of the list" You can wrap either of these implementations up in a class definition with methods identical to those provided by apstack and apqueue. A linked list implementation of a stack class appears on the next few slides. -------------------------------------------------------------------------------- JTStack header file #ifndef _JTSTACK_H #define _JTSTACK_H #include "bool.h" template class ListItem { public: itemType item; ListItem *next; }; template class jtstack { public: jtstack(); // construct empty stack jtstack(const jtstack& s); // copy constructor ~jtstack(); // destructor const jtstack& operator =(const jtstack& rhs); const itemType& top() const; // return top element (NO pop) bool isEmpty() const; // return true if empty, else false int length() const; // return number of elements in stack void push(const itemType& item); // push item onto top of stack void pop(); // pop top element void pop(itemType& item); // combines pop and top void makeEmpty(); // make stack empty (no elements) private: int nItems; ListItem *first; }; #include "jtstack.cpp" #endif -------------------------------------------------------------------------------- JTStack method implementations (jtstack.cpp) Constructors #include "jtstack.h" #include "stdlib.h" #include "iostream.h" template jtstack::jtstack() { first = NULL; nItems = 0; } template jtstack::jtstack(const jtstack& s) { nItems = s.nItems; if (s.first == NULL) { first = NULL; return; } first = new ListItem; ListItem *traverse_copy = first; ListItem *traverse_orig = s.first; first->item = traverse_orig->item; while (traverse_orig->next != NULL) { traverse_copy->next = new ListItem; traverse_copy->next->item = traverse_orig->next->item; traverse_copy = traverse_copy->next; traverse_orig = traverse_orig->next; } traverse_copy->next = NULL; } -------------------------------------------------------------------------------- JTStack method implementations (jtstack.cpp) Destructor, Assignment template jtstack::~jtstack() { ListItem *traverse = first; while (first != NULL) { traverse = first->next; delete first; first = traverse; } } template const jtstack& jtstack::operator =(const jtstack& rhs) { nItems = rhs.nItems; if (rhs.first == NULL) { first = NULL; return *this; } first = new ListItem; ListItem *traverse_copy = first; ListItem *traverse_orig = rhs.first; first->item = traverse_orig->item; while (traverse_orig->next != NULL) { traverse_copy->next = new ListItem; traverse_copy->next->item = traverse_orig->next->item; traverse_copy = traverse_copy->next; traverse_orig = traverse_orig->next; } traverse_copy->next = NULL; return *this; } -------------------------------------------------------------------------------- JTStack method implementations (jtstack.cpp) top(), isEmpty(), length(), push(item) template const itemType& jtstack::top() const { if (isEmpty()) { cerr << "error, popping an empty stack" << endl; abort(); } return first->item; } template bool jtstack::isEmpty() const { return (first == NULL); } template int jtstack::length() const { return nItems; } template void jtstack::push(const itemType& item) { ListItem *newItem = new ListItem; newItem->item = item; newItem->next = first; first = newItem; nItems++; } -------------------------------------------------------------------------------- JTStack method implementations (jtstack.cpp) pop(), pop(item), makeEmpty() template void jtstack::pop() { if (isEmpty()) { cerr << "error, popping an empty stack" << endl; abort(); } ListItem *oldFirst = first; first = first->next; delete oldFirst; nItems--; } template void jtstack::pop(itemType& item) { if (isEmpty()) { cerr << "error, popping an empty stack" << endl; abort(); } item = first->item; ListItem *oldFirst = first; first = first->next; delete oldFirst; nItems--; } template void jtstack::makeEmpty() { ListItem *traverse = first; while (first != NULL) { traverse = first->next; delete first; first = traverse; } nItems = 0; } -------------------------------------------------------------------------------- Other types of linked lists The disadvantage of a singly-linked list is that you can only advance through it in one direction; there's no way to move to the item just before one you have a pointer to. To remove this difficulty, you can create a doubly-linked list. One set of links allows forward movement through the list, the other set allows backward movement. A structure definition for a doubly-linked list might look like: struct ListItem { itemType item; ListItem *next; ListItem *prev; } Your assignment has you implementing insertion, deletion, and traversal of a doubly-linked list. The other linked list variation is the circular list, either singly or doubly linked. The trick there will be to have some conception of a "first" or "last" element. For instance, if you are searching for an item in a circular list, you won't be able to check for a NULL pointer to detect when you've searched all the items.