COM 1101 Algorithms and Data Structures 1 - Winter 2001
Course Wrap-Up, Covering Pointers and Inheritance
for Professor Futrelle's section
College of Computer Science, Northeastern U., Boston, MA
(Version of 2/24/2001)
These pages contain "pointers" to the textbook as well as a number of examples
I've developed, mostly commented code, covering the two topics. You'll notice
that little is said about destructors and copy constructors. This is for lack
of time in this course and also because all the examples here involved simple
dynamic objects. Any more complex uses would, at a minimum, require some
understanding of destructors.
LAB #7: Lab 7 is based strongly on this writeup.
Most of the things you
need to do in Lab 7 are variations on the examples on this page.
Topics on pointers and corresponding pages in your textbook to study.
- Simple pointers and pointer notation, Chap. 11.
More specifically, pointer variables, pgs. 679-685.
- The new operator, display on pg. 686.
- The delete operator, also a display on pg. 686.
- Dangling pointers, "Pitfall" on pg. 689. One of the problems
of manually managing your program's memory allocation is to avoid
deleting a data object when there is still a reference to it
that you are relying on. Since pointers allow more than one variable
to directly reference an object, this is a danger.
- Defining pointer types using typedef, pgs. 690-691.
This is convenient, but does not automatically solve the problems
of using pointer variables. It simply makes it slightly easier to
define them. But you should name the types you define
clearly so you can distinguish them from non-pointer types.
- Passing pointers to functions. The book would have you believe
that this is a complex and dangerous affair, "Pitfall" on pg. 708.
But it is not at all, once you understand what is going on.
In fact, every object argument in functions in languages such as Java
are handled in this way, so it can't possibly be a bad thing!
You simply have to understand that your function has access to the
location of any object whose pointer is passed as a value and any
changes made, e.g., *myPtr = 3.0, are changes to the original item passed, changes
visible in the calling program after the function returns. Reference parameters
in C++ are simply pointer arguments in disguise and behave in the same way.
That's why reference arguments are advertised as ways to "pass back values
via arguments". In fact they don't "pass back" anything, they simply directly
alter the values of the original variables passed as the reference arguments.
Since the book uses typedef everywhere, it's a bit hard to see how pointers
are being passed (just as it is when reference arguments are used). Here's a simple
example of a function that is explicitly passed a pointer and modifies
the value at the location of the pointer.
This example is followed an example of the use of the function:
// adds one to the value at the argument location:
void ptrArgFn(int* a) { (*a)++; }
// before this call, the value referenced by nPtr is 5. After the call it is 6:
int *nPtr;
*nPtr = 5;
ptrArgFn(nPtr);
// at this point *nPtr is 6.
The point being made here is that the function returns nothing but it does access
the location passed to it and alters that value at that location.
- Returning pointers from functions.
In just the same way, we can return a pointer.
If it's a pointer to a newly created object, an object created with new,
then it's perfectly reasonable to return a pointer.
Constructors used with new create objects and return pointers to them
as a matter of course. If the pointer is the address of some automatic
variable, bad things can happen. So don't do it. In the section
below on dynamically creating objects there is an example of
a function that creates and returns an object, but is not a constructor.
- Dynamic allocation of arrays. This topic is treated at some length
in the book. I would rather have seen the book devote more time to
the dynamic creation of objects (class instances), but nevertheless,
it makes important and useful points in its discussion of creating and
using dynamic arrays. So you need to read this discussion, pgs. 694-698.
C++ requires a special form of the delete operator for arrays, pgs. 695-696.
- Dynamic allocation of objects (class instances). This is a topic
that is quite important. In Java this is the only way to create object
instances, by using the new operator.
The book treats this topic only briefly, and in passing, on pgs. 809-812,
when discussing linked lists. But the creation of objects with new
is basically as simple as the use of new for the dynamic
creation of an int, pg. 683. The example that follows shows this.
The section immediately following shows how the constructor can be used to
set various member variables in an object at the time it is created.
// first, we define the class dog for this example and others below
class dog {
public:
int age;
dog* buddy;
};
// Here's how to create a dog (voila, out of nothing!)
// The new operator creates a new dog object, finding a place for
// it in the computer's memory and returning a pointer to it
// (its memory location). The variable aDog is a pointer to a
// dog and the new operator returns just that, a pointer to a dog.
dog *aDog = new dog;
// Let's create another dog and make it the buddy of our first dog:
dog *anotherDog = new dog;
aDog->buddy = anotherDog;
// (see the note on the arrow operator in the Arrays of pointers section below)
// Here's a function that creates an object
// and returns a pointer to it. Not difficult to do!
dog *Hey__Make_Me_A_Dog__Thanks(){
return new dog;
}
- Constructors for objects. In the previous example, we created "empty" dogs.
None of the member variables were set to any values we chose. Since the
initialization of objects is so common and necessary, special mechanisms have been
built into the language to do this, the class of constructor functions.
This is discussed in the book, pgs. 336-345. This is a somewhat lengthy section,
but that's because it's an important topic. Below is a simple example of extending
the dog class by adding a couple of constructors.
class dog {
public:
int age;
dog* buddy;
dog(){}; // defined in place, instead of externally with dog::dog() syntax
dog(int age);
};
// definition of the constructor
dog::dog(int a) { age = a;}
// use of the constructor to create a 7 year old dog
dog *aDog = new dog(7);
// It's as simple as that.
// See the book about why a default constructor was defined (no arguments).
- The delete operation and the destructor.
This section, pgs. 707-708, is worth reading. In many of our
simple programs we never delete anything, we simply let the program
terminate (the end of main()) and everything is reclaimed.
Futhermore, if the objects you're deleting don't create further dynamic
objects internally you don't need to define your own destructor function.
I consider situations in which you have to define your own destructor
function as advanced topics, not covered in this COM1101 class.
- Arrays of pointers. Arrays of pointers can be useful because you can
separately control the initialization of every object referenced by the array.
In the following example, an array of dogs is created, each with a different
age. Each entry in the array is a pointer to an object of type dog.
The new operator returns a pointer and that's why it all works. (If it makes
you more comfortable, you might want to name the array dogPtrs[].
But if you use pointers and references everywhere, like I do, you get a little
tired of adding "Ptr" to everything.)
dog *dogs[10];
for(int i = 0; i < 10; i++) {dogs[i] = new dog(i);}
// a use of the age of a dog would use the arrow operator (pg. 806):
if (dogs[k]->age > oldDogAge) // no need to card the dog
Be sure you know how to use the arrow operator just mentioned.
It is used just like the "dot" operator, except it is used when the variable
is a pointer to an object rather than an object.
- Passing arrays of pointers.
Since arrays of pointers can hold useful collections of information it
is important to understand how they can be passed to functions.
Of course arrays are never copied, they're passed by reference, so
any changes you make to them in the function "stick" -- the changes are
made to the actual array passed and are remain changed, of course,
after the function returns.
// This function takes an array of pointers to dog objects, creates
// a new dog object of the requested age and places the pointer to it
// in the requested element of the array:
void addDog(dog *darr[], int which, int age) {
darr[which] = new dog(age);}
// Here's a simple use of such a function:
dog *someDogs[10];
addDog(someDogs, 3, 7);
- Automatic memory management in languages such as Java, Lisp and Scheme.
Deciding just how and when to reclaim memory for objects you have created
can be quite a tricky process. If you fail, your program may continue to
fill up memory until memory is exhausted (a "memory leak") and your program
will crash. For very large programs the problems can be really complex.
In languages such as Java, Lisp and Scheme there are special processes that
run alongside your programs that analyze what data your program can access.
If certain data in your program is no longer reachable by any variable
or any pointer reference or any chain of them,
then the system automatically returns the memory
to the available memory or "freelist" so it can be re-used as space for
future newly created objects. This "garbage collection" process is efficient
and thorough and never makes mistakes. It can save you from subtle bugs
and from the sometimes overwhelming task of keeping track of all the references
to your data yourself.
Topics on inheritance and corresponding pages in your textbook to study.
The major reading for this is essentially all of Chapter 15, with emphasis
on the topics discussed below.
- Basics of inheritance
The idea behind inheritance is that there are classes that represent
things and there can also be classes that represent more specialized
types of objects. An example would be the class animal and the more
specialized concepts of dog, cat, mouse and bird.
In object-oriented languages such as C++, Java and others, this notion
of specialization is implemented by allowing the creation of classes that
inherit members from other classes. Thus, we can create a dog class and
inherit from the animal class various members such as age and weight.
By "inherit", we mean that we don't have to respecify or write into the dog class
the member variables age and weight, we merely say that the dog class inherits
from the animal class. We say that dog is a subclass
and animal is the superclass
in this example. This hierarchy can extend further if we like, with living-things
being a superclass of animal and sporting-dog
or collie being subclasses of dog. But if you have a specific
dog in mind, your collie "Fella", that should normally be an instance of collie, not
a subclass, because you probably won't be making multiple instances of "Fella".
There are no hard-and-fast rules about this. You have to decide on a reasonable
and useful representation, depending on the problem you're trying to solve.
Here's how you can define a class and subclass. Details are in the book in Chapter 15.
class animal{
public:
int age;
double weight;
};
class dog : public animal{
public:
string breed;
};
// You can now create a dog and set its age:
dog *someDog = new dog;
someDog->age = 12;
The entire process of dealing with objects becomes a bit more complex because
not only are there "local variables" that are members of your class, they may
not even be evident in your class definition, since they can be inherited from
a superclass. So in gaining power, flexibility and efficiency through inheritance
and the subclassing mechanism, you have more to keep track of. On the other hand,
the idea that a dog class has an age member is not a particularly subtle or mysterious
notion.
- Adding new member variables. In the example above, when we created
the dog subclass, we created a new member variable, breed, that did not
appear in the superclass. We can use this new member as follows:
someDog->breed = "Collie";
What we cannot do is to create an instance of animal and refer to
a breed member variable. It has no such member variable. On the other
hand, any subclass of dog will have the breed member variable, since it is
a public member variable. (There is feature of C++, the protected member variable
which is like private but usable ("visible") in subclasses, pg. 853-854.)
- Redefining member functions. When we create more specialized subclasses, it
is sometimes the case that member functions need to be more specialized.
Here is a few examples.
class animal{
public:
int age;
double weight;
void sayHello();
};
class dog : public animal{
public:
void sayHello();
};
void dog::sayHello() {cout << "Woof!" << endl;}
class cat : public animal{
public:
void sayHello();
};
void cat::sayHello() {cout << "Meow" << endl;}
// This means that the same member function sayHello() with the
// same argument list, will act differently depending on whether
// it is used with a cat or a dog instance, e.g.,
dog *aDog = new dog;
cat *aCat = new cat;
aDog->sayHello(); // will print "Woof!", but
aCat->sayHello(); // will print "Meow"
Note that whenever we want to redefine an inherited member function, we
need to puts its prototype in the subclass definition. Otherwise, it
is inherited unchanged.
- Constructors and inheritance.
Initializing an instance of a subclass using constructors is also
a bit more complex, since the superclass may have its own constructor.
In the example below, we use a constructor for a superclass for age
and one for the subclass that includes favorite food.
class reptile {
public:
int age;
reptile(){};
reptile(int age);
};
reptile::reptile(int a) { age = a;}
// a subclass of reptile:
class lizard : public reptile {
public:
string favoriteInsect;
lizard(string favBug);
lizard(int age, string favBug);
};
// Here's how we can write a constructor that will init both
// the age and the favorite insect members:
lizard::lizard(string bug) {favoriteInsect = bug;}
lizard::lizard(int age, string bug) : reptile(age)
{
favoriteInsect = bug;
}
This example is slightly simpler than the textbook's on pgs. 850-852,
because it does not use the initialization list construct, but
just a simple assignment in the constructor body. The important
point to note is the special ':' syntax that allows a superclass
constructor to be inserted before the body of the subclass constructor
that follows in the braces, {....}.
- Polymorphism and run-time dispatching. This is covered in Sec. 15.2
of your text.
I will discuss this topic briefly in lecture.
Roughly, it allows a member function to be applied
to an object that can be of any of number of subclasses, and the particular function
chosen will be determined while the program is running.
Why is this useful? We might have a superclass of type message with various
subclasses for more specialized messages. When a message instance M arrives, you can
simple call M.handleMessage() and the appropriate handleMessage() function will
be called, as long as you've defined such a function for each message type.
Without this you would be forced to build a complex decision structure based on
the superclass, that examined each message to see what type it was and then
called the appropriate function. Since this is such a highly desirable capability
to have in the language, it is built in (as it is in Java and similar languages).
Go to COM1101 home page (Gateway)
Return to Prof. Futrelle's home page