The Web development community is populated by C programmers, Perl programmers, and Visual Basic programmers, as well as Java, JavaScript, and even UNIX shell programmers. When you choose to write a Netscape Navigator plug-in, you become a C++ programmer.
When it was released, object-oriented programming was hailed as the next great step forward, but it has a reputation for a learning curve that runs six to twelve months. C++ is an object-oriented version of C-and C is a language with its own reputation for a steep learning curve.
Fortunately, many of the object-oriented aspects of a plug-in are already built into the shell that Netscape provides. By understanding how a C++ program is put together, a programmer with experience in another language can begin to write plug-ins.
Object-oriented analysis, design, and programming (collectively known as the object-oriented methods) are based on the observation that the real world is made of classes of objects, and that each class has certain things (known as functions) that it allows people to do to it. Each class also has attributes (data) that may remain relatively constant and can be changed by exercising functions.
For example, the class of chairs can be sat on. Chairs also have an associated color. The color of some chairs, such as wooden chairs, can be changed by paint. These chairs have a function, paint(), that changes their color. Other kinds of chairs-upholstered chairs-can have their color changed only by running the function, reupholster().
One big advantage of thinking about software in terms of objects is that the people who pay for software think in terms of objects. If you write a banking application, it is useful to be able to talk to the banker about objects like ATM machines, checks, and loans, rather than describing the "DebitAccount Module."
In this discussion (and in most of the object-oriented literature), a set of related objects is known as a class. As a programmer, when you make a new instance of a class, you call it instantiation. Given a chair class, you can make a whole roomful of chairs by repeatedly calling the class constructor, instantiating chair after chair.
Real-world objects are related by "a-kind-of" relationships. A wooden chair is "a-kind-of " chair. So is an "upholsteredChair." Figure 2.1 illustrates these "a-kind-of" relationships among chairs. "A-kind-of" relationships are sometimes abbreviated to AKO-relationships or are called IS-A relationships.
Developers also noted that related classes tend to allow the same kinds of functions. Figure 2.2 shows an inheritance hierarchy of dancers. Each dancer understands the function dance(), although the ballerina executes that function in a way that the line dancer may scarcely recognize.
Figure 2.2 : Related classes have similar functions, although they may use them in different ways.
This capability to call a function on an instance without worrying about exactly how the instance executes the function is referred to as polymorphism. A program could conceivably fill a room with dancers of all kinds, and then call the function dance() on each dancer. The calling program doesn't have to know the difference between tap and soft-shoe-the dancers know.
Here's a software example of polymorphism. Suppose that a software engineer is designing a computerized telephone switching system. The system will include many kinds of telephone lines: various types of digital line (including the Integrated Services Digital Network, or ISDN) as well as analog lines. When a request arrives to ring a number, however, the software can send a Ring() message to any line, with confidence that the software implementing this line will handle Ring() in the appropriate manner.
Modern software engineering practice suggests that it's a good idea to separate the specification of a piece of software from its implementation. This practice allows the designer to change his or her mind about implementation without affecting calling code-only the specification needs to remain stable.
C and C++ programs usually are divided across two files-header files, with an .h file extension, and source files, which often have a .cpp file extension in C++. It is customary to keep the class specification in a header file and the implementation in the source file.
In the following example shows the header of a class that stores a queue of characters. The file name might be charQueue.h.
class charQueue
{
public:
charQueue();
~charQueue();
void add(char c);
char remove();
class QueueEmpty
{
};
};
| Tip |
When you write class definitions, you can easily forget the trailing semicolon after the final curly brace. To avoid this mistake, take advantage of any automated layout features in your development environment. Microsoft's Visual C++, for example, has ClassWizard and Borland's C++ has ClassExpert; both of these features lay out templates for classes. |
This specification says that there are four publicly available functions. (Functions that are members of a class are also known as methods.) Two of these functions have special names.
Besides these public functions, the class also declared an additional class, used during exception processing (see "Exception Handling" later in this chapter).
The class also has some private or protected components needed for the implementation. We add these components later-they aren't important to the user of the class.
To use charQueue, a calling function makes a new instance by calling the C++ new operator with the class constructor. Then the user can add and remove characters. Finally, the user would delete the instance. This sequence (in the /source/chap02 directory on the companion CD-ROM) is shown in Listing 2.1.
Listing 2.1 main.cpp-Demo of charQueue
#include <iostream.h>
#include <stdlib.h>
#include "charQueue.h"
#include "main.h"
// defines kMaxBufferSize to be a constant short integer of size 81
void main()
{
charQueue* theQueue;
try
{
theQueue = new charQueue();
theQueue->add('f');
theQueue->add('o');
theQueue->add('o');
}
catch (...)
{
cout << "Internal error: Could not put characters into the
queue." << endl;
exit(0);
}
// now lets copy the contents of the queue into an array
char buffer[kMaxBufferSize];
short i=0; // an index into buffer
try
{
for (i=0; i < kMaxBufferSize; i++) // a C++ idiom for "do for
{
// when queue is empty, remove throws a queueEmpty exception
buffer[i] = theQueue->remove();
}
throw BufferFullException();
}
catch (charQueue::BufferFullException )
{
cout << "Internal error: buffer full." << endl;
exit(0);
}
catch (charQueue::QueueEmpty )
{
cout << "Buffer contains " << buffer << endl;
}
catch (...)
{
cout << "Internal error while reading character queue." << endl;
exit(0);
}
delete theQueue;
}
C++ Main Like C programs, C++ programs have a top-level calling routine known as main(). Like C, C++ main routines can take parameters. Unlike C, when the parameter list is empty, it is customary to put nothing between the parentheses and not to specifically put void there.
Also like C programs, C++ files usually start by including some header files. The compiler looks for files whose names are bounded by angle brackets (<, >) in the system include directories. The compiler looks for files whose names are bounded by quotes ("") in the current directory.
The main.h file shown in Listing 2.2 is used to declare a single constant. const in C++ provides a type-safe way to declare constants and is preferred in favor of old-style C macros. The main header file also holds an exception class definition needed by main. You can find main.h on the companion CD-ROM in the /source/chap02 directory.
Listing 2.2 main.h-The main Specification
#ifndef MAIN_H
#define MAIN_H
const short kMaxBufferSize = 81;
class BufferFullException
{
};
#endif
The first line in main.cpp, charQueue* theQueue; declares a pointer to a charQueue. The pointer is named theQueue. In C it's customary to put the asterisk next to the variable. This practice also is common in C++ but it's often clearer to have the asterisk follow the type name so that the line can be read "declare a charQueue pointer named theQueue."
Exception Handling Many programs are written as a series of calls to the operating system libraries. Typically, these calls either return data or an error code if there is a problem. This design means that the programmer has two choices-either to check the result of every call for an error (which takes time and clutters up the code) or to check the only errors that "can really occur." Of course, the second option leaves the program open to unexpected defects.
With the newest versions of C++, the programmer has a third choice: write the code in a straightforward way-one call after the next-but catch errors (known as exceptions) that are thrown by the code.
The general syntax of the C++ exception-handling mechanism is as follows:
try
{
// do something in here
// throw an exception if there is a problem
}
catch (which exception to catch)
{
// handle exceptions out here
}
There are two try...catch sequences in this code. The first sequence handles exceptions that occur when setting up the queue and filling it. The second sequence handles exceptions that occur while reading from the queue-the most common exception should be "Queue Empty."
The expression catch (...)
is used to denote code that catches any exception not already
caught.
| Tip |
Make your code less likely to cause a run-time failure by ensuring that all code is protected by a catch (...) clause. In this way, unexpected exceptions can be handled in the catch (...) clause rather than crashing the program. |
Note, too, that this class charQueue
has its own exception classes. Building exception classes for
every class that can throw exceptions is a good idea.
| Tip |
Most class libraries use an exception hierarchy. If you use Microsoft Foundation Classes, consider deriving your exceptions from CException. |
| Caution |
Don't confuse the C++ exception-handling described here with the so-called "C exceptions," "MFC exceptions," and "structured exceptions" offered by early versions of the MFC. These older styles were introduced before exception-handling was part of the C++ language. New code should use the C++ style shown in this chapter. |
Calling a Constructor Recall that a constructor allocates memory, performs any initialization called for in the implementation code, and returns a pointer to the new instance. The line theQueue = new charQueue(); calls the charQueue class constructor and puts the address of the new instance into the pointer variable theQueue.
Calling a Member Function C++ gives the programmer two ways to call a member function. When you have a pointer to an instance (such as charQueue), use the arrow notation (for example, charQueue->add('f');). If you have the instance itself (a dereferenced pointer), use dot notation: theActualQueue.add('f');.
Recall that add() return no value. (It is of type void.) The remove() function call returns a char, so the following assignment calls the member function and puts the resulting character into the i[sup]th slot in the buffer array:
buffer[i] = theQueue->remove();
I/O Through iostreams Note that the example includes <iostream.h>. This class defines the C++ bindings to standard input, standard output, and standard error (cin, cout, and cerr) as well as numerous other stream-oriented I/O classes and methods. Streams are useful when you are writing to disk files and other devices-in the context of plug-ins, the programmer generally uses native graphical user interface (GUI) objects and methods to interact with the user.
If you save the code fragment from charQueue.h as charQueue.h and try to build the application, you find that it fails during the link step because it cannot find any implementation of the charQueue methods. To turn this code into a runnable program, you need to make three changes:
The source code that implements charQueue.cpp is in Listing 2.3.
Listing 2.3 charQueue.cpp-One Way to Implement the Class
#include "charQueue.h"
#include <assert.h>
charQueue::listNode* charQueue::listNode::kNull = (listNode*) 0;
charQueue:: charQueue()
{
fHead = fTail = listNode::kNull;
}
charQueue::~charQueue()
{
if (listNode::kNull != fTail)
{
listNode* aPointerFromTail = fTail;
while (listNode::kNull != aPointerFromTail->fNext)
{
aPointerFromTail = aPointerFromTail->fNext;
assert (listNode::kNull != aPointerFromTail->fPrevious);
delete aPointerFromTail->fPrevious;
}
assert (listNode::kNull != aPointerFromTail);
delete aPointerFromTail;
fTail = fHead = listNode::kNull;
}
}
void charQueue::add(char aChar)
{
if (listNode::kNull == fTail)
{
fTail = new listNode();
fTail->fChar = aChar;
fHead = fTail;
}
else
{
listNode* aPointerFromHead = fHead;
while (listNode::kNull != aPointerFromHead->fPrevious)
{
aPointerFromHead = aPointerFromHead->fPrevious;
}
aPointerFromHead->fPrevious = new listNode();
aPointerFromHead->fPrevious->fChar = aChar;
aPointerFromHead->fPrevious->fNext = aPointerFromHead;
fTail = aPointerFromHead;
}
}
char charQueue::remove()
{
char theResult;
if (listNode::kNull != fHead)
{
theResult = fHead->fChar;
listNode* thePreviousNode = fHead->fPrevious;
if (listNode::kNull != thePreviousNode)
thePreviousNode->fNext = listNode::kNull;
else
fTail = listNode::kNull;
delete fHead;
fHead = thePreviousNode;
}
else
{
throw QueueEmpty();
}
return theResult;
}
charQueue::listNode::listNode()
{
fPrevious = fNext = kNull;
}
Skip over the declaration of kNull for a moment and examine the implementation of each of the four public functions. This file reveals that charQueue is implemented as a double-linked list built from dynamic memory. The constructor initializes a head and tail pointer to null. The destructor starts from the tail and works its way to the head, deleting list nodes as it goes.
The add() method builds a new list node and clips it into the growing list. Figure 2.3 shows the list as it grows from no nodes to one, two, and three nodes.
Figure 2.3 : charQueue::add( ) attaches list nodes to the linked list.
The remove() method goes to the head of the queue and reads out the character, then it deletes the head node and points the head at the next node in the queue. Figure 2.4 shows the sequence as nodes are removed from the list.
Figure 2.4 : charQueue::remove( ) takes nodes off the list from the head.
The remove method can throw one kind of exception. If a user of the class attempts to remove a character from an empty queue, remove throws QueueEmpty.
Classes have two kinds of members: functions (known as methods) and data. Listing 2.4 shows the finished specification of the charQueue class.
Listing 2.4 charQueue.h-The Complete charQueue Class Specification
#ifndef CHARQUEUE_H
#define CHARQUEUE_H
class charQueue
{
public:
charQueue();
~charQueue();
void add(char aChar);
char remove();
class QueueEmpty
{
};
private:
class listNode
{
friend class charQueue;
static listNode* kNull;
public:
listNode();
char fChar;
listNode* fPrevious;
listNode* fNext;
};
listNode* fHead;
listNode* fTail;
};
#endif
Access Restrictions and Friends The complete specification has two major sections-public and private. The language allows each member to be specified as public, protected, or private. Public members can be accessed by any other class or by stand-alone functions such as main. Public members represent the class's interface to the outside world.
Protected members are not available to outsiders, but they are accessible from descendants of the class. Private members are accessible only by the class itself.
There is one exception to this rule. If a class or a class method is declared to be a friend, this class or method can access private and protected members just as though they were public. This approach is useful when you need the protection of private or protected members, but want to "poke a hole" in the security shield to give a small number of outsiders access.
Note that this specification includes an embedded class, listNode. Because listNode is used only by charQueue, the entire class definition was embedded as a private member, and charQueue is a friend to listNode. This statement means that listNode is not known outside of charQueue, and that charQueue can only get at listNode's internals because of its status as a friend.
Access Methods Unless the class is very simple, it is common practice to make data members inaccessible from the outside world and force outsiders to use access methods. For example, listNode has three public data members: fChar, fPrevious, and fNext. The class could have been specified with these members private and access methods such as Char(), SetChar(), and so on.
Class Variables Data members are associated with each instance. If you make three different list nodes, each can have a different character in fChar. For this reason, data members are often referred to as instance variables-one set of variables is available for each instance.
Sometimes it is necessary to define class variables. There is one set of class variables for each class. To specify that a variable is a class variable, declare it to be static. kNull is declared to be a class variable and is initialized at the top of charQueue.cpp.
On Constructors and Destructors If the programmer does nothing but define a class, the compiler handles building constructors and destructors. The default constructor takes no parameters-it builds an instance of the class and initializes the data members with default values.
You often want a version of the constructor that takes parameters so that you can initialize the instance with specific values. If you pass an object as the parameter to a constructor, you can build a constructor that uses the value of the parameter as the basis for the new instance.
A special type of constructor, known as the copy constructor, takes an instance of the class as a parameter. Therefore, the programmer can write the following:
class X
{
X();
X(const X& x);
X(const Y& y);
.
.
.
}
| Note |
The notation const means that the programmer intends that the specified item not be changed throughout the scope of the const. In the context of a function call, X(const X& x); means that the programmer intends to pass a reference to an instance of class X to the constructor and promises not to use this reference to change the data members of x. The compiler enforces the programmer's promise, helping the programmer make sure that whatever he or she does to that reference later doesn't inadvertently change the contents of the const object. |
The first constructor shown is the default constructor, which can be used to make static or dynamic instances of the class. The second constructor takes an instance of class X and makes a new instance with the same values as the original. The third constructor makes a new instance of X based on the values in an instance of class Y. These constructors allow the programmer to write code like the following example:
main()
{
X theX; // invoke the default constructor.
Y theY; // the default constructor of another class
X anX = theY; // run the Y-to-X constructor
X anotherX = theX; // run the copy constructor
}
If the programmer doesn't provide an implementation of the copy constructor, the compiler generates one. Sometimes this version is acceptable-sometimes it can lead to disaster. Consider the situation in Figure 2.5.
Figure 2.5 : Using compiler-generated "shallow copy" on a class with dynamic data members.
The compiler's "shallow copy" constructor reuses the same dynamic data from the original. Here's how such a constructor behaves:
X theX("Object 1");
X anotherX = theX;
anotherX.SetName("Object 2");
cout << theX.Name() << endl;
This code fragment outputs "Object 2"-probably not what the programmer intended. The problem is that the "shallow copy" copied the pointer to the string. When the copy changed the string, the name of the original also changed.
Figure 2.6 illustrates what the programmer probably intended.
Figure 2.6 : Using "deep copy" on a class with dynamic data members.
To implement "deep copy," the programmer should write a copy constructor that makes new dynamic data members:
X::X(const X& x)
{
fName = new char[kStringSize];
strcpy(fName, x.Name());
}
| Tip |
Whenever you define a new class, consider whether you need to define your own copy constructor. If the class has data members that are pointers or references, the answer is "Yes"-define your own "deep copy" constructor. |
| Note |
If you're new to C++, you may be surprised to see more than one version of a function with the same name. For example, all of the constructors of class X are named X-they differ only in their parameters. This technique is known as overloading. C++ allows you to have as many versions of a function as you like-as long as their signature is different. The signature includes all of the parameters but not the function's return type. Therefore, you can have int X::foo (); but you cannot have char X::foo(int); because int X::foo (int); and char X::foo(int) have the same signature. N |
If you allocate memory in the constructor or during the life of the instance, you should deallocate this memory explicitly in the destructor. For example, in class X used in the preceding paragraphs, the constructors make a new array of chars on the heap. The constructor should read as follows:
X::~X
{
delete fName;
}
Don't Forget the Operators Whenever you define a copy constructor, you also nearly always need an assignment operator. The copy constructor takes care of code like the following:
X anotherX = theX;
It is not invoked, however, when you write the following:
X anotherX; anotherX = theX;
In this case, the first line runs the default constructor. The
second copy assigns the values from theX
to the existing instance anotherX.
| Tip |
After you build an assignment operator, building a copy constructor is easy, so its usually a good idea to write the assignment operator first, and then define the copy constructor in terms of the assignment operator. |
The assignment operator looks just like the preceding copy constructor except that it first tests to make sure that the user is not assigning an object to itself:
X& X::operator=(const X& rhs)
{
if (this == &rhs)
return *this;
// copy all the members in an appropriate way
strcpy(fName, rhs.fName);
}
Operators are usually called by using their symbol (for example, +, ++, =). Here the author takes the rather unusual but quite reliable approach of calling the assignment operator function directly as the copy constructor:
X::X (const X& rhs)
{
this->operator=(rhs);
}
| Note |
The copy constructor could have been implemented as follows: X::X(const X& rhs) This implementation implicitly calls the copy constructor. Paul Kimmel points out in Chapter 17 of his book, Special Edition Using Borland C++ 5 (Que, 1996), that the implicit call is too vague and may be error-prone. Kimmel's book is an excellent reference on modern C++, whether you are using Borland C++ 5 or some other compiler. |
One problem with the charQueue.cpp shown previously is that it's limited to characters. C++ is regarded as a type-safe language (although a programmer can easily overcome type-checking with typecasting). Suppose that you define a class TString. To provide a queue for strings based on class charQueue you would have to reimplement charQueue as stringQueue.
If you then wanted a queue of Tcustomers, you would need yet another queue class. Building new utility classes to deal with each new class quickly becomes tiresome, and you find yourself asking if you can somehow let the computer do the work.
In C++ the answer is "Yes." C++ allows the programmer to build templates that defer the type until compile time. Templates are based on the concept of parameterized types, which was added to C++ after the rest of the language had been developed.
Most popular implementations of C++ started using templates in 1992 or 1993, so some books on the subject call parameterized types an "advanced topic." They are not particularly "advanced" or complex, however-in fact, they are essential for many kinds of real-world applications.
Here's the remove method from charQueue, reworked so that it accepts the type as a parameter. Note that the data member fchar was renamed fdata to better reflect its new role.
template <class T>
<T> charQueue::remove()
{
T theResult;
if (listNode::kNull != fHead)
{
theResult = fHead->fdata;
listNode* thePreviousNode = fHead->fPrevious;
if (listNode::kNull != thePreviousNode)
thePreviousNode->fNext = listNode::kNull;
else
fTail = listNode::kNull;
delete fHead;
fHead = thePreviousNode;
}
else
{
throw QueueEmpty();
}
return theResult;
}
Listing 2.5 shows how to declare a parameterized class.
Listing 2.5 charQueue.H-The Parameterized Queue Class Specification
#ifndef QUEUE_H
#define QUEUE_H
class Queue
{
public:
Queue();
~Queue();
void add(T theData);
T remove();
class QueueEmpty
{
};
private:
class listNode
{
friend class Queue;
static listNode* kNull;
public:
listNode();
T fData;
listNode* fPrevious;
listNode* fNext;
};
listNode* fHead;
listNode* fTail;
};
#endif
To instantiate a template class, just pass the type to the template:
theCharQueue = new Queue<char>(); theStringQueue = new Queue<TString>(); theCustomerQueue = new Queue<TCustomer>();
The CD-ROM has a complete parameterized implementation of the
queue class in the Queue.h,
Queue.cpp, and QMain.cpp
files.
| Tip |
Templates can be used for other parameters besides type. For example, if you want to build a static vector such as buffer from main.cpp, you could specify its size as a parameter. |
| Caution |
Some C++ compilers have a difficult time deriving new classes from template classes. The classic example is a stack class derived from a vector class. Microsoft Visual C++ 4.0 does not have this problem; Borland C++ 5 does. The CD-ROM includes a program you can use to test your compiler (at /source/chap02/tmpinst/). This code also shows a fix that works in the Borland compiler. For more information about this problem and about the fix, see Chapter 19, "Using Template Classes," of Special Edition Using Borland C++ 5 (Que, 1996). |
Container classes are classes like the queue from the example, which has instances of other classes or of native data types (like char). The need for well-written container classes comes up over and over in all kinds of programs, and the parameterized types of C++ allow reusable libraries of template classes.
Most C++ compilers now come with the Standard Template Library (STL), which is just such a library of template classes. Figure 2.7 illustrates the five types of components in the STL.
Figure 2.7 : The STL uses container classes and the classes
that manipulate them.
| Tip |
Plug-ins are likely to be implemented across more than one platform, so you can lower overall costs and implementation time by building portable code. If you have STL on all of your development platforms, use it and avoid writing your own container classes. |
Containers STL offers both sequence containers and associative containers. Sequence containers are similar to the conventional array available in C, C++, and most other languages. In a sequence class, the order in which data is stored directly reflects the order in which it was added to the class. The sequence containers are as follows:
Table 2.1 shows how these classes differ from each other.
Each class is tuned for a particular type of performance. If your program does not need random access, for example, you may get a performance boost by using the list rather than the deque (pronounced "deck").
Associative containers store data based on some attribute of the data, known as a key. Using a key, the associative container class returns the data. Many associative classes are implemented by using a binary tree or a hash table, which allow data to be looked up faster in an associative class than in a sequence class.
The down side? Associative classes take up more space because they must hold the data structure that holds their keys and the pointers to the data.
Associative classes allow fast access, but require more memory
than sequence classes. Table 2.2 summarizes the features of the
various associative container classes.
A set is a good class to use if you are implementing a spell-checker dictionary. The data is the key. When the program retrieves the word from the set, there's nothing else to look up.
On the other hand, an in-memory database might be implemented by using a map. The map uses the key to fetch the data, but the data includes information not found in the key. A customer record can contain an address, buying history, and other information. The customer ID can serve as the key.
Adapters Occasionally, the nature of a problem
needs a special kind of container. STL provides three adapters
that use one of the existing sequence container classes to implement
a container with special functions. Table 2.3 shows which sequence
containers can be used as the basis for each adapter.
| Adapter Class | Vector | Deque | List |
| Stack | |||
| Queue | |||
| Priority Queue |
There also are adapters for the iterator classes, described at the end of the following section, and function objects, described at the end of the section on function objects.
Iterators To apply an algorithm to a data collection, the STL provides classes that iterate over the containers, retrieving one object at a time. These classes are referred to as iterators. STL defines five types of iterators.
Input iterators use a simple model of the data. Consider the container as a tape, with data stored in cells along the length of the tape. The tape can only move in one direction. As it moves, the data in the cell under the read-head is read out. Figure 2.8 illustrates this model.
Figure 2.8 : An input operator is read-only and unidirectional.
An output operator uses the same model but is write-only.
Forward iterators are similar to input and output iterators, but objects may be dereferenced more than once. Bidirectional iterators further extend the model, allowing the "tape" to move both forward and backward.
The most powerful iterators are random access iterators. They can do everything a bi-directional iterator can do. They also support the [] operator, which allows random access to any element in the container.
STL defines adapters for the iterator classes-classes that extend the functionality and are based on one of the five iterators described in this section. There are three iterator adapters:
Reverse iterators are just like forward iterators, but they move backward through the container. Insert iterators are specialized output operators. Continuing the tape analogy, insert operators push new cells into the middle of the tape.
The raw storage iterator is an output iterator that writes to uninitialized memory. Typically, a programmer calls new to get a chunk of raw memory, and then fills it with the raw storage iterator.
All of these iterators form a hierarchy, as shown in Figure 2.9. If an algorithm calls for, say, an input iterator, any of the "higher" iterators (that is, forward, bidirectional, or random access) work.
| Caution |
Not all containers support all iterators. Random access iterators, for example, are defined on vectors but not on deques. |
Function Objects C++ has a wealth of knowledge about native types, but this information is missing when algorithms are applied to user-defined classes. Suppose that the programmer is applying the sort algorithm to a list of TCustomer objects. There is no "natural" way to order customers. The programmer must supply a function object that can be invoked to tell the sort algorithm how to compare two different TCustomers.
STL's built-in function objects are available in three categories: arithmetic, comparison, and logical. The arithmetic operators include the following:
The built-in comparison function objects are as follows:
The logical function objects are shown in the following list:
STL also provides adapters for function objects. These adapters include negators, binders, and adapters (for pointers to functions).
Algorithms STL has over 100 built-in algorithms that can be applied to data in the containers through iterators. These algorithms are divided into four categories:
Putting It All Together-the charQueue Example in STL Listing 2.6 (on the CD-ROM in the /source/chap02directory) shows the same function as main.cpp but with STL.
Listing 2.6 stlMain.cpp-Using STL Lists, Iterators, and the Copy Algorithm
#define NOMINMAX
#include <iostream.h>
#include <algo.h>
#include <list.h>
#include <iterator.h>
typedef std::list<char> charQueue;
int main()
{
// make and populate the queue
charQueue* theQueue = new charQueue();
theQueue->push_back('f');
theQueue->push_back('o');
theQueue->push_back('o');
//associate an iterator with cout
std::ostream_iterator<char> theOutStream(cout);
//finally, copy out the list to stdout using iterators and the copy
algorithm.
std::copy (theQueue->begin(), theQueue->end(), theOutStream);
cout << endl;
delete theQueue;
return 0;
}
Note that Listing 2.6 proceeds in three steps. First, a list (with parameterized type char) is instantiated and populated. You could easily have used a deque or vector for this same purpose.
Next, a special output iterator called the ostream_iterator was set up, also parameterized for char.
Finally, copy was invoked.
Copy expects input iterators as its first two parameters-these
parameters define where in the container the copying begins and
ends. The third parameter is an output iterator-ostream_iterator
qualifies, so the contents of the list are written directly to
cout.
| Tip |
STL is shipped with Microsoft's Visual C++ but without technical support. Some of the names conflict with names in Microsoft Foundation Classes. To use STL and MFC in the same program, put STL in a separate namespace-by convention, std. If you follow Microsoft's instructions, wrap the declarations of the STL headers in a namespace declaration. If you use the FTP version, just include the STL files in the usual way. Teris has already built the namespace declaration (std) into the include files themselves. |
| On the Web |
Microsoft includes directions on how to modify the STL source so that it can be used with MFC, but a version online was modified even more thoroughly. See ftp://ftp.rahul.net/pub/teris/readme.stl and ftp://ftp.rahul.net/pub/teris/stl.zip. You also can get mstring.h from this site-mstring is a portable string class that you can use until strings are added to the standard library. |
When you are designing real-world classes, the following several issues become apparent:
Suppose that a programmer is writing code for a motor controller. The motor controller uses two types of position sensors, which are read in completely different ways. (In this example, these two types of sensors are resolvers and encoders.)
The programmer may decide that the TPositionSensor class must have a Read() method but to leave its implementation up to the derived classes. In C++, the programmer shows that a derived class can override a method by using the keyword virtual. A method is specified to have no implementation-the C++ term is pure virtual- by putting = 0 after the function declaration. Therefore,
class TPositionSensor
{
.
.
.
virtual TPosition Read() = 0;
.
.
.
}
tells the compiler to require all classes derived from TPositionSensor to define Read(). Because the compiler can never instantiate the TPositionSensor class (what would it do when someone called Read()?) TPositionSensor becomes an abstract class. (Classes that can be instantiated are called concrete classes.)
By convention, abstract classes get the word "abstract" added to their name, so TPositionSensor becomes TAbstractPositionSensor. The finished class hierarchy is shown in Figure 2.10.
| Tip |
It's a good idea to derive new classes only from abstract classes. Therefore, if the designer introduced a new kind of encoder (such as a serial encoder), he or she might change the inheritance hierarchy to the one shown in Figure 2.11 rather than the one shown in Figure 2.12. In every abstract class, the programmer should declare all the methods except constructors as virtual. This technique allows all future derived classes to override the method, incorporate it into their own version, or use it "as is" (unless it is pure virtual). Note that you can (and should) have virtual destructors, but you cannot define virtual constructors (because each class gets its own constructors anyway.) |
Figure 2.11: Derive new classes only from abstract types.
Usually inheritance is based in a real-world relationship between two classes. The class of wooden chairs, for example, really is a subset of the class of chairs. But occasionally, the relationship is more accidental.
A furniture manufacturer may have TSeat, TLegSet, and Tback classes. Clearly, TChair should not be derived from these classes because a chair isn't a subset of the classes of seats, legs, and backs. On the other hand, the programmer who already worked out methods such as TSeat::Upholster and TLegSet::Paint() may want to reuse this code in the TChair class.
One way to reuse code is to derive the new class privately from the parent classes. Therefore, the programmer might write the following:
class TChair: TSeat, TLegSet, TBack
{
public:
TChair(TSeat s, TLegSet l, TBack b) :
TSeat(s), TLegSet(l), TBack(b)
{
}
private:
// private data members are used to characterize the state of
// objects of the new class
TDecor fDecor;
TLoadCapacity fLoad;
}
Now the programmer can paint the legs or upholster the seat or
back by reusing the methods in the base classes, but the private
derivation doesn't imply that a chair is a kind of seat, back,
or legs.
| Tip |
Often a composite class has one dominant base class and one or more modifying bases. You can recognize this situation when the customer begins to use adjectives to modify nouns. The noun may correspond to the dominant base class and could be instantiated by itself. The adjectives add new features or capabilities, and do not stand alone. By convention, the dominant base class in this situation takes a T prefix. The "adjective" classes are referred to as mix-in classes and take an M prefix. Therefore, you might write the following: class TWindsorChair : private TAbstractChair, MColonialPeriod |
Private derivation is most commonly used when the composite object needs to be capable of overriding virtual methods in the base classes. For example, calling myChair->Upholster() may cause TChair to invoke a modified form of upholster for TSeat and TBack, and to invoke Paint() on the TLegSet.
Sometimes private derivation goes too far-the derived class can access protected members of the private base class. The programmer may choose to place instances of related classes into data members, clearly indicating a "has-a" relationship rather than an "is-a" relationship. The new class still can call public methods of the embedded classes, and various methods (or even the whole class) can be specified to be friends of the embedded class.
Hybrid methods, which incorporate private derivation and embedded instances, also are possible, as follows:
class TWindsorChair : private TAbstractChair, MColonialPeriod
{
public:
TWindsorChair(TBack* theBack, TLegSet* theLegSet, TSeat* theSeat);
private:
TBack* fBack;
TLegSet* fLegSet;
TSeat* fSeat;
}
The added complexity of managing objects and classes of many different sorts has lead to the adoption of design and coding style standards for C++. Little widespread agreement exists among programmers-some programmers preface class names with C, others prefer T and M as shown in this chapter. Some programmers use underscores to separate words in names-others use mixed case.
Many programmers append the so-called "Hungarian notation" to the front of their variable names. Others believe this notation unnecessarily reveals implementation details. These naysayers prefer to define classes to represent major types, such as using TAltitude to represent the altitude in feet rather than, say, an int.
The exact set of style- and naming-conventions is less important than the consistency with which they are used. For example, many programming teams adopted the rule that data members have names starting with a lowercase f.
These teams denote local variables and parameters with a and the. For example, a local variable might be called theCount or anEntry. These teams also agree among themselves that they never abbreviate and they devise consistent rules on capitalization. In this way they don't need to guess whether the variable name is fTimePeriod, ftimePeriod, ftimePd, or fTime_Period.
The code shown in this book can be readily adapted to a variety of style- and naming-standards, or can be used as a basis for a standard if an organization doesn't yet have a standard.
The examples in this chapter use simple, character-based interfaces. Real programs on Microsoft Windows, Macintosh, and UNIX systems with the X-Window System all spend much more time and attention on the user interface. Their designs reflect windows and window managers, message queues and event lists, and numerous containers, iterators, and algorithms.
The good news is that most development environments have programs like AppWizard (in Microsoft Visual C++) and AppExpert (in Borland C++) to help lay the foundation of an application, and ClassWizard and ClassExpert to fill out many of the details. Even better news is that many of the details of interfacing with the user are handled by Navigator for the programmer who writes a plug-in. Nevertheless, for a programmer new to GUIs, there is a steep learning curve.
Programmers new to one of the platforms that support Navigator plug-ins or readers who need to come up to speed on a new platform quickly may profit from one or more of the books in the following lists.
Some Windows books that can help you learn more are:
Some Macintosh books that can help you learn more are:
For UNIX, use one of the preceding books to learn the basics of C++; character-oriented programs for UNIX resemble "console" applications under Windows or the Macintosh.
An X Windows book that can help you learn more is:
Many programmers have successfully worked on Internet applications and even Web applications without using C++-or at least without using some of the more powerful features of C++, such as parameterized types, overloading, and the STL.
This chapter provided an overview of C++ for programmers coming to the language from other development environments and a review of some of the newer features for those who learned C++ early on.
For more information about related topics, see the following: