March 08 (Monday) March 12 (Friday)
Previously, our student grading program was just concerned with a single type of student. Now, we introduce a new type of student, namely, a graduate student. Graduate students are identical to regular students except that graduate students must complete a thesis and their final grade is determined by their course work grade and their thesis grade (their final mark is the lesser of the two).
To accommodate for this difference we extract all the details that
are common between undergraduate and graduate students and put them
in a class named Core
. We then use this class as a
base class upon which a graduate student can be derived.
(i.e. a graduate student class can be considered as a
subclass of a core (undergraduate) student class).
We also introduce another accessibility keyword in the context of a class,
namely protected
. Protected members can be accessed by the
class in which they are defined and by its derived classes. It cannot
be accessed outside the context of any of these classes. In the
example below, we want the Grad
class to have access
to the midterm
and final
exams marks as
well as to the homework
vector. So we put these
data members in the protected
section of the base class.
We can define the header file for the two classes as follows:
#ifndef GUARD_Core_h
#define GUARD_Core_h
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
class Core {
public:
Core(): midterm(0), final(0) { }
Core(std::istream& is) { read(is); }
std::string name() const;
// as defined in 13.1.2/230
virtual std::istream& read(std::istream&);
virtual double grade() const;
virtual ~Core() { }
protected:
// accessible to derived classes
std::istream& read_common(std::istream&);
double midterm, final;
std::vector<double> homework;
virtual Core* clone() const { return new Core(*this); }
private:
// accessible only to `Core'
std::string n;
friend class Student_info;
};
class Grad: public Core {
public:
Grad(): thesis(0) { }
Grad(std::istream& is) { read(is); }
// as defined in 13.1.2/230; Note: `grade' and `read'
// are `virtual' by inheritance
double grade() const;
std::istream& read(std::istream&);
private:
double thesis;
Grad* clone() const { return new Grad(*this); }
};
bool compare(const Core&, const Core&);
bool compare_Core_ptrs(const Core* cp1, const Core* cp2);
#endif
Core.h
Note that a Grad
student is exactly the same as a
Core
student, except that the class defines an extra data member
to store the thesis
grade. Because the method used to read
in each type of student is different (since we must read in the thesis
mark for graduate students) and because the calculating of the final
grade is also different for undergraduate and graduate students, we make
the read()
and grade()
function virtual
in the base class, Core
. By making these functions virtual,
we are telling the compiler that when we invoke either of these functions
via a pointer or a reference to a Core
object, the actual
function called will depend upon the actual type of the pointer or
reference. This is known as dynamic binding or dynamic
function invocation. For example, consider the following code fragment:
Core *p; p = new Core; p->read(cin); // Invoke Core::read() p = new Grad; p->read(cin); // Invoke Grad::read()
Note that the actual read()
function invoked depends upon
the type of pointer assigned to p
. One other important
point to note here is that given a type which is a pointer to an object
of a base class (p
in the above fragment, for example), we
can assign to it a pointer to an object of that base class (obviously)
or any object of any class derived from this base class. Hence,
in the above code fragment, p = new Grad;
is a perfectly
legal statement since new Grad
will return a pointer to an
object of type Grad
and Grad
is derived from
Core
. The same idea applies in the context of references.
If r
is a reference to a Core
object, the
we can assign to r
either a Core
object or
a Grad
object. When we invoke the read()
or
grade()
method via r
, the appropriate method will
be called depending upon the type that r
actually references.
If we just used a Core
variable (i.e. not
a Core
reference or Core
pointer), then
static invocation will result. For example, consider the
following code fragment:
Core c; Core c1; Grad g1; c = c1; c.read(cin); // Invoke Core::read() c = g1; c.read(cin); // Invoke Core::read()
Even though the assignment c = g1
took place before the
second invocation of the read()
, the read()
method that is defined by Core
is invoked and not
the read()
method as defined by Grad
.
During the c = g1
assignment, none of the extra details
that makes g1
a Grad
object (e.g.
the thesis
data member) are preserved when the assignment
to c
is made. Therefore, c
is very much a
Core
object and the invocation of the read()
method will be statically determined during compile time (and not
dynamically during run-time).
The Grad
class essentially defines its own version of
the virtual read()
and grade()
functions.
This is known as a derived class overriding its base class's
virtual functions. If the Grad
class did not define its
own read()
method, for example, then the base class's
read()
method will be called.
Overriding is different from overloading in that the number and types of the arguments must be identical for overriding (if not, then the method in the derived class essentially shadows the method in the base class). In overloading, the number and/or types of the arguments must be different in order to ensure unambiguous compile-time resolution of calls to the overloaded functions.
Core.cpp
source module
The following source module defines all the methods of the
Core
and Grad
classes that were not
defined in the header file. Note the differences in the implementation
of the read()
and grade()
methods between
the two classes.
#include <algorithm>
using std::min;
#include "Core.h"
#include "grade.h"
using std::istream;
using std::string;
using std::vector;
std::istream& read_hw(std::istream& in, std::vector<double>& hw);
string Core::name() const { return n; }
double Core::grade() const
{
return ::grade(midterm, final, homework);
}
istream& Core::read_common(istream& in)
{
// read and store the student's name and exam grades
in >> n >> midterm >> final;
return in;
}
istream& Core::read(istream& in)
{
read_common(in);
read_hw(in, homework);
return in;
}
istream& Grad::read(istream& in)
{
read_common(in);
in >> thesis;
read_hw(in, homework);
return in;
}
double Grad::grade() const
{
return min(Core::grade(), thesis);
}
bool compare(const Core& c1, const Core& c2)
{
return c1.name() < c2.name();
}
bool compare_Core_ptrs(const Core* cp1, const Core* cp2)
{
return compare(*cp1, *cp2);
}
Core.cpp
Finally, we can define a main()
function that employs the
above two classes. Note the differences between this main()
function and its earlier implementations in the student grading problem:
vector
of Core
pointers
instead of Student_info
objects. The temporary which
we use to read each of the students is also a pointer and not an object.
This means that whenever we access the temporary or the elements
in the vector, we must remember to dereference. The reason for
storing pointers is so that the read()
and grade()
methods are to be determined dynamically.
Core
object or a
Grad
object. Once the appropriate object is created,
we invoke the virtual read()
method on it to read all the
information relevant for that type of student.
new
, we must
remember to destroy them using delete
. Note that because
the pointer returned by students[i]
could point to either a
Grad
or a Core
object, we must define
Core
's destructor as virtual. We can leave its body empty.
Therefore in the class definition of Core
above, we have:
virtual ~Core() { }
#include <vector>
#include <string>
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include "Core.h"
using std::cout; using std::cin;
using std::domain_error; using std::endl;
using std::setprecision; using std::setw;
using std::streamsize; using std::sort;
using std::string; using std::vector;
using std::max;
int main()
{
vector<Core*> students; // store pointers, not objects
Core* record; // temporary must be a pointer as well
char ch;
string::size_type maxlen = 0;
// read and store the data
while (cin >> ch) {
if (ch == 'U')
record = new Core; // allocate a `Core' object
else
record = new Grad; // allocate a `Grad' object
record->read(cin); // `virtual' call
maxlen = max(maxlen, record->name().size());// dereference
students.push_back(record);
}
// pass the version of `compare' that works on pointers
sort(students.begin(), students.end(), compare_Core_ptrs);
// write the names and grades
for (vector<Core*>::size_type i = 0;
i != students.size(); ++i) {
// `students[i]' is a pointer that we dereference
// to call the functions
cout << students[i]->name()
<< string(maxlen + 1 - students[i]->name().size(), ' ');
try {
double final_grade = students[i]->grade();
streamsize prec = cout.precision();
cout << setprecision(3) << final_grade
<< setprecision(prec) << endl;
} catch (domain_error e) {
cout << e.what() << endl;
}
// free the object allocated when reading
delete students[i];
}
return 0;
}
main_core_ptrs.cpp
We can also use a common C++ idiom called a handle to solve
the above problem. In this example, the main()
function
becomes a bit cleaner as complexity is pushed onto a handle class which
maintains a pointer to a Core
object (this pointer, of
course can then be used to point to either a Core
object
or a Grad
object).
We define a Student_info
class to store the handle
for each student object. Note that this class does all the pointer
maintenance for us so that the user of this class (e.g. the
main()
function) does not have to deal with all these
details.
Of particular importance is the use of the clone()
method which creates a duplicate of the object in the context of
Student_info
's copy constructor and the assignment operator.
Because the handle class only maintains a pointer to a Core
object, we have no idea what actual type is pointed to by the pointer.
By defining a trivial virtual clone()
method for each class,
we can ensure that during the invocation of the handle's copy constructor
or assignment operator, an object of the appropriate class will be
created for us. In the Core
class, the clone()
method is defined as:
virtual Core* clone() const { return new Core(*this); }
whereas in the the Grad
class, the clone()
method is defined as:
Grad* clone() const { return new Grad(*this); }
Note that the return types for the clone()
virtual
function are different (this is allowed by C++). Note that we generally
do not want classes other than the Student_info
class
to access the clone()
method. Therefore, this method
is made protected inside the Core
class. In order
for the Student_info
class to get access to the
clone()
method, the entire Student_info
class is made a friend
of the Core
class.
Student_info.h
#ifndef GUARD_Student_info_h
#define GUARD_Student_info_h
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
#include "Core.h"
class Student_info {
public:
// constructors and copy control
Student_info(): cp(0) { }
Student_info(std::istream& is): cp(0) { read(is); }
Student_info(const Student_info&);
Student_info& operator=(const Student_info&);
~Student_info() { delete cp; }
// operations
std::istream& read(std::istream&);
std::string name() const {
if (cp) return cp->name();
else throw std::runtime_error("uninitialized Student");
}
double grade() const {
if (cp) return cp->grade();
else throw std::runtime_error("uninitialized Student");
}
static bool compare(const Student_info& s1,
const Student_info& s2) {
return s1.name() < s2.name();
}
private:
Core* cp;
};
#endif
Student_info.h
Student_info.cpp
#include <iostream>
#include "Core.h"
#include "Student_info.h"
using std::istream;
istream& Student_info::read(istream& is)
{
delete cp; // delete previous object, if any
char ch;
is >> ch; // get record type
if (ch == 'U') {
cp = new Core(is);
} else {
cp = new Grad(is);
}
return is;
}
Student_info::Student_info(const Student_info& s): cp(0)
{
if (s.cp) cp = s.cp->clone();
}
Student_info& Student_info::operator=(const Student_info& s)
{
if (&s != this) {
delete cp;
if (s.cp)
cp = s.cp->clone();
else
cp = 0;
}
return *this;
}
Student_info.cpp
Last modified: March 11, 2004 16:16:46 NST (Thursday)