This example demonstrates how polymorphism can be used to make it appear
as if one container (e.g., vector, list
etc...) can hold similar but fundamentally different objects. In a
previous example we implemented the following
classes: TextDoc, Folder, and
SuperFolder. The first class represented a text document.
The second class used a list data member to store
multiple TextDoc objects. The third class inherited
from the Folder class and added the ability to store
Folder objects within it with an additional
list data member. It should be noted that the
SuperFolder class could store TextDoc
and Folder objects, but it could not store
SuperFolder objects. Therefore, we could not represent
a directory structure that was more than three levels deep (a
SuperFolder that contained a Folder
that contained a TextDoc).
In this example we will
- use only one list data member,
- be able to represent arbitrarily deep directory structures, and
- remove some of the repetetive code in the TextDoc
and Folder classes found in the previous example.
In the previous example, we had very similar functions in the
TextDoc and Folder classes.
TextDoc::getDocName() was very similar to
Folder::getFolderName(). The data members
TextDoc::docName and Folder::fname
were also very similar. In this example, we will avoid this redundancy
by creating a base class, DesktopItem, that will describe
characteristics that are common to all items that you may find on your
computer desktop. The new versions of the TextDoc
(TextFile) and Folder classes will
inherit this functionality from the DesktopItem class.
There are other advantages for creating this base class, but it makes
sense to use inheritance here even if we didn't make use of the
other advantages.
Because derived class objects have some common characteristics with
their parent class objects, it is possible for a base class pointer to
point to a derived class object. This is useful because we can now
create a list of pointers to DesktopItem
objects as a data member in the Folder class. This
data member will be able to "store" any kind of desktop item.
Not only does this allow us to avoid having to have to
list data members in the Folder
class, it also means that if we decide to define another specific type
of desktop item, (e.g., an MP3File class), we will
not need to modify the Folder class. It will
automatically be able to keep track of MP3File
objects (provided the MP3File class inherits the
DesktopItem class).
The true power of polymorphism is run-time binding (a.k.a., dynamic
binding or late binding). The Folder class has a
list
of DesktopItem pointers as a data member. When the
program is compiled, it is not clear what each DesktopItem*
points to. (Consider the first DesktopItem* in the
list. How do we know if it is pointing to a
DesktopItem, a TextFile, or a
Folder? It depends on what object was first appended
to the Folder object, and we may not know this until
the program is run.)
If we were to call getItemName (a member function
from the DesktopItem class), the code implemented
in the DesktopItem.cpp file will be called. That's
just what we want because the function is not overridden in any of the
derived classes. However, if we were to call size
(another member function from the DesktopItem class),
the code implemented in the DesktopItem.cpp file will
be called. That's fine if the pointer is pointing to a
DesktopItem object, but it it is pointing to a
Folder object, it should be calling the overridden
function from the Folder class instead (same goes
for a TextFile object). The default in C++ is to
call the DesktopItem class regardless of what the
pointer is pointing to. This is known as static binding
(a.k.a., compilte-time binding or early binding) because the decision of
which function to call (or bind/link to) is done once and does not change.
Run-time binding allows us to delay the decision of which function to
call until the program is running. This often referred to as
dynamic binding because the decision of which function to
bind to may change as the program is executed. This feature is enabled by
use of the virtual keyword. In order to make use of
dynamic binding, we must have a base class pointer that points to an object
from a derived class (e.g., a DesktopItem* that points
to a Folder object). By declaring a member function
of the base class to be virtual, the binding of that
function, when called by way of the base class pointer, will be delayed
until run-time. At run-time, code will be run to determine the what type
of object the pointer is pointing to and call the appropriate version
of the function. Consider the following example:
Folder aFolder("Example");
DesktopItem* ptr=&aFolder;
cout << ptr->getItemName();
cout << ptr->size();
Here we created a Folder object and made a pointer,
ptr, that points to it. We then call two member functions:
getItemName() and size().
The first function is bound at compile time because it was not declared to
be a virtual function. The second function is bound at
run-time because it was declare as a virtual function.
As a result, when this code is executed, the computer first looks to see
what type of object ptr is pointing to and then calls
the appropriate version of size(). Polymorphism
(this concept) is what allows me to implement the Folder
class with just one list.
It should also be noted that this design is much more flexible than
the previous design. For example, if we were to create a
MP3File class that described MP3 files, we could
add MP3 files to my folder (provided that the MP3File
class inherited from the the DesktopItem class) without
modifying my Folder class at all.
In the above discussion I have talked as if we could create
DesktopItem objects. It just so happens that we
cannot. The reason we can't create DesktopItem
objects is because the DesktopItem class is an
abstract base class.
Abstract base classes are useful for defining a functional interface
for a family of classes that are derived from it. Typically the abstract
base class defines a class that is so general that no objects from the
base class actually exist. For example, an animal class would be an abstract
base class. There are specific types of animals that may be found in the
wild, but an animal, in and of itself, does not exist... it must be a specific
type of animal. In this example, we will never create a
DesktopItem; we will only create specific types of
DesktopItems, like TextFiles and
Folders. Therefore, it makes sense that the
DesktopItem class is an abstract base class.
One way to figure out if a class should be an abstract base class is
to determine if there is a member function that should be implemented in
all of the derived classes that cannot be implemented in the base class.
In this example, it doesn't make sense to implement the
erase() function in the DesktopItem
since there are no data members that can be erased. Therefore, we don't
actually implement this function. Instead, we declare
erase() to be a pure virtual function.
A pure virtual function is declared like a virtual function
with one addition: it ends with = 0;. This signifies
that it is a pure virtual function and that it should not be implemented
in the .cpp file.
Any class with at least one pure virtual function is an abstract base
class. Objects cannot be created from this class since there is at
least one member function that can't be called (since we didn't implemented
it). Okay, so why create a class that can't have objects? Well, we can
still create pointers to objects from the class (they just have to
point to objects that are from classes derived from the abstract base
class). Abstract base classes can be useful for defining a common interface
for all the derived classes. In addition, they allow us to "fake"
storing all of these different, but related, objects in one container.
In our example, the abstract base class, DesktopItem is
used to provide a common interface for TextFile and
Folder objects and allows us to keep track of these
different kinds of objects in one list.
1// DesktopItem.h
2// Author: t a y l o r@msoe.edu and sections 6 of the MSOE Spring 2002 CS183 course
3// Date: 4-18-2002
4// Purpose: Defines a DesktopItem class which can be used as a base
5// for other classes that represent specific types of desktop
6// items.
7
8#ifndef DESKTOPITEM_H
9#define DESKTOPITEM_H
10
11#include <string>
12using std::string;
13
14class DesktopItem {
15public:
16 // Default constructor
17 DesktopItem(const string& name);
18
19 // Copy constructor
20 DesktopItem(const DesktopItem& old);
21
22 // Destructor
23 virtual ~DesktopItem();
24
25 // Assignment operator
26 virtual DesktopItem& operator=(const DesktopItem& rhs);
27
28 // Changes the name of the desktop item
29 void rename(const string& name);
30
31 // Returns the name of the desktop item
32 const string& getItemName() const;
33
34 // Returns the size of the desktop item (in bytes)... overridden in
35 // derived classes
36 virtual unsigned int size() const;
37
38 // Erases the connents of the desktop item... must be overridden in
39 // derived classes
40 virtual void erase() = 0; // Pure virtual function
41
42 // Dynamically allocates a copy of the desktop item and returns a pointer
43 // to the new item... must be overridden in derived classes
44 virtual DesktopItem* clone() const = 0; // Pure virtual function
45protected:
46 string itemName;
47private:
48 // Default constructor hidden from use
49 DesktopItem();
50};
51
52#endif
1// TextFile.h
2// Author: t a y l o r@msoe.edu and sections 6 of the MSOE Spring 2002 CS183 course
3// Date: 4-18-2002
4// Purpose: Defines a TextFile class which represents a text document.
5
6#ifndef TEXTFILE_H
7#define TEXTFILE_H
8
9#include <string>
10#include "DesktopItem.h"
11using std::string;
12
13class TextFile : public DesktopItem {
14public:
15 // Default constructor
16 TextFile(const string& name);
17
18 // Copy constructor
19 TextFile(const TextFile& rhs);
20
21 // Destructor
22 ~TextFile();
23
24 // Assignment operator
25 TextFile& operator=(const TextFile& rhs);
26
27 // Returns a const reference to the text contained in the TextFile object
28 const string& contents() const;
29
30 // Returns a reference to the text contained in the TextFile object
31 string& contents();
32
33 // Returns the number of the bytes required to store the text contained
34 // in the TextFile object
35 unsigned int size() const;
36
37 // Erases the text in the TextFile object
38 void erase();
39
40 // Make a dynamic copy of the current object and return a pointer to
41 // the newly created object
42 DesktopItem* clone() const;
43
44protected:
45 string text;
46
47private:
48 // Default constructor has been hidden
49 TextFile();
50};
51
52#endif
1// Folder.h
2// Author: t a y l o r@msoe.edu and sections 6 of the MSOE Spring 2002 CS183 course
3// Date: 4-18-2002
4// Purpose: Defines a Folder class which represents a file system folder
5// that stores desktop items (pointers to DesktopItem objects).
6
7#ifndef FOLDER_H
8#define FOLDER_H
9
10#include <string>
11#include <list>
12#include "DesktopItem.h"
13
14using std::string;
15using std::list;
16
17class Folder : public DesktopItem {
18public:
19 // Default constructor
20 Folder(const string& name);
21
22 // Copy constructor
23 Folder(const Folder& rhs);
24
25 // Destructor
26 ~Folder();
27
28 // Assignment operator
29 Folder& operator=(const Folder& rhs);
30
31 // Searches for a desktop item with the same name as item. If none exists,
32 // item is added to the list of desktop items and true is returned.
33 // Otherwise, it returns false.
34 bool add(const DesktopItem* item);
35
36 // Searches for a desktop item called name. If one is found, it removes
37 // it from the SuperFolder object and returns true. Otherwise, it
38 // returns false.
39 bool remove(const string& name);
40
41 // Searches for a desktop item called name. If one is found, it returns
42 // a pointer to it. Otherwise it returns null pointer.
43 DesktopItem* getItem(const string& name);
44
45 // Returns the number of bytes used by all of the items in the folder
46 unsigned int size() const;
47
48 // Removes all desktop items from the folder object
49 void erase();
50
51 // Make a dynamic copy of the current object and return a pointer to
52 // the newly created object
53 DesktopItem* clone() const;
54protected:
55 // Adds the items in the list passed in to the list in the current object
56 void add(const list<DesktopItem*>& items);
57
58 list<DesktopItem*> desktopItems;
Here we have just one list to keep track of all
different kinds of desktop items. Since a DesktopItem*
can point to DesktopItem, TextFile,
and Folder objects, we can keep track of all of these
in one list.
Even more significantly, if we were to create additional classes that
describe other kinds of desktop items (e.g., XMLFile,
MSWordFile, MP3File,
MSExcelFile, LaserPrinter, etc...),
they could all be added to a Folder object without
needing to modify the Folder class implementation at all!
(This assumes that each of these additional classes inherits the
DesktopItem class.)
|
59private:
60 // Default constructor has been hidden
61 Folder();
62};
63
64#endif
1// DesktopItem.cpp
2// Author: t a y l o r@msoe.edu
3// Date: 4-19-2002
4// Purpose: Defines a DesktopItem class which can be used as a base
5// for other classes that represent specific types of desktop
6// items.
7
8#include <cassert>
9#include "DesktopItem.h"
10
11// t a y l o r@msoe.edu, 4-19-2002
12DesktopItem::DesktopItem(const string& name) : itemName(name)
13{
14 // Nothing else to do
15}
16
17// t a y l o r@msoe.edu, 4-19-2002
18DesktopItem::DesktopItem(const DesktopItem& old) : itemName(old.itemName)
19{
20 // Nothing else to do
21}
22
23// t a y l o r@msoe.edu, 4-19-2002
24DesktopItem::~DesktopItem()
25{
26 // Nothing to do
27}
28
29// t a y l o r@msoe.edu, 4-19-2002
30DesktopItem& DesktopItem::operator=(const DesktopItem& rhs)
31{
32 if(this!=&rhs) {
33 itemName = rhs.itemName;
34 }
35 return *this;
36}
37
38// t a y l o r@msoe.edu, 4-19-2002
39void DesktopItem::rename(const string& name)
40{
41 itemName = name;
42}
43
44// t a y l o r@msoe.edu, 4-19-2002
45const string& DesktopItem::getItemName() const
46{
47 return itemName;
48}
49
50// t a y l o r@msoe.edu, 4-19-2002
51unsigned int DesktopItem::size() const
52{
53 return itemName.size();
54}
55
56// t a y l o r@msoe.edu, 4-19-2002
57DesktopItem::DesktopItem()
58{
59 assert(false);
60}
1// TextFile.cpp
2// Author: t a y l o r@msoe.edu and sections 2 and 6 of the
3// MSOE Spring 2001 CS183 course
4// Date: 4-19-2002
5// Purpose: Defines a TextFile class which represents a text document.
6
7#include <string>
8#include <cassert>
9#include "TextFile.h"
10
11using std::string;
12
13// t a y l o r@msoe.edu, 4-19-2002
14TextFile::TextFile(const string& name) : DesktopItem(name)
15{
16 // Nothing else to do
17}
18
19// t a y l o r@msoe.edu, 4-19-2002
20TextFile::TextFile(const TextFile& rhs) : DesktopItem(rhs),
In the previous constructor, the initializer list called the
DesktopItem::DesktopItem(const string& name) constructor. In
this initializer list we are calling the
DesktopItem::DesktopItem(const DesktopItem& old)
constructor. The interesting thing here is that while the constructor
takes a reference to a DesktopItem, we are really passing
a reference to a TextFile.
Generally speaking, we have forced a derived class object to "look like"
a base class object. This is known as upcasting, and it is perfectly legal
since the base class knows (at least partially) what the derived class object
can do. However, the base class only knows about the stuff in the derived
class that was inherited from the base class. Therefore, any additional
members (data or functions) that were defined in the derived class are not
available to old in the DesktopItem
constructor. This loss of data is known as object slicing.
In this case, the fact that we have lost data associated with
rhs when passing to the DesktopItem
constructor should not concern us because we are relying on the
DesktopItem constructor only to take care of the data
members inherited from the DesktopItem class. The
second item in the initializer list takes care of copying the
text data member.
Before we move on, let's consider the following alternate implementation
of the copy constructor:
TextFile::TextFile(const TextFile& rhs) : text(rhs.text)
{
itemName = rhs.itemName;
}
In this case we do not explicitly call the DesktopItem
copy constructor, instead, the default constructor from the
DesktopItem class is implicitly called before the
TextFile constructor is executed. As a result,
we need to set the value of the itemName data member within
the body of the TextFile constructor. It turns out
that this implementation is not legal for the same reason we had to call
the DesktopItem constructor in the previous constructor
implementation. Did you figure out why it wouldn't work above? In both
cases we have to have an explicit call to a DesktopItem
constructor because the default constructor for the DesktopItem
class is private (and therefore, not available to the
TextFile class). It should also be noted
that placing itemName(rhs.itemName) in the initializer list
will cause a compile error. Why do you think this results in an error?
|
21 text(rhs.text)
22{
23 // Nothing else to do
24}
25
26// t a y l o r@msoe.edu, 4-19-2002
27TextFile::~TextFile()
28{
29 // Nothing to do
30}
31
32// t a y l o r@msoe.edu, 4-19-2002
33TextFile& TextFile::operator=(const TextFile& rhs)
34{
35 if(this!=&rhs) {
36 DesktopItem::operator=(rhs);
37 text = rhs.text;
38 }
39 return *this;
40}
41
42// t a y l o r@msoe.edu, 4-19-2002
43const string& TextFile::contents() const
44{
45 return text;
46}
47
48// t a y l o r@msoe.edu, 4-19-2002
49string& TextFile::contents()
50{
51 return text;
52}
53
54// t a y l o r@msoe.edu, 4-19-2002
55unsigned int TextFile::size() const
56{
57 return DesktopItem::size() + text.size();
58}
59
60// t a y l o r@msoe.edu, 4-19-2002
61void TextFile::erase()
62{
63 text = "";
64}
65
66// t a y l o r@msoe.edu, 4-19-2002
67DesktopItem* TextFile::clone() const
68{
69 return new TextFile(*this);
70}
71
72// t a y l o r@msoe.edu, 4-19-2002
73TextFile::TextFile() : DesktopItem("")
74{
75 assert(false);
76}
1// Folder.cpp
2// Author: t a y l o r@msoe.edu
3// Date: 4-19-2002
4// Purpose: Defines a Folder class which represents a file system folder
5// that stores desktop items (pointers to DesktopItem objects).
6
7#include <string>
8#include <list>
9#include <iostream>
10#include <cassert>
11#include "Folder.h"
12using std::cerr;
13
14using std::string;
15using std::list;
16
17// t a y l o r@msoe.edu, 4-19-2002
18Folder::Folder(const string& name) : DesktopItem(name)
19{
20 // Nothing else to do
21}
22
23// t a y l o r@msoe.edu, 4-19-2002
24Folder::Folder(const Folder& rhs) : DesktopItem(rhs)
25{
26 // Make a deep copy of all of the items in the list of items
27 add(rhs.desktopItems);
28}
29
30// t a y l o r@msoe.edu, 4-19-2002
31Folder::~Folder()
32{
33 erase();
34}
35
36// t a y l o r@msoe.edu, 4-19-2002
37Folder& Folder::operator=(const Folder& rhs)
38{
39 if(this!=&rhs) {
40 DesktopItem::operator=(rhs);
41 erase();
42 add(rhs.desktopItems);
43 }
44 return *this;
45}
46
47// t a y l o r@msoe.edu, 4-19-2002
48bool Folder::add(const DesktopItem* item)
49{
50 bool found = false;
51 // Iterate through the list until itemNames match (or we get to the
52 // end of the list)
53 std::list<DesktopItem*>::const_iterator itr = desktopItems.begin();
54 while(!found && itr!=desktopItems.end()) {
55 if(item->getItemName() == (*itr)->getItemName()) {
56 found = true;
57 } else {
58 ++itr;
59 }
60 }
61 // Since no match was found, create a copy of the object that item is
62 // pointing to and a pointer to it to the desktopItems list
63 if(!found) {
64 desktopItems.push_back(item->clone());
65 }
66 return !found;
67}
68
69// t a y l o r@msoe.edu, 4-19-2002
70bool Folder::remove(const string& name)
71{
72 bool found = false;
73 std::list<DesktopItem*>::iterator itr = desktopItems.begin();
74 // Look for a match
75 while(!found && itr!=desktopItems.end()) {
76 // If a match is found, deallocate memory holding the object and
77 // remove the pointer to it from the desktopItem list
78 if(name == (*itr)->getItemName()) {
79 delete *itr;
80 desktopItems.erase(itr);
81 found = true;
82 } else {
83 ++itr;
84 }
85 }
86 return found;
87}
88
89// t a y l o r@msoe.edu, 4-19-2002
90DesktopItem* Folder::getItem(const string& name)
91{
92 DesktopItem* retVal=0;
93 std::list<DesktopItem*>::const_iterator itr = desktopItems.begin();
94 while(itr!=desktopItems.end() && name != (*itr)->getItemName()) {
95 ++itr;
96 }
97 if(itr!=desktopItems.end()) {
98 retVal = *itr;
99 }
100 return retVal;
101}
102
103// t a y l o r@msoe.edu, 4-19-2002
104unsigned int Folder::size() const
105{
106 unsigned int sz = DesktopItem::size();
107 std::list<DesktopItem*>::const_iterator itr = desktopItems.begin();
108 // Iterate through the list of desktop items and add the size of
109 // each item to the total
110 while(itr!=desktopItems.end()) {
111 sz += (*itr)->size();
112 ++itr;
113 }
114 return sz;
115}
116
117// t a y l o r@msoe.edu, 4-19-2002
118void Folder::erase()
119{
120 std::list<DesktopItem*>::iterator itr = desktopItems.begin();
121 // Free the memory where each object is actually stored.
122 while(itr!=desktopItems.end()) {
123 delete *itr;
124 ++itr;
125 }
126 // Get rid of all the pointers in the desktopItems list
127 desktopItems.clear();
128}
129
130// t a y l o r@msoe.edu, 4-19-2002
131DesktopItem* Folder::clone() const
132{
133 return new Folder(*this);
134}
135
136// t a y l o r@msoe.edu, 4-19-2002
137void Folder::add(const list<DesktopItem*>& newItems)
138{
139 std::list<DesktopItem*>::const_iterator itr = newItems.begin();
140 while(itr!=newItems.end()) {
141 desktopItems.push_back((*itr)->clone());
142 ++itr;
143 }
144}
145
146// t a y l o r@msoe.edu, 4-19-2002
147Folder::Folder() : DesktopItem("")
148{
149 assert(false);
150}