Implementing ADTs in C++ Classes
Steven J Zeil
1 Implementing ADTs in C++
An ADT is implemented by supplying
-
a data structure for the type name.
-
coded algorithms for the operations.
We sometimes refer to the ADT itself as the ADT specification or the ADT interface, to distinguish it from the code of the ADT implementation.
1.1 Declaring New Data Types
In the course of designing a program, we discover that we need a new data type. What are our options?
-
Use an existing type
-
Build our ADT “from scratch” by declaring a new class
-
Inherit from an existing class and make slight modifications
Use an Existing Type
On rare occasions, we may be lucky enough to have an existing type that provides exactly the capabilities we want for our new type.
typedef std::string CityName;
typedef
basically gives us a way to attach a more convenient name to an existing type.
Build our ADT “from scratch”
-
Specify exactly what data and functions we want to associate with the new ADT.
-
We do this by declaring a new class, e.g.,
class Book {
public:
Book (Author) // for books with single authors
Book (Author[], int nAuthors) // for books with multiple authors
std::string getTitle() const;
void putTitle(std::string theTitle);
int getNumberOfAuthors() const;
std::string getIsBN() const;
void putISBN(std::string id);
Publisher getPublisher() const;
void putPublisher(const Publisher& publ);
AuthorPosition begin();
AuthorPosition end();
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
⋮
};
Inherit from an existing class
In between those two extremes, we sometimes have an existing type that provides almost everything we want for our new type, but needs just a little more data or a few more functions to make it what we want.
- Create an extended version of the existing type using inheritance
class BookInSeries: public Book {
public:
std::string getSeriesTitle() const;
void putSeriesTitle(std::string theSeries);
int getVolume() const;
void putVolume(int);
private:
std::string seriesTitle;
int volume;
};
1.2 Data Members
class Book {
public:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
static const int maxAuthors = 12;
Author* authors; // array of Authors
};
- To be useful, an ADT must usually contain some internal data. These are declared as data members of the class.
Privacy
class Book {
public:
⋮
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
static const int maxAuthors = 12;
Author* authors; // array of Authors
};
1.3 Function Members
In addition to data, we can associate selected functions with our ADTs. For example, the missing access to the book data in our prior example can be provided by such function members.
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"
class Book {
public:
typedef const Author* AuthorPosition;
Book (Author); // for books with single authors
Book (const Author[], int nAuthors); // for books with multiple authors
std::string getTitle() const;
void setTitle(std::string theTitle);
int getNumberOfAuthors() const;
std::string getISBN() const;
void setISBN(std::string id);
Publisher getPublisher() const;
void setPublisher(const Publisher& publ);
AuthorPosition begin() const;
AuthorPosition end() const;
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
static const int MAXAUTHORS = 12;
Author authors[MAXAUTHORS];
};
#endif
- Now, any member functions that we declare must eventually be implemented as well by providing the appropriate bodies.
Then, in a separate file named book.cpp
we would place the function definitions (bodies).
#include "book1.h"
// for books with single authors
Book::Book (Author a)
{
numAuthors = 1;
authors[0] = a;
}
// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
numAuthors = nAuthors;
for (int i = 0; i < nAuthors; ++i)
{
authors[i] = au[i];
}
}
std::string Book::getTitle() const
{
return title;
}
void Book::setTitle(std::string theTitle)
{
title = theTitle;
}
int Book::getNumberOfAuthors() const
{
return numAuthors;
}
std::string Book::getISBN() const
{
return isbn;
}
void Book::setISBN(std::string id)
{
isbn = id;
}
Publisher Book::getPublisher() const
{
return publisher;
}
void Book::setPublisher(const Publisher& publ)
{
publisher = publ;
}
Book::AuthorPosition Book::begin() const
{
return authors;
}
Book::AuthorPosition Book::end() const
{
return authors+numAuthors;
}
void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
int i = numAuthors;
int atk = at - authors;
while (i >= atk)
{
authors[i+1] = authors[i];
i--;
}
authors[atk] = author;
++numAuthors;
}
void Book::removeAuthor (Book::AuthorPosition at)
{
int atk = at - authors;
while (atk + 1 < numAuthors)
{
authors[atk] = authors[atk + 1];
++atk;
}
--numAuthors;
}
ADTs need not be complicated
None of the functions in that ADT are particularly complex.
-
It’s a common mistake to believe that an abstract idea only “needs” to be an ADT if it is complicated.
-
Instead, an abstraction should become and ADT if doing so makes the code that uses it more expressive.
-
Most functions in a typical ADT are basic get/set attribute functions.
-
Inline Functions
Many of the member functions in this example are simple enough that we might consider an alternate approach, declaring them as inline functions, in which case the book.h
file would look something like this:
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"
class Book {
public:
typedef const Author* AuthorPosition;
Book (Author); // for books with single authors
Book (const Author[], int nAuthors); // for books with multiple authors
std::string getTitle() const { return title; }
void setTitle(std::string theTitle) { title = theTitle; }
int getNumberOfAuthors() const { return numAuthors; }
std::string getISBN() const { return isbn; }
void setISBN(std::string id) { isbn = id; }
Publisher getPublisher() const { return publisher; }
void setPublisher(const Publisher& publ) { publisher = publ; }
AuthorPosition begin() const;
AuthorPosition end() const;
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
static const int MAXAUTHORS = 12;
Author authors[MAXAUTHORS];
};
#endif
and the book.cpp
file would be reduced to this:
#include "book1.h"
// for books with single authors
Book::Book (Author a)
{
numAuthors = 1;
authors[0] = a;
}
// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
numAuthors = nAuthors;
for (int i = 0; i < nAuthors; ++i)
{
authors[i] = au[i];
}
}
Book::AuthorPosition Book::begin() const
{
return authors;
}
Book::AuthorPosition Book::end() const
{
return authors+numAuthors;
}
void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
int i = numAuthors;
int atk = at - authors;
while (i >= atk)
{
authors[i+1] = authors[i];
i--;
}
authors[atk] = author;
++numAuthors;
}
void Book::removeAuthor (Book::AuthorPosition at)
{
int atk = at - authors;
while (atk + 1 < numAuthors)
{
authors[atk] = authors[atk + 1];
++atk;
}
--numAuthors;
}
2 Special Functions
2.1 Constructors
Initializing Data
class Address {
public:
std::string getStreet() const;
void putStreet (std::string theStreet);
std::string getCity() const;
void putCity (std::string theCity);
std::string getState() const;
void putState (std::string theState);
std::string getZip() const;
void putZip (std::string theZip);
private:
std::string street;
std::string city;
std::string state;
std::string zip;
};
class Author
{
public:
std::string getName() const {return name;}
void putName (std::string theName) {name = theName;}
const Address& getAddress() const {return address;}
void putAddress (const Address& addr) {address = addr;}
long getIdentifier() const {return identifier;}
private:
std::string name;
Address address;
const long identifier;
};
-
One of the weaknesses of the ADT design shown above is that there is no easy way to initialize new address and author objects.
-
Of course, we could do it one data field at a time:
Address addr;
addr.putStreet ("21 Pennsylvania Ave.");
addr.putCity ("Washington");
addr.putState ("D.C.");
addr.putZip ("10001");
Author doe;
doe.putName ("Doe, John");
doe.putAddress (addr);
Problems with Field-By-Field Initialization
-
It would be quite easy to forget to initialize one or more of the data fields.
-
As the code gets modified over the course of the project, some misguided programmer might place some additional lines of code in between these.
That leads to a real possibility of our using addr
before all the data fields have been initialized.
-
There’s no way to initialize the Author’s
identifier
data.-
We don’t have a
putIdentifier
function because we did not want to allow arbitrary changes to that data. -
Note that the
Author::identifier
field is declared asconst
.
-
-
Last, but not least, the process just takes too long - too many lines of code written just to initialize one data object.
Constructor Functions
C++ provides a special kind of member function to streamline the initialization process. It’s called a constructor.
Suppose we add a constructor to each of our Address and Author classes.
class Address {
public:
Address (std::string theStreet, std::string theCity,
std::string theState, std::string theZip);
std::string getStreet() const;
void putStreet (std::string theStreet);
std::string getCity() const;
void putCity (std::string theCity);
std::string getState() const;
void putState (std::string theState);
std::string getZip() const;
void putZip (std::string theZip);
private:
std::string street;
std::string city;
std::string state;
std::string zip;
};
class Author
{
public:
Author (std::string theName, Address theAddress, long id);
std::string getName() const {return name;}
void putName (std::string theName) {name = theName;}
const Address& getAddress() const {return address;}
void putAddress (const Address& addr) {address = addr;}
long getIdentifier() const {return identifier;}
private:
std::string name;
Address address;
const long identifier;
};
Declaring Variables with Constructors
Then we can create a new author object much more easily:
Address addr ("21 Pennsylvania Ave.",
"Washington",
"D.C.", "10001");
Author doe ("Doe, John", addr, 1230157);
or, since the addr
variable is probably only there only for the purpose of initializing this author and is probably not used elsewhere, we can do:
Author doe ("Doe, John",
Address (
"21 Pennsylvania Ave.",
"Washington",
"D.C.", "10001"),
1230157);
Implementing Constructors
The implementation of this constructor is pretty straightforward.
Address::Address
(std::string theStreet, std::string theCity,
std::string theState, std::string theZip)
{
street = theStreet;
city = theCity;
state = theState;
zip = theZip;
}
Initialization Lists
The implementation of the Author
constructor has one complication.
Author::Author (std::string theName,
Address theAddress, long id)
: identifier (id)
{
name = theName;
address = theAddress;
}
- We can’t say
identifier = id;
in the function body, because identifier
was declared as being const
and so cannot be assigned to.
Example: Alternate Constructors
Author::Author (std::string theName,
Address theAddress, long id)
: identifier (id)
{
name = theName;
address = theAddress;
}
or
Author::Author (std::string theName,
Address theAddress, long id)
: name(theName), address(theAddress),
identifier(id)
{
}
The second constructor might run slightly faster.
2.2 Destructors
The flip-side of initializing new objects is cleaning up when old objects are going away.
A destructor for the class Foo
is a function of the form
~Foo();
Destructors are never called explicitly. Instead the compiler generates a call to an object’s destructor for us.
The Compiler Calls a Destructor when…
- Execution passes outside of the
{ ... }
within which the object was declared. For example, if we wrote
if (someTest)
{
Book b = text361;
cout << b.getTitle() << endl;
}
what the compiler would actually generate would be something along the lines of
if (someTest)
{
Book b = text361;
cout << b.getTitle() << endl;
b.~Book(); // implicitly generated by the compiler
}
The Compiler Calls a Destructor when…
- When we
delete
a pointer to an object, the object’s destructor is (implicitly) called prior to actually recovering the memory occupied by the object. For example, if we were to write:
Book *bPointer = new Book(text361); // initialized using copy constructor
⋮
cout << bPointer->getTitle() << endl;
delete bPointer;
what the compiler would actually generate would be something along the lines of
Book *bPointer = new Book(text361); // initialized using copy constructor
⋮
cout << bPointer->getTitle() << endl;
bPointer->~Book();
free(bPointer); // recover memory at the address in bPointer
Inside a Destructor
The most common uses for destructors is to clean up allocated memory for an object that is about to disappear.
- If we have used
new
to allocate any memory on the heap for one or more data members of the object,- we usually
delete
that memory in the destructor.
- we usually
2.3 Operators
Almost every operator in the language can be declared in our own classes.
Almost all of the things we use as operators,
- including
+ - * / | & < > <= >= = == != ++ -- -> += -= *= /=
, - and
the [ ]
used in array indexing and the ( )
used in function calls, * but not .
or ::
,
are actually “syntactic sugar” for a function whose name if formed by appending the operator itself to the word “operator
”.
Operators as Shorthand
For example,
a + b*(-c)
is actually just a shorthand for
operator+(a, operator*(b, operator-(c)))
- and if you write
testValue = (x <= y);
that is a shorthand for
operator=(testValue, operator<=(x, y);
Declaring Operators
Book operator+ (const Book& left, const Book& right);
and then call it like this:
Book b = book1 + book2;
but that’s probably not a good idea, because it’s not clear just what it means to add two books together.
There are, however, a few operators that would make sense for Book
and for almost all ADTs.
Assignment
Chief among these is the assignment operator.
- When we write
book1 = book2
, that’s shorthand forbook1.operator=(book2)
We’ll look more closely at when and how to write our own assignment operators in the next lesson.
I/O
Another, very common set of operators that programmers often write for their own code are the I/O operators <<
and >>
, particularly the output operator <<
.
Output Operator Example
Here’s a possible output routine for our Book
class.
ostream& operator<< (ostream& out, const Book& b)
{
out << b.getISBN() << "\n";
out << b.getTitle() << "\n";
for (AuthorPosition current = b.begin(); current != b.end(); ++current)
{
Author au = *current;
out << au << ", ";;
}
out << "\n published by" << b.getPublisher();
out << endl;
return out;
}
- This assumes that we have implemented an output operator for Author.
- But if you bought into the idea of providing an output operator for every class you write, that one should already exist.
Output Operator Example
ostream& operator<< (ostream& out, const Book& b)
{
out << b.getISBN() << "\n";
out << b.getTitle() << "\n";
for (AuthorPosition current = b.begin(); current != b.end(); ++current)
{
Author au = *current;
out << au << ", ";;
}
out << "\n published by" << b.getPublisher();
out << endl;
return out;
}
- The return statement at the end returns the output stream to which we are writing.
let's us write "chains" of output expressions like:
cout << book1 << " is better than "
<< book2 << endl;
which is treated by the compiler as if we had written:
(((cout << book1) << " is better than ")
<< book2) << endl;
so that each output operation, in turn, passes the stream on to the next one in line.
Comparisons
After assignments and I/O, the most commonly programmed operators would be the relational operators, especially ==
and <
.
class Address
{
⋮
bool operator== (const Address&) const;
⋮
};
The trickiest thing about providing these operators is making sure we understand just what they should mean for each individual ADT.
Equality via All Data Members Equal
This would be a reasonable implementation:
bool Address::operator== (const Address& right) const
{
return (street == right.street)
&& (city == right.city)
&& (state == right.state)
&& (zip == right.zip);
}
We could do something similar for Author
:
bool Author::operator== (const Author& right) const
{
return (name == right.name)
&& (address == right.address)
&& (identifier == right.identifier);
}
(which, interestingly, makes use of the Address::operator==
that we have just defined).
Equality via “keys”
But there’s actually another choice that might be reasonable. If every author has a unique, unchanging identifier, we might be able to just say:
bool Author::operator== (const Author& right) const
{
return (identifier == right.identifier);
}
on the grounds that two Author
objects with the same identifier value must actually be describing the same person, even if they are inconsistent in the other fields.
- That’s a different meaning for
==
.- Neither of these two possible meanings is obviously better or “more correct” than the other.
- In a real project, we would need to look hard at how we will use these objects to decide what meaning is appropriate for
==
.
How Many Relational Ops do we Need?
-
In C++ we usually provide only operators
==
and<
-
Many
std::
functions use these two, but none rely on the other 4 (!= > >= <=
).
-
-
The standard library contains functions defining each of these 4 additional relational operators in terms of
==
and<
.-
We get these additional operators by simply saying
-
using namespace std::rel_ops;
Designing a Good <
In C++, we traditionally provide operator<
whenever possible
Again, just what this should mean depends upon the abstraction we are trying to support, but
-
If
x < y
is true, theny < x
andx == y
must be false. -
If
x == y
is true, thenx < y
andy < x
must be false. -
If
x == y
is false, then eitherx < y
ory < x
(but not both) must be true. In other words, given any two values x and y, one and exactly one of the relationsx < y
,y < x
, andx == y
should be true.
Example: Comparing Authors
For example, this would be a reasonable pair of comparison operators for Author
s:
bool Author::operator== (const Author& right) const
{
return (identifier == right.identifier);
}
bool Author::operator< (const Author& right) const
{
return (identifier < right.identifier);
}
Example: Comparing Addresses
Here is a reasonable pair for Address
:
bool Address::operator== (const Address& right) const
{
return (street == right.street)
&& (city == right.city)
&& (state == right.state)
&& (zip == right.zip);
}
bool Address::operator< (const Address& right) const
{
if (street < right.street)
return true;
else if (street == right.street)
{
if (city < right.city)
return true;
else if (city == right.city)
{
if (state < right.state)
return true;
else if (state == right.state)
{
if (zip < right.zip)
return true;
}
}
}
return false;
}
3 Example: Multiple Implementations of Book
One of the characteristics we expect when working with ADTs is that the implementing data structures and algorithms can be altered without changing the interface.
3.1 Simple Arrays
The authors are stored in an array of fixed size, statically allocated as a part of the Book
object.
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"
class Book {
public:
typedef const Author* AuthorPosition;
Book (Author); // for books with single authors
Book (const Author[], int nAuthors); // for books with multiple authors
std::string getTitle() const { return title; }
void setTitle(std::string theTitle) { title = theTitle; }
int getNumberOfAuthors() const { return numAuthors; }
std::string getISBN() const { return isbn; }
void setISBN(std::string id) { isbn = id; }
Publisher getPublisher() const { return publisher; }
void setPublisher(const Publisher& publ) { publisher = publ; }
AuthorPosition begin() const;
AuthorPosition end() const;
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
static const int MAXAUTHORS = 12;
Author authors[MAXAUTHORS];
};
#endif
Adding and Removing
- The code to add and remove authors is rather typical array manipulation code.
#include "book1.h"
// for books with single authors
Book::Book (Author a)
{
numAuthors = 1;
authors[0] = a;
}
// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
numAuthors = nAuthors;
for (int i = 0; i < nAuthors; ++i)
{
authors[i] = au[i];
}
}
Book::AuthorPosition Book::begin() const
{
return authors;
}
Book::AuthorPosition Book::end() const
{
return authors+numAuthors;
}
void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
int i = numAuthors;
int atk = at - authors;
while (i >= atk)
{
authors[i+1] = authors[i];
i--;
}
authors[atk] = author;
++numAuthors;
}
void Book::removeAuthor (Book::AuthorPosition at)
{
int atk = at - authors;
while (atk + 1 < numAuthors)
{
authors[atk] = authors[atk + 1];
++atk;
}
--numAuthors;
}
3.2 Dynamically Allocated Arrays
A more flexible approach can be obtained by allocating the array of authors on the heap.
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"
class Book {
public:
typedef const Author* AuthorPosition;
Book (Author); // for books with single authors
Book (const Author[], int nAuthors); // for books with multiple authors
std::string getTitle() const { return title; }
void setTitle(std::string theTitle) { title = theTitle; }
int getNumberOfAuthors() const { return numAuthors; }
std::string getISBN() const { return isbn; }
void setISBN(std::string id) { isbn = id; }
Publisher getPublisher() const { return publisher; }
void setPublisher(const Publisher& publ) { publisher = publ; }
AuthorPosition begin() const;
AuthorPosition end() const;
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
int MAXAUTHORS;
Author* authors;
};
#endif
In the .h file, the statically sized array authors
is replaced by a pointer to an (array of) Author
.
Dynamically Allocated Arrays (cont.)
#include "book2.h"
// for books with single authors
Book::Book (Author a)
{
MAXAUTHORS = 4;
authors = new Author[MAXAUTHORS];
numAuthors = 1;
authors[0] = a;
}
// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
MAXAUTHORS = 4;
authors = new Author[MAXAUTHORS];
numAuthors = nAuthors;
for (int i = 0; i < nAuthors; ++i)
{
authors[i] = au[i];
}
}
Book::AuthorPosition Book::begin() const
{
return authors;
}
Book::AuthorPosition Book::end() const
{
return authors+numAuthors;
}
void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
if (numAuthors >= MAXAUTHORS)
{
Author* newAuthors = new Author[2*MAXAUTHORS];
for (int i = 0; i < MAXAUTHORS; ++i)
newAuthors[i] = authors[i];
MAXAUTHORS *= 2;
delete [] authors;
authors = newAuthors;
}
int i = numAuthors;
int atk = at - authors;
while (i > atk)
{
authors[i] = authors[i-1];
i--;
}
authors[atk] = author;
++numAuthors;
}
void Book::removeAuthor (Book::AuthorPosition at)
{
int atk = at - authors;
while (atk + 1 < numAuthors)
{
authors[atk] = authors[atk + 1];
++atk;
}
--numAuthors;
}
The code is still pretty straightforwardly array-based.
-
The constructors are changed to now allocate the array on the heap.
-
The array size allocated is smaller, because of a more sophisticated approach used in
addAuthor
.-
There, before adding a new author to the book, we make a check to see if the array is already full.
-
If it is, we allocate a new, larger array, copy the former list of authors from the old array into the new one, discard the old array, and then proceed to add the new author into the new, larger array.
-
3.3 Linked Lists
A still more flexible approach can be obtained by using a linked list of authors. In this case, I have chosen a doubly-linked list (pointers going both forward and backward from each node).
In this approach, each book object occupies many blocks of memory, most of them being linked list nodes allocated on the heap.
#ifndef BOOK_H
#include "author.h"
#include "authoriterator.h"
#include "publisher.h"
class Book {
public:
typedef AuthorIterator AuthorPosition;
Book (Author); // for books with single authors
Book (const Author[], int nAuthors); // for books with multiple authors
std::string getTitle() const { return title; }
void setTitle(std::string theTitle) { title = theTitle; }
int getNumberOfAuthors() const { return numAuthors; }
std::string getISBN() const { return isbn; }
void setISBN(std::string id) { isbn = id; }
Publisher getPublisher() const { return publisher; }
void setPublisher(const Publisher& publ) { publisher = publ; }
AuthorPosition begin() const;
AuthorPosition end() const;
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
AuthorNode* first;
AuthorNode* last;
friend class AuthorIterator;
};
#endif
The Book
itself holds pointers to the first and the last node in the chain.
Linked Lists impl
#include "authoriterator.h"
#include "book3.h"
// for books with single authors
Book::Book (Author a)
{
numAuthors = 1;
first = last = new AuthorNode (a, 0, 0);
}
// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
numAuthors = 0;
first = last = 0;
for (int i = 0; i < nAuthors; ++i)
{
addAuthor(end(), au[i]);
}
}
Book::AuthorPosition Book::begin() const
{
return AuthorPosition(first);
}
Book::AuthorPosition Book::end() const
{
return AuthorPosition(0);
}
void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
if (first == 0)
first = last = new AuthorNode (author, 0, 0);
else if (at.pos == 0)
{
last->next = new AuthorNode (author, last, 0);
last = last->next;
}
else
{
AuthorNode* newNode = new AuthorNode(author, at.pos->prev, at.pos);
at.pos->prev = newNode;
if (at.pos == first)
first = newNode;
else
newNode->prev->next = newNode;
}
++numAuthors;
}
void Book::removeAuthor (Book::AuthorPosition at)
{
if (at.pos == first)
{
if (first == last)
first = last = 0;
else
{
first = first->next;;
first->prev = 0;
}
}
else if (at.pos == last)
{
last = last->prev;
last->next = 0;
}
else
{
AuthorNode* prv = at.pos->prev;
AuthorNode* nxt = at.pos->next;
prv->next = nxt;
nxt->prev = prv;
}
delete at.pos;
--numAuthors;
}
The code is considerably messier - pointer manipulation can be tricky, no matter how many times you do it.
- The code is quite efficient however.
- Adding and removing nodes remains quick no matter how many authors are actually in the list.
3.4 Standard List
We presume that the data in this approach is arranged pretty much as before, though we don’t really know that for sure because we trust the abstraction provided by the std::list
ADT.
#ifndef BOOK_H
#include <list>
#include "author.h"
#include "publisher.h"
class Book {
public:
typedef std::list<Author>::const_iterator AuthorPosition;
typedef std::list<Author>::const_iterator const_AuthorPosition;
Book (Author); // for books with single authors
Book (const Author[], int nAuthors); // for books with multiple authors
std::string getTitle() const { return title; }
void setTitle(std::string theTitle) { title = theTitle; }
int getNumberOfAuthors() const { return numAuthors; }
std::string getISBN() const { return isbn; }
void setISBN(std::string id) { isbn = id; }
Publisher getPublisher() const { return publisher; }
void setPublisher(const Publisher& publ) { publisher = publ; }
const_AuthorPosition begin() const;
const_AuthorPosition end() const;
AuthorPosition begin();
AuthorPosition end();
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
std::list<Author> authors;
};
#endif
#include "authoriterator.h"
#include "book4.h"
// for books with single authors
Book::Book (Author a)
{
numAuthors = 1;
authors.push_back(a);
}
// for books with multiple authors
Book::Book (const Author au[], int nAuthors)
{
numAuthors = nAuthors;
for (int i = 0; i < nAuthors; ++i)
{
authors.push_back(au[i]);
}
}
Book::const_AuthorPosition Book::begin() const
{
return authors.begin();
}
Book::const_AuthorPosition Book::end() const
{
return authors.end();
}
Book::AuthorPosition Book::begin()
{
return authors.begin();
}
Book::AuthorPosition Book::end()
{
return authors.end();
}
void Book::addAuthor (Book::AuthorPosition at, const Author& author)
{
authors.insert (at, author);
++numAuthors;
}
void Book::removeAuthor (Book::AuthorPosition at)
{
authors.erase (at);
--numAuthors;
}
The code is quite simple, at least for anyone already familiar with the std::list
ADT.
4 Example: Implementing Book::AuthorPosition
Our Book
ADT calls for a “helper” ADT, the AuthorPosition
, to represent the position of a particular author within the author list for the book.
Iterator Interface Review
To review, our iterator must supply the following operations:
class AuthorPosition {
public:
AuthorPosition();
// get data at this position
Author operator*() const;
// get a data/function member at this position
Author* operator->() const;
// move forward to the position just after this one
AuthorPosition operator++();
// Is this the same position as pos?
bool operator== (const AuthorPosition& pos) const;
bool operator!= (const AuthorPosition& pos) const;
};
4.1 Simple Arrays
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"
class Book {
public:
typedef const Author* AuthorPosition;
Book (Author); // for books with single authors
Book (const Author[], int nAuthors); // for books with multiple authors
std::string getTitle() const { return title; }
void setTitle(std::string theTitle) { title = theTitle; }
int getNumberOfAuthors() const { return numAuthors; }
std::string getISBN() const { return isbn; }
void setISBN(std::string id) { isbn = id; }
Publisher getPublisher() const { return publisher; }
void setPublisher(const Publisher& publ) { publisher = publ; }
AuthorPosition begin() const;
AuthorPosition end() const;
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
static const int MAXAUTHORS = 12;
Author authors[MAXAUTHORS];
};
#endif
If we implement Book
using simple arrays, the implementation of AuthorPosition
is trivial.
It takes advantage of the fact that the conventional interface for iterators in C++ is chosen to mimic the behavior of pointers to array elements.
Iterators and Arrays
The only differences lie in how one gets access to the starting and ending positions.
T a[N]; |
Container c; |
|
---|---|---|
get element at position | *p p-> |
*it it-> |
move forward 1 position | ++p |
++it |
compare positions | p1 == p2 |
it1 == it2 |
p1!= p2 |
it1 != it2 |
|
position of first element | a |
c.begin() |
position just after last element | a+N |
c.end() |
Book (Array) Iterator
class Book {
public:
typedef const Author* AuthorPosition;
⋮
};
Book::AuthorPosition Book::begin() const
{
return authors;
}
Book::AuthorPosition Book::end() const
{
return authors + numAuthors;
}
The pointer type already provides the *
, ->
, ++
, ==
, and !=
operations we need.
4.2 Dynamically Allocated Arrays
#ifndef BOOK_H
#include "author.h"
#include "publisher.h"
class Book {
public:
typedef const Author* AuthorPosition;
Book (Author); // for books with single authors
Book (const Author[], int nAuthors); // for books with multiple authors
std::string getTitle() const { return title; }
void setTitle(std::string theTitle) { title = theTitle; }
int getNumberOfAuthors() const { return numAuthors; }
std::string getISBN() const { return isbn; }
void setISBN(std::string id) { isbn = id; }
Publisher getPublisher() const { return publisher; }
void setPublisher(const Publisher& publ) { publisher = publ; }
AuthorPosition begin() const;
AuthorPosition end() const;
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
int MAXAUTHORS;
Author* authors;
};
#endif
Everything we have said about iterators over simple arrays applies equally well to dynamic arrays.
So the implementation of AuthorPostion is identical:
class Book {
public:
typedef const Author* AuthorPosition;
⋮
4.3 Linked Lists
#ifndef BOOK_H
#include "author.h"
#include "authoriterator.h"
#include "publisher.h"
class Book {
public:
typedef AuthorIterator AuthorPosition;
Book (Author); // for books with single authors
Book (const Author[], int nAuthors); // for books with multiple authors
std::string getTitle() const { return title; }
void setTitle(std::string theTitle) { title = theTitle; }
int getNumberOfAuthors() const { return numAuthors; }
std::string getISBN() const { return isbn; }
void setISBN(std::string id) { isbn = id; }
Publisher getPublisher() const { return publisher; }
void setPublisher(const Publisher& publ) { publisher = publ; }
AuthorPosition begin() const;
AuthorPosition end() const;
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
AuthorNode* first;
AuthorNode* last;
friend class AuthorIterator;
};
#endif
To get the desired behaviors for our iterator ADT, we will need to implement it as a class in its own right. The declaration shown here provides our desired interface.
class AuthorIterator {
public:
AuthorIterator ();
Author operator*() const;
const Author* operator->() const;
const AuthorIterator& operator++(); // prefix form ++i;
AuthorIterator operator++(int); // postfix form i++;
bool operator== (const AuthorIterator& ai) const;
bool operator!= (const AuthorIterator& ai) const;
private:
AuthorNode* pos;
AuthorIterator (AuthorNode* p)
: pos(p)
{}
friend class Book;
};
Inside the Book
class, we write
class Book {
public:
typedef AuthorIterator AuthorPosition;
⋮
friend class AuthorIterator;
};
which gives AuthorPosition
access to all private members of Book
.
Implementing the Iterator Ops
For example, we implement the *
operator like this:
Author AuthorIterator::operator*() const
{
return pos->au;
}
returning the data field (only) from the linked list node.
Implementing the Iterator Ops (cont.)
The other particularly interesting operator is ++
:
// prefix form ++i;
const AuthorIterator& AuthorIterator::operator++()
{
pos = pos->next;
return *this;
}
// postfix form i++;
AuthorIterator AuthorIterator::operator++(int)
{
AuthorIterator oldValue = *this;
pos = pos->next; return oldValue;
}
The main thing that happens here is simple advancing the pos pointer to the next linked list node.
4.4 Standard List
#ifndef BOOK_H
#include <list>
#include "author.h"
#include "publisher.h"
class Book {
public:
typedef std::list<Author>::const_iterator AuthorPosition;
typedef std::list<Author>::const_iterator const_AuthorPosition;
Book (Author); // for books with single authors
Book (const Author[], int nAuthors); // for books with multiple authors
std::string getTitle() const { return title; }
void setTitle(std::string theTitle) { title = theTitle; }
int getNumberOfAuthors() const { return numAuthors; }
std::string getISBN() const { return isbn; }
void setISBN(std::string id) { isbn = id; }
Publisher getPublisher() const { return publisher; }
void setPublisher(const Publisher& publ) { publisher = publ; }
const_AuthorPosition begin() const;
const_AuthorPosition end() const;
AuthorPosition begin();
AuthorPosition end();
void addAuthor (AuthorPosition at, const Author& author);
void removeAuthor (AuthorPosition at);
private:
std::string title;
int numAuthors;
std::string isbn;
Publisher publisher;
std::list<Author> authors;
};
#endif
If we use a std::list
to keep our authors, the implementation of our iterator becomes fairly simple again.
- That’s because all the
std
containers provide their own iterators- (which, obviously, will follow the
std
interface), and so - we can simply implement our own iterator in terms of that one:
- (which, obviously, will follow the
class Book {
public:
typedef std::list<Author>::const_iterator AuthorPosition;
typedef std::list<Author>::const_iterator const_AuthorPosition;
Implementing the Iterator Ops
Implementing the begin()
and end()
functions is pretty straightforward.
Book::const_AuthorPosition Book::begin() const
{
return authors.begin();
}
Book::const_AuthorPosition Book::end() const
{
return authors.end();
}
Book::AuthorPosition Book::begin()
{
return authors.begin();
}
Book::AuthorPosition Book::end()
{
return authors.end();
}
.