Programming Language Concepts Using C and C++/Object Orientation and Inheritance in C++
Logic of inheritance does not change much from one programming language to another. For instance, if base and derived classes share the same public interface, the derived class is said to be a subtype of its base class and instances of it can be treated as instances of the base class. Or, thanks to dynamic dispatch, inheritance can be used to provide polymorphism.
However, newcomers to C++ from Java, or any other object-oriented programming language for that matter, are in for a few surprises. What we will do in this chapter is to take a look at inheritance in C++ and underline the differences with other programming languages.
Inheritance Peculiarities in C++Edit
Inheritance KindsEdit
First peculiarity of C++ is the assortment of inheritance kinds it offers to programmers: public, protected, and private. This meets the newcomer with the first inheritance example she tries. Take a look at the following code snippet.
class D : B { ... }
This innocent-looking code claims to derive D
from B
. However, it will not let you treat an object of D
as an object of B
. That’s right: D
is not seen as a subtype of B
. It looks like either C++ or the programmer got it wrong. Not exactly! Similar to the default section being private
, inheritance, unless otherwise stated, is taken to be of the so-called private kind. Deferring the answer of what we mean by private inheritance to another section, we modify the above code to meet our expectations as given below.
class D : public B { ... }
Done with this peculiarity, let’s move on to see some code examples. We will be using the following class definitions throughout the examples.
class B { public: B(void) { } void f1s(void) { cout << "In B::f1s(void)" << endl; } virtual void f1d(void) { cout << "In B::f1d(void)" << endl; } virtual void f2(int i) { cout << "In B::f2(int)" << endl; } ... }; // end of class B class D : public B { public: D(void) { } void f1s(void) { cout << "In D::f1s(void)" << endl; } virtual void f1d(void) { cout << "In D::f1d(void)" << endl; } virtual void f2(short s) { cout << "In D::f2(short)" << endl; } ... int _m_i; short _m_s; }; // end of class D
No Member InitializationEdit
... D* d = new D(); cout << d->_m_i << " " << d->_m_s << endl; ...
As a Java programmer you might think the above code fragment should produce two 0's in succession. However, it will output two random values. Unlike Java and C# where, unless overridden, data members are provided with default initial values, C++ compiler does not initialize the data members. If they need to be initialized to 0 or to any other value, this must be done explicitly by the programmer. It should also be noted that one cannot declare a data member with an initial value. That is, changing int _m_i;
to int _m_i = 0;
in D
will give rise to a syntactic error.
D() : _m_i(0), _m_s(0) { }
orD() { _m_i = 0; _m_s = 0; }
The C++ compiler has full confidence in the programmer. After all, a C++ programmer does not make a mistake. An error prone statement, which may be seen as the begetter of a hideous mistake in Java, is assumed to be the conscious decision of an all-knowing programmer. It’s not a bug it’s a feature!
Default Dispatch Type is Static DispatchEdit
... B* b; // the static type of b is B* if (bool_expr) b = new D(); // if this branch is taken b’s dynamic type will be D* else b = new B(); // if control falls through to this limp, dynamic type of b will be B* b->f1s(); ...
As a competent Java programmer you would expect this to produce—depending on the value bool_expr
evaluates to—"In B::f1s(void)"
or "In D::f1s(void)"
. However, in C++ it always outputs "In B::f1s(void)"
!!! Unlike Java, C++, unless otherwise told, uses static dispatch in binding function calls. This means the address of the function invoked by the call to f1s
will be resolved statically. That is, the compiler will use the static type of the identifier. In other words, functions invoked as a result of the calls made can be figured out by checking the program text.
... B* b; if (bool_expr) b = new D(); else b = new B(); b->f1d(); ...
A virtual function is dispatched dynamically. So, unlike the previous one, this example will compile and yield an output depending on the value of bool_expr
. If it evaluates to true
it will output "In D::f1d(void)", otherwise it will output "In B::f1d(void)".
Hide-by-name OverloadingEdit
... D* d = new D(); int i = 3; d->f2(i); // will be dispatched to D::f2(short) ...
With Java semantics, above code outputs "In B::f2(int)". After all, d
is also an object of type B
and can use the public interface of B
just like a genuine B
object. So, both D::f2(short)
and B::f2(int)
are exposed to clients of d
. Not in C++! Unlike Java, where base and derived class member functions make up a set of overloaded functions, C++ restricts this set to a single scope. Since derived and base classes are different scopes, any derived class function with a name coinciding with a base class function will shadow all the functions in the base class. Technically, we say C++ hides by name while Java is said to hide by signature.
But doesn’t it go against the logic of inheritance? You claim d
to be an object of B
(through the public inheritance relationship) and don’t let its clients use some function appearing in the public interface of B
? That’s right and C++ provides means to meet your expectations.
Example: Delegation |
---|
|
Regardless of whether the call is to some virtual function or not, explicit use of the class name in the function call causes it to be dispatched statically. In the following function, for instance, [although this
is of type D*
and f2d(int)
is virtual
] function call in the second statement will be dispatched to B::f2d(int)
.
Note this type of static dispatch can be used to invoke any function in the receiver object’s class or any function in any one of the ancestor classes.
void f2d(int i) { cout << "Delegating..."; B::f2d(i); } ... }; // end of class D ... D* d = new D(); int i = 3; d->f2d(i); // will be delegated to B::f2d(int) through D::f2d(int) short s = 5; d->f2d(s); // will be dispatched to D::f2d(short) ...
Example: using declaration |
---|
|
Multiple Inheritance, No Root ClassEdit
Unlike Java, where a class can derive from one and only one class, C++ supports derivation from multiple classes. Considered with the fact that interface notion is not supported, this feature is heavily used to implement interfaces.
class D : public B1, public B2 { ... }
One other point to take note of in C++ is its lack of a root class. That is, there is no class—such as the Object
class in Java—that serves as a common denominator among different classes. Consequently, one talks about a directed acyclic graph of classes instead of a tree of classes.
Test ProgramEdit
#include <iostream>
#include <string>
using namespace std;
namespace CSE224 {
namespace DS {
class B {
public:
B(void) { }
void f1s(void) { cout << "In B::f1s(void)" << endl; }
virtual void f1d(void) { cout << "In B::f1d(void)" << endl; }
virtual void f2(int i) { cout << "In B::f2(int)" << endl; }
virtual void f2d(int i) { cout << "In B::f2d(int)" << endl; }
virtual void f2u(string s) { cout << "In B::f2u(string)" << endl; }
virtual void f2u(void) { cout << "In B::f2u(void)" << endl; }
}; // end of class B
class D : public B {
public:
D(void) { }
void f1s(void) { cout << “In D::f1s(void)” << endl; }
virtual void f1d(void) { cout << “In D::f1d(void)” << endl; }
virtual void f2(short s) { cout << “In D::f2(short)” << endl; }
virtual void f2d(short s) { cout << “In D::f2d(short)” << endl; }
virtual void f2d(int i) { cout << “Delegating...”; this->B::f2d(i); }
virtual void f2u(float f) { cout << “In D::f2u(float)” << endl; }
using B::f2u;
int _m_i;
short _m_s;
}; // end of class D
} // end of namespace DS
} // end of namespace CSE224
using namespace CSE224::DS;
void default_is_static_dispatch(void) {
cout << "TESTING DEFAULT DISPATCH TYPE" << endl;
cout << "b: Static type: B*, Dynamic type: D*" << endl;
B* b = new D();
cout << "Sending (non-virtual) f1s(void) to b..."; b->f1s();
cout << "Sending (virtual) f1d(void) to b..."; b->f1d();
} // end of void default_is_static_dispatch(void)
void call_delegation(void) {
cout << "Testing delegation..." << endl;
D* d = new D();
int i = 3;
cout << "Sending (virtual) f2d(int) to d...";
d->f2d(i);
short s = 5;
cout << "Sending (virtual) f2d(short) to d...";
d->f2d(s);
} // end of void call_delegation(void)
void using_declaration(void) {
cout << "Testing the using declaration..." << endl;
D* d = new D();
float f = 3.0;
cout << "Sending (virtual) f2u(float) to d...";
d->f2u(f);
string s = string(“abc”);
cout << "Sending (virtual) f2u(string) to d...";
d->f2u(s);
cout << "Sending (virtual) f2u(void) to d...";
d->f2u();
} // end of void using_declaration(void)
void CPP_hides_by_name(void) {
cout << "TESTING HIDE-BY NAME" << endl;
D* d = new D();
int i = 3;
cout << "Sending (virtual) f2(int) to d...";
d->f2(i);
call_delegation();
using_declaration();
} // end of void CPP_hides_by_name(void)
void no_member_initialization(void) {
cout << "TESTING MEMBER INITIALIZATION" << endl;
D* d = new D();
cout << "_m_i: " << d->_m_i << " _m_s: " << d->_m_s << endl;
} // end of void no_member_initialization(void)
int main(void) {
no_member_initialization();
cout << endl;
default_is_static_dispatch();
cout << endl;
CPP_hides_by_name();
return 0;
} // end of int main(void)
gxx –o Test.exe Peculiarities.cxx↵ Test↵ TESTING MEMBER INITIALIZATION _m_i: -1 _m_s: 9544 TESTING DEFAULT DISPATCH TYPE b: Static type: B*, Dynamic type: D* Sending (non-virtual) f1s(void) to b...In B::f1s(void) Sending (virtual) f1d(void) to b...In D::f1d(void) TESTING HIDE-BY NAME Sending (virtual) f2(int) to d...In D::f2(short) Testing delegation... Sending (virtual) f2d(int) to d...Delegating...In B::f2d(int) Sending (virtual) f2d(short) to d...In D::f2d(short) Testing the using declaration... Sending (virtual) f2u(float) to d...In D::f2u(float) Sending (virtual) f2u(string) to d...In B::f2u(string) Sending (virtual) f2u(void) to d...In B::f2u(void)
Inheritance a la JavaEdit
In this part of the handout, we provide an insight into how C++ and Java can be related as far as inheritance is concerned. This is accomplished by simulating the concepts found in Java using those found in C++. Such an approach should not be taken as an advertisement campaign of Java; needless to say Java is not without competition. It should rather be taken as an incomplete attempt at providing clues to the inner workings of the mentioned concepts.
Root Class and the Interface ConceptEdit
In Java, expressing common attributes among unrelated objects is made possible by means of the root class and the interface concept. The former defines a common denominator among all classes, while the latter is used to classify a group of classes.[1] For instance, because it is listed in Object
all objects can be tested for equality with an object of a compatible type; or objects of classes claiming to be Comparable
can be compared with a compatible object.
These two notions are not supported in C++ as a linguistic abstraction. Instead, programmers are expected to resort to using conventions or simulate it through other constructs. For instance, testing for equality is accomplished by overriding the default implementation of the ==
operator; interface concept, which is not directly supported, can be simulated by means of abstract classes with pure virtual functions.
#ifndef OBJECT_HXX
#define OBJECT_HXX
namespace System {
class Object {
Our intention in having the header file Object
is to define a root class that can be used as a polymorphic type in generic functions, such as compareTo
defined in IComparable
; we do not mean to provide any shared functionality as is done in Object
class of Java. This, however, cannot be accomplished simply by defining an empty class. In order for a type to be polymorphic in C++ it must have at least one virtual function. We therefore include a dummy virtual function in our class definition.
But then, why did we make its access modifier protected
? First of all, it cannot be public
because we don’t want any functionality to be exposed through this class. What about declaring no_op to be private
? After all, declaring it as protected
means deriving classes can now send the no_op
message. Answer lies in the nature of polymorphism: In order for polymorphism to be possible, one should be able to override the definition of a dynamically-dispatched function found in the base class. This implies such functions should be open at least to the derived classes. As a matter of fact C++ compilers will not even let you declare virtual
functions in a <syntaxhighlightlang="cpp" enclose="none">private</syntaxhighlight> section.
protected:
virtual void no_op(void) { return; }
}; // end of class Object
} // end of namespace System
#endif
Definition: A pure virtual function is a virtual function that is not given a function body in the declaring class. A derived class claiming to be concrete must therefore provide an implementation for such a function.
In terms of C++-supported concepts, an interface is a "fieldless" abstract class whose all functions are pure virtual.
#ifndef ICOMPARABLE_HXX
#define ICOMPARABLE_HXX
#include "Object"
namespace System {
class IComparable {
public:
virtual int compareTo(const Object&) const = 0;
}; // end of class IComparable
} // end of namespace System
#endif
ModuleEdit
Interface (Rational)Edit
#ifndef RATIONAL_HXX
#define RATIONAL_HXX
#include <iostream>
using namespace std;
#include "IComparable"
#include "Object"
using namespace System;
#include "math/exceptions/NoInverse"
#include "math/exceptions/ZeroDenominator"
#include "math/exceptions/ZeroDivisor"
using namespace CSE224::Math::Exceptions;
namespace CSE224 {
namespace Math {
Having defined the interface concept as a variation on the class concept, we naturally should be cautious about speaking of the implementation relation. This is indeed the case in C++: one can speak of the extension relation only. As a consequence support for multiple inheritance is a must.
class Rational : public Object, public IComparable {
public:
Rational(long num = 0, long den = 1) throw(ZeroDenominator) {
if (den == 0) {
cerr << "Error: ";
cerr << "About to throw ZeroDenominator exception" << endl;
throw ZeroDenominator();
} // end of if (den == 0)
_n = num;
_d = den;
this->simplify();
} // end of constructor(long=, long=)
Rational(Rational& existingRat) {
_n = existingRat._n;
_d = existingRat._d;
} // end of copy constructor
Notice the following functions, unlike the rest of the member functions, will be dispatched statically. In Java, such an effect can be achieved by declaring methods to be final
.
long getNumerator(void) const { return _n; }
long getDenominator(void) const { return _d; }
In addition to marking functions virtual
, we also declare them to return a reference. This is because a reference is the best candidate for serving the purpose of handles in Java: it is an inheritance-aware, compiler-managed pointer.[2] That is, we can pass as argument to the next function [or any function expecting a reference to Rational
, for that matter] an object belonging in the class hierarchy rooted by the Rational
class; dereferencing of a reference is automatically done by the compiler-synthesized code.
As an alternative—although the resulting code would be less writable and readable—we could have used a plain pointer. However, using a plain object type is out of question. This is due to the fact that polymorphism together with inheritance requires sending the same message—that is what polymorphism is all about—to objects of probably varying sizes—enter inheritance—which in turn implies passing and returning variable-sized objects. This is something compilers cannot deal with! We should provide some assistance, which we do by injecting a fixed-size programming entity in between: pointer or reference.
virtual Rational& add(const Rational&) const;
virtual Rational& divide(const Rational&) const throw(ZeroDivisor)
virtual Rational& inverse(void) const throw(NoInverse);
virtual Rational& multiply(const Rational&) const;
virtual Rational& subtract(const Rational&) const;
virtual int compareTo(const Object&) const;
Note the following function serves a purpose akin to that of toString
in Java. Replacing sstream instead of ostream
and changing the implementation accordingly would make the analogy a more perfect one.
friend ostream& operator<<(ostream&, const Rational&);
private:
long _n, _d;
long min(long n1, long n2);
Rational& simplify(void);
}; // end of class Rational
} // end of namespace Math
} // end of namespace CSE224
#endif
Implementation (Rational)Edit
#include <iostream>
#include <memory>
using namespace std;
#include "Object"
using namespace System;
#include "math/exceptions/NoInverse"
#include "math/exceptions/ZeroDenominator"
#include "math/exceptions/ZeroDivisor"
using namespace CSE224::Math::Exceptions;
#include "math/Rational"
namespace CSE224 {
namespace Math {
Rational& Rational::
add(const Rational& rhs) const {
Note the missing try-catch
block! Unlike Java, C++ does not mandate the programmer to put all potentially problematic code in a guarded region. One can say all C++ exceptions are treated like the Java exceptions deriving from the RuntimeException
class. This gives the programmer a degree of freedom that enables her to come up with cleaner code. For instance, reaching the next line means we are adding two well-formed Rational
objects. Result of such an action can never create a problem!
Rational* sum = new Rational(_n * rhs._d + _d * rhs._n, _d * rhs._d);
return sum->simplify();
} // end of Rational& Rational::add(const Rational&) const
Rational& Rational::
divide(const Rational& rhs) const throw(ZeroDivisor) {
try {
Rational& tmp_inv = rhs.inverse();
Rational& ret_rat = this->multiply(tmp_inv);
Now that we are done with the temporary object that holds the inverse of rhs
, we must return it to the memory allocator or put up with the consequences of creating garbage at each use of this function. That’s pretty annoying! But then again, a C/C++ programmer does not make such easy mistakes.
Notice the address-of operator before tmp_inv
. Application of this operator to a reference returns the starting address of the region aliased by the reference. [Remember references are silently dereferenced at their point of use] In our case, this will be the address of the object created as a result of sending the message inverse
to rhs
.
delete &tmp_inv;
return ret_rat;
} catch (NoInverse e) {
cerr << "Error: About to throw ZeroDivisor exception" << endl;
throw ZeroDivisor();
}
} // end of Rational& Rational::divide(const Rational&) const
Rational& Rational::
inverse(void) const throw(NoInverse) {
try {
Rational *res = new Rational(_d, _n);
return *res;
} catch(ZeroDenominator e) {
cerr << "Error: About to throw NoInverse exception" << endl;
throw NoInverse(_n, _d);
}
} // end of Rational& Rational::inverse(void) const
Rational& Rational::
multiply(const Rational& rhs) const {
Rational *res = new Rational(_n * rhs._n, _d * rhs._d);
return res->simplify();
} // end of Rational& Rational::multiply(const Rational&) const
Rational& Rational::
subtract(const Rational& rhs) const {
We formulate subtraction in terms of other operations: instead of subtracting a value, we add the negated value. For doing this we create two temporary objects meaningful only throughout the current call. Before returning to the caller we should return them to the memory allocator.
A so-called smart pointer object is exactly what we want. Such an object is initialized to point to a dynamically allocated object created by a new expression and frees it—the dynamically allocated object—at the end of its (smart pointer’s) lifetime. The following figure showing the memory layout after execution of line 47 should make this clear.
Heap object local to the function is created together with the smart pointer object, which is itself a local object created on the run-time stack.9 This means allocation-constructor call and destructor call-deallocation of this smart pointer object will be handled by the compiler-synthesized code. In other words, programmer need not worry about the life-cycle management of the smart pointer object. So, if we can guarantee the heap object is destroyed-deallocated together with this smart pointer, its life-cycle management will not be a problem anymore. This is accomplished by delet(e)ing the heap object within the destructor of the related smart pointer object, which means the heap object will have been destroyed-deallocated by the time the destruction of the smart pointer object is over. The following then describes the life-cycle of the smart pointer and the related heap object.
- Create the smart pointer in the run-time stack.
- Pass the related heap object to the constructor of the smart pointer.
- Use the heap object.
- Call the destructor of the smart pointer by means of the compiler-synthesized code.
- Delete the heap object from within the destructor of the smart pointer.
According to this, anonymous Rational
objects—new Rational(-1)
and &(rhs.multiply(*neg_1))
—created in the following definitions will have been automatically—that is, without the intervention of the programmer—returned before leaving the function.
auto_ptr< Rational > neg_1(new Rational(-1));
auto_ptr< Rational > tmp_mul(&(rhs.multiply(*neg_1)));
Observe the following application of the dereferencing operator receives as its operand a non-pointer variable, which may at first seem as an error. After all, *
works by returning the contents of memory indicated by its sole operand. However, this rather limited description ignores the possibility of overloading the dereferencing operator. It is indeed the overloaded version of this operator that enables the use of a non-pointer type. The following application of *
makes use of the overloaded version defined within the auto_ptr
class, which returns the contents of the heap object managed by the smart pointer.
To make things clearer, we can suggest the following implementation for the auto_ptr
class.
template <class ManagedObjectType> class auto_ptr { public: auto_ptr(ManagedObjectType* managedObj) { _managed_heap_object = managedObj; ... } // end of constructor(ManagedObjectType*) ... ManagedObjectType operator*(void) { ... return *_managed_heap_object; } // end of ManagedObjectType operator*(void) ... private: ManagedObjectType* _managed_heap_object; } // end of class auto_ptr<ManagedObjectType>
Rational &ret_rat = add(*tmp_mul);
return(ret_rat);
} // end of Rational& Rational::subtract(const Rational&) const
int Rational::
compareTo(const Object& rhs) const {
double this_equi = ((double) _n) / _d;
In addition to the traditional C-style casting C++ offers a variety of cast operators: const_cast
, dynamic_cast
, static_cast
, and reinterpret_cast
. Each of these performs a subset of the functionality offered by the traditional cast operator and therefore one can say the new operators do not add any new functionality. Nevertheless, thanks to the extra support from the compiler, they enable writing more type-safe programs. Using the new operators we state our intentions explicitly and therefore get more maintainable code.
Example: Removing const -ness property of an object.
|
---|
|
It should be noted that const_cast can also be used to change volatile -ness property of an object.
|
Since our intention of removing the const
-ness has been made explicit by the relevant operator, maintainer of the code will more quickly spot the occurrence of cast and realize what is being done. Alternative scheme of using the C-style cast lacks these qualities: it is both difficult to find the location of the cast and figure out that const
-ness is being removed.
Using dynamic_cast
will also provide us with the benefit of safer code. This particular operator is used to bi-directionally cast between polymorphic classes—that is; classes that has at least one virtual function—that are related with each other by a public derivation.
Question | ||
---|---|---|
dynamic_cast can be used to convert only between pointer/reference types. Why?
| ||
|
Definition: Converting from one class to another in the same class hierarchy is referred to as downcasting if the target type is more specialized. In case the target type is less specialized the act of casting is called upcasting.
Upcasting to a public base class is always successful since the messages listed in the interface of the target type is a subset of the source type interface. On the other hand, casting from a derived class to one of its non-public base classes leads to a compile-time error. Similarly, downcasting may give rise to run-time errors since we can potentially send messages that are not found in the interface of the source type.
Example: Safer code with dynamic_cast .
|
---|
|
The above code downcasts a PB*
variable to PD*
, through which one can send the extra message named g
. For this example, this doesn't seem to be a problem. But what if pb
was used to point to an object of PB
instead of PD
? What if it is used to point to objects of different types as is shown in the following fragment?
if(some_condition) { ... ; pb = new D; ... } else { ...; pb = new B; ... } ... PD* pd = dynamic_cast<PD*>(pb);
There is no guarantee that we can send g
to the underlying object, which can be of type PB
or PD
. This guarantee we are seeking can be provided only if we can check the object type at run-time. And this is exactly what dynamic_cast
does: by checking compatibility of the pointer/reference—static type—with the object—dynamic type—dynamic_cast
decides whether the cast taking place is valid or not. If so a legitimate value is returned. Otherwise, in the case of casting a pointer, a NULL
value is returned, which basically removes any possibility of sending an illegal message; in the case of failing to cast a reference std::bad_cast
exception is thrown. Same actions are taken also when source and target types are not related with inheritance.
Note this cost due to the run-time check performed by the compiler-synthesized code is never experienced as a result of using the traditional cast operator. This is because the C-style cast operator makes no use of any run-time information.
Observe casting up the class hierarchy—since messages received by an object of the derived class is a subset of that of its base classes—does not need any run-time checks. This means cost due to dynamic_cast
is not rationalized: why should we pay for making a control, whose result is known to us? Solution offered by C++ is another cast operator that does its job using compile-time information: static_cast
. This operator can be used for performing conversions that are implicitly carried out by the compiler, performing these implicit conversions in the reverse direction. It can also be used in place of dynamic_cast
if skipping the run-time checks is guaranteed to be safe.
Example: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
This doesn't fully cover the functionality offered by the traditional cast operator. Conversions between unrelated/incompatible pointer types are missing, for instance. This missing functionality is covered by the
It should be kept in mind that this operator makes no checks on the source and target types; it simply bitwise-copies the contents of the target into the source. double rhs_equi = ((double) dynamic_cast<const Rational&>(rhs)._n) / dynamic_cast<const Rational&>(rhs)._d;
if (this_equi > rhs_equi) return 1;
else if (this_equi == rhs_equi) return 0;
else return -1;
} // end of int Rational::compareTo(const Object&) const
long Rational::
min(long n1, long n2) { return (n1 > n2 ? n1 : n2); }
Rational& Rational::
simplify(void) {
long upperLimit = min(_n, _d);
for (long i = 2; i <= upperLimit;)
if ((_n % i == 0) && (_d % i == 0)) { _n /= i; _d /= i; }
else i++;
if (_d < 0) { _n *= -1; _d *= -1; }
return(*this);
} // end of Rational& Rational::simplify(void)
ostream& operator<<(ostream& os, const Rational& rat) {
os << rat._n << " ";
if (rat._d != 1) os << "/ " << rat._d;
return os;
} // end of ostream& operator<<(ostream&, const Rational&)
} // end of namespace Math
} // end of CSE224
Interface (Whole)Edit#ifndef WHOLE_HXX
#define WHOLE_HXX
#include <iostream>
using namespace std;
#include "math/Rational"
namespace CSE224 {
namespace Math {
class Whole : public Rational {
public:
Remember Whole(long num) : Rational(num) { }
Whole(void) : Rational(0) { }
Whole(Whole& existingWhole) :
Rational(existingWhole.getNumerator()) { }
Whole& add(const Whole& rhs) const;
virtual Rational& add(const Rational&) const;
}; // end of class Whole
} // end of namespace Math
} // end of namespace CSE224
#endif
Implementation (Whole)Edit#include <iostream>
using namespace std;
#include "math/Rational"
#include "math/Whole"
namespace CSE224 {
namespace Math {
Rational& Whole::
add(const Rational& rhs) const {
cout << "[In Whole::add(Rational)] ";
return (Rational::add(rhs));
} // end of Rational& Whole::add(const Rational&) const
Whole& Whole::
add(const Whole& rhs) const {
cout << "[In Whole::add(Whole)] ";
Whole *res = new Whole(getNumerator() + rhs.getNumerator());
return *res;
} // end of Whole& Whole::add(const Whole&) const
} // end of namespace Math
} // end of namespace CSE224
Exception ClassesEdit#ifndef NOINVERSE_HXX
#define NOINVERSE_HXX
#include <iostream>
using namespace std;
namespace CSE224 {
namespace Math{
namespace Exceptions {
class NoInverse {
public:
NoInverse(long num, long den) {
cerr << "Error: Throwing a NoInverse exception" << endl;
_n = num; _d = den;
} // end of constructor(long, long)
void writeNumber(void) {
cerr << "The problematic number is " << _n << "/" << _d << endl;
} // end of void writeNumber()
private:
long _n, _d;
}; // end of class NoInverse
} // end of namespace Exceptions
} // end of namespace Math
} // end of namespace CSE224
#endif
#ifndef ZERODENOMINATOR_HXX
#define ZERODENOMINATOR_HXX
namespace CSE224 {
namespace Math {
namespace Exceptions {
class ZeroDenominator {
public:
ZeroDenominator(void) {
cerr << "Error: Throwing a ZeroDenominator exception" << endl;
} // end of default constructor
}; // end of class ZeroDenominator
} // end of namespace Exceptions
} // end of namespace Math
} // end of namespace CSE224
#endif
#ifndef ZERODIVISOR_HXX
#define ZERODIVISOR_HXX
#include <iostream>
using namespace std;
namespace CSE224 {
namespace Math {
namespace Exceptions {
class ZeroDivisor {
public:
ZeroDivisor(void) {
cerr << "Error: Throwing a ZeroDivisor exception" << endl;
} // end of default constructor
}; // end of class ZeroDivisor
} // end of namespace Exceptions
} // end of namespace Math
} // end of namespace CSE224
#endif
Test ProgramEdit#include <iostream>
#include <memory>
using namespace std;
#include "math/Whole"
using namespace CSE224::Math;
#include "math/exceptions/ZeroDenominator"
using namespace CSE224::Math::Exceptions;
int main(void) {
cout << "Creating a Whole object(five) and initializing it with 5..." << endl;
auto_ptr < Whole > fivep(new Whole(5));
Whole& five = *fivep;
cout << "Creating a Rational object(three) and initializing it with 3..." << endl;
auto_ptr < Rational > threep(new Rational(3));
Rational& three = *threep;
cout << "Result of five.multiply(three) = ";
cout << five.multiply(three) << endl;
cout << "***************************************************" << endl;
cout << "Result of three.add(three) = ";
cout << three.add(three) << endl;
cout << "Result of three.add(five) = ";
cout << three.add(five) << endl;
cout << "Result of five.add(three) = ";
cout << five.add(three) << endl;
cout << "Result of five.add(five) = ";
cout << five.add(five) << endl;
cout << "***************************************************" << endl;
cout << "Setting a Rational object(ratFive) as an alias for a Whole object(five)..." << endl;
Rational& ratFive = five;
cout << "Result of ratFive.add(three) = ";
cout << ratFive.add(three) << endl;
cout << "Result of ratFive.add(five) = ";
cout << ratFive.add(five) << endl;
cout << "Result of ratFive.add(ratFive) = ";
cout << ratFive.add(ratFive) << endl;
cout << "Result of five.add(ratFive) = ";
cout << five.add(ratFive) << endl;
cout << "Result of three.add(ratFive) = ";
cout << three.add(ratFive) << endl;
cout << "***************************************************" << endl;
cout << "Creating a Rational object(r1) and initializing it with 3/2..." << endl;
auto_ptr < Rational > r1p(new Rational(3, 2));
Rational& r1 = *r1p;
cout << "Result of five.multiply(r1) = ";
cout << five.multiply(r1) << endl;
cout << "Result of five.divide(r1) = ";
cout << five.divide(r1) << endl;
return 0;
} // end of int main(void)
Private InheritanceEditProgramming being a pragmatic endeavor, one must strive to do it as efficiently as possible. Arguably the most effective way to achieve this is to re-use artifacts that have already been used in different stages of previous projects.[3] By doing so we save on development and test time, which means our next product makes it to the market in a shorter time. One way to achieve reuse is inheritance. And for many it seems to be the only affordable one. Nevertheless, there is a contender: composition. This technique is realized by making an object a member of another.
For the above example we say an object of In C++, similar effect can be achieved through the so-called private inheritance.
Interface (List)Edit#ifndef LIST_HXX
#define LIST_HXX
#include "ds/exceptions/List_Exceptions"
using namespace CSE224::DS::Exceptions;
namespace CSE224 {
namespace DS {
class List {
friend ostream& operator<<(ostream&, const List&);
public:
List(void) : _size(0), _head(NULL), _tail(NULL) { }
List(const List&);
~List(void);
List& operator=(const List&);
bool operator==(const List&);
double get_first(void) throw(List_Empty);
double get_last(void) throw(List_Empty);
void insert_at_end(double new_item);
void insert_in_front(double new_item);
double remove_from_end(void) throw(List_Empty);
double remove_from_front(void) throw(List_Empty);
bool is_empty(void);
unsigned int size(void);
private:
What follows is the definition of a nested class, a class defined inside another. Such a class, when defined in a Although it might be tempting to draw a parallel between nested classes and inner classes of Java, that would be a mistake. As opposed to the special relation between the inner class and its enclosing class, in C++ the enclosing class has no special access privileges with regard to the classes nested within it. For this reason, changing Another remark to be made is that nested classes of C++ do not keep any record of the enclosing object in the objects of the inner class, which makes them like more the static inner classes of Java. class List_Node {
public:
List_Node(double val) : _info(val), _next(NULL), _prev(NULL) { }
double _info;
List_Node *_next, *_prev;
}; // end of class List_Node
private:
List_Node *_head, *_tail;
unsigned int _size;
A function declared in the void insert_first_node(double);
}; // end of List class
} // end of namespace DS
} // end of namespace CSE224
#endif
Implementation (List)Edit#include <iostream>
using namespace std;
#include "ds/List"
#include "ds/exceptions/List_Exceptions"
using namespace CSE224::DS::Exceptions;
namespace CSE224 {
namespace DS {
List::
List(const List& rhs) : _head(NULL), _tail(NULL), _size(0) {
List_Node *ptr = rhs._head;
for(unsigned int i = 0; i < rhs._size; i++) {
this->insert_at_end(ptr->_info);
ptr = ptr->_next;
} // end of for(unsigned int i = 0; i < rhs._size; i++)
} // end of copy constructor
Now that nodes of the Notice our decision is not affected by whether the List::
~List(void) {
for (int i = 0; i < _size; i++)
remove_from_front();
} // end of destructor
List& List::
operator=(const List& rhs) {
if (this == &rhs) return (*this);
for(unsigned int i = _size; i > 0; i--)
this->remove_from_front();
List_Node *ptr = rhs._head;
for(unsigned int i = 0; i < rhs._size; i++) {
this->insert_at_end(ptr->_info);
ptr = ptr->_next;
} // end of for(unsigned int i = 0; i < rhs._size; i++)
if (rhs._size == 0) {
_head = _tail = NULL;
_size = 0;
} // end of if(rhs._size == 0)
return (*this);
} // end of assignment operator
bool List::
operator==(const List& rhs) {
if (_size != rhs._size) return false;
if (_size == 0 || this == &rhs) return true;
List_Node *ptr = _head;
List_Node *ptr_rhs = rhs._head;
for (unsigned int i = 0; i < _size; i++) {
if (!(ptr->_info == ptr_rhs->_info))
return false;
ptr = ptr->_next;
ptr_rhs = ptr_rhs->_next;
} // end of for(unsigned int i = 0; i < _size; i++)
return true;
} // end of equality-test operator
double List::
get_first(void) throw(List_Empty) {
if (is_empty()) throw List_Empty();
return (_head->_info);
} // end of double List::get_first(void)
double List::
get_last(void) throw(List_Empty) {
if (is_empty()) throw List_Empty();
return (_tail->_info);
} // end of double List::get_last(void)
void List::
insert_at_end(double new_item) {
if (is_empty()) insert_first_node(new_item);
else {
List_Node *new_node = new List_Node(new_item);
_tail->_next = new_node;
new_node->_prev = _tail;
_tail = new_node;
}
_size++;
} // end of void List::insert_at_end(double)
void List::
insert_in_front(double new_item) {
if (is_empty()) insert_first_node(new_item);
else {
List_Node *new_node = new List_Node(new_item);
new_node->_next = _head;
_head->_prev = new_node;
_head = new_node;
}
_size++;
} // end of void List::insert_in_front(double)
double List::
remove_from_end(void) throw(List_Empty) {
if (is_empty()) throw List_Empty();
double ret_val = _tail->_info;
List_Node *temp_node = _tail;
if (_size == 1) { head = _tail = NULL; }
else {
_tail = _tail->_prev;
_tail->_next = NULL;
}
delete temp_node;
_size--;
return ret_val;
} // end of double List::remove_from_front(void)
double List::
remove_from_front(void) throw(List_Empty) {
if (is_empty()) throw List_Empty();
double ret_val = _head->_info;
List_Node *temp_node = _head;
if (_size == 1) { _head = _tail = NULL; }
else {
_head = _head->_next;
_head->_prev = NULL;
}
delete temp_node;
_size--;
return ret_val;
} // end of double List::remove_from_front(void)
bool List::
is_empty(void) { return(_size == 0); }
unsigned int List::
size(void) { return _size; }
void List::
insert_first_node(double new_item) {
List_Node *new_node = new List_Node(new_item);
_head = _tail = new_node;
} // end of void List::insert_first_node(double)
ostream& operator<<(ostream& os, const List& rhs) {
List tmp_list = rhs;
os << "<" << rhs._size << "> <head: ";
for (int i = 0; i < rhs._size - 1; i++) {
os << tmp_list._head->_info << ", ";
tmp_list._head = tmp_list._head->_next;
} // end of for(int i = 0; i < rhs._size; i++)
if (rhs._size > 0) os << tmp_list._head->_info;
os << ": tail>";
return(os);
} // end of ostream& operator<<(ostream&, const List&)
} // end of namespace DS
} // end of namespace CSE224
Interface (Stack)Edit#ifndef STACK_HXX
#define STACK_HXX
#include <iostream>
using namespace std;
#include "ds/exceptions/Stack_Exceptions"
using namespace CSE224::DS::Exceptions;
#include "ds/List"
namespace CSE224 {
namespace DS {
The C++ offers an alternative: private inheritance. Using private inheritance, the derived class can still make use of the functionality offered by the base class but the base class interface is not exposed through the derived class. For this reason, class Stack : private List {
public:
Now that the derived class can reuse the base class functionality but do not expose it to its users, this type of inheritance is also called implementation inheritance. For a similar reason, public inheritance is also called interface inheritance. We do not need to write the functions of the orthodox canonical form because compiler-synthesized versions provide the equivalent of what we are required to do. This is basically because the only data field of // Stack(void);
// Stack(const Stack&);
// ~Stack(void);
// Stack& operator=(const Stack&);
// bool operator==(const Stack&);
double peek(void) throw(Stack_Empty);
double pop(void) throw(Stack_Empty);
void push(double new_item);
Thanks to the following statement, we selectively expose a function from the privately inherited base class. It’s as if the using List::is_empty;
}; // end of Stack class
} // end of namespace DS
} // end of namespace CSE224
#endif
Implementation (Stack)Edit#include <iostream>
using namespace std;
#include "ds/Stack"
#include "ds/exceptions/Stack_Exceptions"
using namespace CSE224::DS::Exceptions;
namespace CSE224 {
namespace DS {
double Stack::
peek(void) throw(Stack_Empty) {
double ret_val;
Users of our class should not be aware of how we implement the try { ret_val = get_first();}
catch(List_Empty) { throw Stack_Empty(); }
return ret_val;
} // end of double Stack::peek(void)
void Stack::
push(double new_item) { List::insert_in_front(new_item); }
double Stack::
pop(void) throw(Stack_Empty) {
double ret_val;
try { ret_val = remove_from_front(); }
catch(List_Empty) { throw Stack_Empty(); }
return ret_val;
} // end of double Stack::pop(void)
} // end of namespace DS
} // end of namespace CSE224
Virtual InheritanceEditWith the possibility of multiple inheritance rises the issue of the so-called virtual inheritance. Consider the class hierarchy shown in Figure 2. Question that awaits your answer is: How many Whichever one is the right answer, there might be cases where either one turns out to be a better choice. We must find a way to tell the difference between the options. This is where the notion of virtual inheritance comes into the picture. We define the
Thanks to these definitions, there is now only one Note use of virtual inheritance causes the order of constructor call to be changed: Virtual base classes are always constructed prior to non-virtual base classes regardless of where they appear in the inheritance hierarchy. A typical use of virtual inheritance involves implementation of mix-in classes. Mix-in classes are used for tuning the behavior of a base class and can be combined to obtain even more specialized classes. For example, using the following code one can create windows with different styles: plain window, window with menu, window with border, and window with menu and border. As a matter of fact, we can come up with our own mix-in, say scroll-bar mix-in, and get scroll-bar-supporting versions of these window styles.
|