class counter { int n; public: counter() { n = 0; } void operator++() { n = n + 1; } int value() { return n; } };
counter c; c++; cout << c.value();the first thing to note is that a counter is created by declaring the variable c to be of type counter. Next, we see that the counter c is incremented (by one), and finally, the value of the counter is written to standard output.
class counter { public: counter(int v = 0 ) : n(v) { init("default"); } counter(char* s, int v=0) : n(v) { init(s); } ~counter() { delete[] id; } char* name() { return id; } void operator++() { n = n + 1; } int value() { return n; } private: int n; char* id; void init(char* s) { id = new char[strlen(s)+1]; strcpy(id, s); } };
counter(int v = 0 ) : n(v) { init("default"); }
counter c("ctr-1"); c++; cout << c.name() << " = " << c.value();Apart from a member function name which returns the id of the counter, we also encounter a destructor for a counter object, defined as
class counter { public: counter(int v = 0 ); counter( char* s, int v = 0 ); ~counter() { delete[] id; } const char* name() const { return id; } void operator++() { n = n + 1; } int value() const { return n; } private: int n; char* id; void init(char* s); };
counter c("ctr-2"); c++; c.name()[4]='1'; cout << c.name() << " = " << c.value();
class counter { public: counter(int v = 0 ) : n(v), id("default") { } counter( char* s, int v = 0 ); ~counter() { delete[] id; } const char* name() { return id; } void operator++() { n = n + 1; } operator int() { return n; } operator char*() { return id; } private: int n; char* id; };
void fun( counter& c ) { cout << (char*) c << " = " << (int) c; }
fun("ctr-3");
shape s;would result in a compiler error. Having an abstract class shape available, we may define concrete shapes, such as circle and rectangle, as in slide 2-concrete.// error: abstract class
class circle : public shape {\fbox{circle}
public: circle( int x, int y, int r) : shape(x,y), _radius(r) { } void draw() { cout << "C:" << _x << _y << _radius; } protected: int _radius; }; class rectangle : public shape {\fbox{rectangle}
public: rectangle( int x, int y, int l, int r ) : shape(x,y), _l(l), _r(r) { } void draw() { cout << "R:" << _x << _y << _l << _r; } protected: int _l,_r; };
A code fragment illustrating the use of concrete shapes looks
as follows:
circle c(1,1,2); rectangle r(2,2,1,1);
c.draw(); r.draw();
To illustrate the power of virtual functions (and dynamic binding) we will add a compound shape to our hierarchy of shapes. See slide 2-compound.
class compound : public shape {\ifsli{}{\fbox{compound}}
public: compound( shape* s = 0 ) : fig(s) { next = 0; } void add( shape* s ) { if (next) next->add(s); else next = new compound(s); } void move(int x, int y) { if (fig) fig->move(x,y); if (next) next->move(x,y); } void draw() { if (fig) fig->draw(); if (next) next->draw(); } private: shape* fig; compound* next; };
As an example of the use of a compound shape, consider the following fragment:
compound s; s.add( new circle(1,1,2) ); s.add( new rectangle(2,2,3,5) ); s.draw(); s.move(7,7); s.draw();After creating an empty compound shape, two shapes, respectively a circle and a rectangle, are added. The compound shape is asked to draw itself, it is moved, and then asked to draw itself again. The compound shape object, when moving and drawing the list of shapes, has no knowledge of what actual shapes are contained in the list, which may be compound shapes themselves. This illustrates how we may achieve polymorphic behavior by using inheritance.
A more explicit example of the polymorphic behavior of shapes is given by the following code fragment.
shape* fig[3]; fig[0] = &s;After storing some actual shapes, including a compound shape, in an array of (pointers to) shapes, a simple loop with a uniform request for drawing is sufficient to display all the shapes contained in the array, independent of their actual type.the compound shape
fig[1] = new circle(3,3,5); fig[2] = new rectangle(4,4,5,5); for( int i = 0; i < 3; i++ ) fig[i]->draw();
This example is often used to demonstrate that when adopting an object-oriented approach the programmer no longer needs to include lengthy case statements to choose between the various drawing operations on the basis of an explicit type tag.
The careful reader may have noted that the absence of the declaration virtual for the member function move may lead to problems. Indeed, this leads to erroneous behavior since moving only the origin of the compound shape will not do. In our slightly wasteful implementation of a compound shape, the member variables inherited from shape play no role. Instead, each shape in the list must be moved. This could be repaired either by declaring the function as virtual or by redefining and eliminating . This illustrates that it takes careful consideration to decide whether or not to make a member function virtual. Some even suggest making member functions virtual by default, unless it is clear that they may be declared non-virtual.
class student { ... }; class assistant { ... }; class student_assistant : public student, public assistant { public: student_assistant( int id, int sal ) : student(id), assistant(sal) {} };
Dynamic binding for instances of a class derived by multiple inheritance works in the same way as in the case of single inheritance. However, ambiguities between member function names must be resolved by the programmer.
class person { }; class student : virtual public person { ... } class assistant : virtual public person { ... } class student_assistant : public student, public assistant { ... };
To ensure that {\em student_assistant}
contains only one copy of the person
class, both the student and assistant
classes must indicate that the person
is inherited in a virtual manner.
Otherwise, we may not have a declaration of the form
person* p = new student_assistant(20,6777,300);
In the example in slide 2-assertions, assertions are used to check for the satisfaction of both the pre- and post-conditions of a function that computes the square root of its argument, employing a method known as Newton iteration.
double sqrt( double arg ) {\fbox{sqrt}
require ( arg >= 0 ); double r=arg, x=1, eps=0.0001; while( fabs(r - x) > eps ) { r=x; x=r-((r*r-arg)/(2*r)); } promise ( r - arg * arg <= eps ); return r; }
Whereas Eiffel directly supports the use of assertions by allowing access to the value of an instance variable before the execution of a method through the keyword old, the C++ programmer must rely on explicit programming to be able to compare the state before an operation with the state after the operation.
class counter {\fbox{counter}
public: counter(int n = 0) : _n(n) { require( n >= 0 ); promise( invariant() );\c{// check initial state}
} virtual void operator++() { require( true );\c{// empty pre-condition}
hold();\c{// save the previous state}
_n += 1; promise( _n == old_n + 1 && invariant() ); } int value() const { return _n; }\c{// no side effects}
virtual bool invariant() { return value() >= 0; } protected: int _n; int old_n; virtual void hold() { old_n = n; } };
Assertions may also be used to check whether the object is correctly initialized. The pre-condition stated in the constructor requires that the counter must start with a value not less than zero. In addition, the constructor checks whether the class invariant, stated in the (virtual) member function invariant, is satisfied. Similarly, after checking whether the post-condition of the function is true, the invariant is checked as well.
class bounded : public counter {\fbox{bounded}
public: bounded(int b = MAXINT) : counter(0), max(b) {} void operator++() { require( value() < max() );\c{// to prevent overflow}
counter::operator++(); } bool invariant() { return value() <= max && counter::invariant(); } private: int max; };
The class bounded, defined in slide 2-ass-2, refines the class counter by imposing an additional constraint that the value of the (bounded) counter must not exceed some user-defined maximum. This constraint is checked in the invariant function, together with the original , which was declared virtual to allow for overriding by inheritance.
In addition, the increment
From a formal perspective, the use of assertions may be regarded as a way of augmenting the type system supported by object-oriented languages. More importantly, from a software engineering perspective, the use of assertions provides a guideline for the design of classes and the use of inheritance. In the next chapter we will discuss the use of assertions and the notion of contracts in more detail.