Previous | Table of Contents | Next |
Similarly, when it comes time to delete a Node, the destructor is virtual. Thus each of the delete p; statements in the Tree definition determines during execution what kind of Node is being deleted so that its constituent Trees can be (recursively) deleted.
The C++ compiler can check types in this program during compilation, despite the dynamic binding, because each use of dynamic binding is restricted to a single inheritance hierarchy. In practice, this restriction results in catching many errors during compilation that would otherwise go undetected until execution. The restriction also makes it possible for run-time selection to be extremely fast; in most applications the overhead for virtual functions is unmeasurably small.
We said earlier that dynamic binding makes it possible to add new kinds of nodes without changing all the code that uses nodes. Heres an example.
Suppose we want to add a TernaryNode to represent ternary operators such as ?: (the if-then-else operator). First we declare TernaryNode:
class TernaryNode: public Node { friend class Tree; private: const char* op; Tree left; Tree middle; Tree right; TernaryNode(const char* a, const Tree& b, const Tree& c, const Tree& d): op (a), left (b), middle (c), right (d) { } void print(ostream& o) const { o << op << ( << left << , << middle << , << right << ); } };
This declaration is similar to the declaration of BinaryNode and, in fact, started as a copy of it. Next we define a Tree constructor for a TernaryNode:
Tree::Tree(const char* op, const Tree& left, const Tree& middle, const Tree& right) { p = new TernaryNode(op, left, middle, right); }
We insert a declaration for this constructor into the class definition for Tree:
class Tree { friend class Node; friend ostream& operator<<(ostream&, const Tree&); public: Tree(int); Tree(const char*, const Tree&); Tree(const char*, const Tree&, const Tree&); Tree(const char*, const Tree, const Tree&, const Tree&); // new Tree(const Tree& t) { p = t.p; ++p->use; } ~Tree() { if (p->use == 0) delete p; } Tree& operator=(const Tree& t); private: Node* p; };
and we are done!
7.5.3.6. Assignment
Although we did not use Tree assignment in this example, we should define it anyway. Here is one way to do it:
Tree& Tree::operator=(const Tree& t) { ++t.p->use; if (--p->use == 0) delete p; p = t.p; return *this; }
Whenever we define an assignment operator, it is essential to be sure that the right thing happens when we assign an object to itself. In this case, the definition works because we are careful to increment t.p->use before we decrement p->use (which is equivalent to this->p->use). The reason is that if this points to the same object as the one to which p refers, incrementing t.p->use will ensure that p->use cannot be zero after it was decremented, so the object we are assigning is not going to be deleted while we are still using it.
C++ comes with an extensive libraryso extensive that there is not room here to describe it in detail. However, after acquiring a thorough knowledge of the base language, it is not hard to understand the library. The library does, however, rely on a few subtle concepts, so we will describe those concepts here.
What is the point of a standard library? It is impossible for a library to provide everything that every programmer might want, so a library must restrict its goals. In the case of C++, the main ideas are to offer facilities with the property that
Of course, the C++ library does not include every facility that meets one of these requirements, but most of the library is motivated by one or more of them.
Every part of the library has an associated header, which makes the relevant part of the library available to the user in namespace std. So, for example, if we want to use the I/O library, we must say
#include <iostream>
in each translation unit that uses it. There is no actual requirement that there be a file of source text that corresponds to iostream, but it is easiest to think of iostream as being a file that declares appropriate facilities and makes their definitions available to the compiler.
Because the library ordinarily lives in namespace std, it is necessary to say somehow that the program intends to use names from that namespace. So, for example, it is not sufficient to say
#include <iostream> int main() { cout << Hello, world << endl; // Wrong! }
because there is no clue that cout is really std::cout and not some other variable named cout.
Previous | Table of Contents | Next |