Section 4 - Classes and Dynamic Allocation
10A.4.1 Declaring and Instantiating Objects
Everything we said regarding primitive data types applies to classes. In fact, it is through its use in class objects that dynamic memory allocation makes a little more sense. As you probably know, you can declare classes that contain large numbers of member data. That means each object is going to be LARGE. That motivates us to not declare (instantiate) an actual object until, and unless, we need it. Thus, pointers are an efficient way to deal with class objects.
Pointers, of course, are all the same size: small. So no matter how large a class object is, the pointer to such an object is the same size as the pointer to an int or char. We can allocate lots of those and later instantiate objects for them to point to only when we need to.
Let's look at a Card class that I will define in the final code. (You can surmise the details by the Card object use in the following.) We start with the declarations and instantiation of the objects:
Card *card1, *card2, *card3; card1 = new Card; card2 = new Card('5'); card3 = new Card('9', hearts); Card *card4 = new Card('j', clubs), *card5 = new Card('1', diamonds);
I mixed it up a little bit to show you the variety that occurs.
- I declared the first three Cards as pointers and instantiated them directly afterwards in separate statements. The last two cards were instantiated in the declaration themselves.
- There are also parameters to class constructors which do not exist in primitive types. Therefore, you see those arguments being passed in the instantiated steps above (where in the simpler way of declaring objects the arguments are passed in the declaration).
This is very common in C++ and other languages: Declaration and instantiation are done (and thought of) separately. It is also universal to use the keyword new in all languages for instantiation of objects.
10A.4.2 Dereferencing Objects Through Pointers
We could use the, now instantiated, object card1 as we used primitives, by dereferencing through the *. It would look something like this:
(*card1).set('2', clubs); cout << (*card1).toString();
As you can see, this is hard to stomach. First we dereference the pointer to make it an object with *. Then we dereference the object to get to the field (instance member) with the dot ("."). A common alternative is the arrow, ->, (constructed with the two keys - and >)
card1->set('2', clubs); cout << card1->toString();
With this new notation let's see a little more of the pointer-ized Card program:
if ( ! card1->set(2, clubs) ) cout << "incorrect value (2, clubs) passed to card::set()\n\n"; if ( ! card1->set('2', clubs) ) cout << "incorrect value ('2', clubs) passed to card::set()\n\n"; cout << card1->toString() << endl << card2->toString() << endl << card3->toString() << endl << card4->toString() << endl << card5->toString() << endl<< endl;
10A.4.3 Assignments between pointers
We know that if we assign one object to another, all of the fields of the RHS (right hand side) object are transferred to the LHS (left hand side) object:
*card1 = *card4;
This is nothing new. Both the internal suit and the value of the *card4 are moved to *card1. The only thing that looks different is the asterisk before each variable, but we know that's just how we dereference the pointers to get the objects. So we should understand what is going on. But, what about this:
card1 = card4;
? This is very different. No data is moved between the objects. Instead the address in card4 is assigned to card1. Now both card1 and card4 point to the same object. This is exactly the same as with primitive floats or longs, as we saw. I'm just repeating this here for emphasis.
Now, what happened to original object pointed to by card1 after this assignment? Guess what? It's lost. Not de-allocated, but lost. This could be bad. Very bad. You see, the object that card1 pointed to had no name, so we don't know how to get at it any more. We used card1 to get it using the notation *card1, but now card1 points to something else, namely the object controlled by card4.
While I say this could be bad, it could also be fine. It depends on what we did before this statement in our program. For instance we might have saved the object first, before the assignment. In this example, assume card1 and card4 have been already declared and instantiated as above. We use a Card pointer, temp, so solve our problem:
Card *temp; temp = card1; // now temp and card1 both point to the same object card1 = card4; // now card1 and card4 both point to the same object
This is good. We declared a Card pointer, temp, but instead of allocating an object for it, we assigned card1 to it. The reason this isn't a problem is that we have no prior object to which temp was pointing, so nothing was lost. After these two assignments, temp is the only pointer variable left pointing to its object - the object originally created using card1.
10A.4.4 Deleting Objects
Let's look at deleting object memory again. We use the pointer to delete the object controlled by that pointer:
Card *temp; temp = card1; // now temp and card1 both point to the same object card1 = card4; // now card1 and card4 both point to the same object delete temp; // deletes the first card delete card1; // deletes the fourth card delete card4; // run time error!! card4 already deleted.
Try to stay focused. Read the comments to the right of each statement above and you will see what's going on. It is the object pointed to that gets deleted. It doesn't matter if card1 was used to originally instantiate an object. If it no longer points to that object then it cannot be used to delete it. We had to use temp to delete the first card, since, after the series of assignment statements, temp was the only remaining pointer pointing to that Card. Meanwhile, both card1 and card4 pointed to the same object afterwards, so we could use either to delete that Card. And if we try to use both? Crash and burn. The second attempt to delete the same object will generate a run-time error (exception). That's why, when we finish our card program, we have to be careful.
Here is the remainder of that program. (The complete main is in the next section).
*card1 = *card4; cout << "after assigning *card4 to *card1:\n"; cout << card1->toString() << endl << card4->toString() << endl << endl; card1 = card4; cout << "after assigning card4 to card1:\n"; cout << card1->toString() << endl << card4->toString() << endl << endl; delete card1; delete card2; delete card3; // delete card4; if you don't omit this you'll get a run-time error delete card5;
You should be able to guess that printing out both card1 and card4 after either of the two assignments above will result in the same values. We can't tell the difference by looking at the values themselves whether we are cout-ing the data from the same object, or cout-ing data from two different objects that happen to be the same.
This is where professional C++ programmers earn their money. C++ is faster and more efficient than any other programming language, but the cost is high: Programmers have to manage memory carefully in C++.
10A.4.5 The Main() Listing
The Card class prototypes and member method definitions are finally defined in the full listing. main() will use pointers to Cards, rather than simple Card objects, to demonstate the concepts.
If you have questions, please ask in the course discussion area.
#include <string> #include <iostream> #include <sstream> #include <cctype> using namespace std; enum Suit { clubs, diamonds, hearts, spades }; // class Card prototype ----------------------- class Card { private: char value; Suit suit; public: Card(char value = 'A', Suit suit = spades); string toString(); bool set(char value = 'A', Suit suit = spades); char getVal(); Suit getSuit(); }; // end of class Card prototype -------------- int main() { Card *card1, *card2, *card3; card1 = new Card; card2 = new Card('5'); card3 = new Card('9', hearts); Card *card4 = new Card('j', clubs), *card5 = new Card('1', diamonds); if ( ! card1->lset(2, clubs) ) cout << "incorrect value (2, clubs) passed to card::set()\n\n"; if ( ! card1->lset('2', clubs) ) cout << "incorrect value ('2', clubs) passed to card::set()\n\n"; cout << card1->ltoString() << endl << card2->ltoString() << endl << card3->ltoString() << endl << card4->ltoString() << endl << card5->ltoString() << endl<< endl; *card1 = *card4; cout << "after assigning *card4 to *card1:\n"; cout << card1->ltoString() << endl << card4->ltoString() << endl << endl; delete card1; // need this before the next line, else get silent memory leak. card1 = card4; cout << "after assigning card4 to card1:\n"; cout << card1->ltoString() << endl << card4->ltoString() << endl << endl; delete card1; // actually deleting what was originally card4 delete card2; delete card3; // delete card4; if you don't omit this you'll get a run-time error delete card5; } // beginning of Card method definitions ------------- // constructor Card::Card(char value, Suit suit) { // if not valid, set to Ace of Spades if ( !set(value, suit) ) set('A', spades); } // stringizer string Card::toString() { string retVal; char strVal[2]; // convert char to a CString strVal[0] = value; strVal[1] = '\0'; // convert from CString to s-c string retVal = string(strVal); if (suit == spades) retVal += " of Spades"; else if (suit == hearts) retVal += " of Hearts"; else if (suit == diamonds) retVal += " of Diamonds"; else if (suit == clubs) retVal += " of Clubs"; return retVal; } // mutator bool Card::set(char value, Suit suit) { char upVal; // convert to uppercase to simplify upVal = toupper((int)value); // check for validity if ( upVal == 'A' || upVal == 'K' || upVal == 'Q' || upVal == 'J' || upVal == 'T' || (upVal >= '2' && upVal <= '9') ) { this->lsuit = suit; this->lvalue = upVal; return true; } else return false; } // accessors char Card::getVal() { return value; } Suit Card::getSuit() { return suit; } // end of Card method definitions --------------
Here is the output:

10A.4.5 Future Lessons in Pointers
We have to stop at this point, because the next things I have to say about pointers belong in the next course. In particular, how pointers relate to arrays is a very important topic with profound consequences. We'll see that in CS 2B.