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
is common between undergraduate and graduate students and put them
in a class call 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 any 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
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 function
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 operation since new Grad will return a pointer
to an object of type Grad and Grad is
derived from Core.
If we got rid of the 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 are 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);
}
Finally, we can define a main() function that employs
the above two classes. Note the only major 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 will 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 remove 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;
}
We can also use a common C++ idiom called a handle to solve
the above problem. In this example, the main() function
is 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 the Student_info class the represent 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, that 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.
#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
#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;
}
main() function
Again, we define our main() function below. Note that
this main function is almost identical to the original main()
function that we saw in earlier lectures. The only change is that
the global sort function is passed a static
comparison method, as defined in the Student_info class.
Because this method is static, there is no need to invoke
this method via an object of type Student_info.
Instead we use this function simply by using the class name and the
scope resolution operator (i.e. Student_info::compare).
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
#include "Student_info.h"
using std::cin; using std::cout;
using std::domain_error; using std::endl;
using std::setprecision; using std::setw;
using std::sort; using std::streamsize;
using std::string; using std::vector;
using std::max;
int main()
{
vector<Student_info> students;
Student_info record;
string::size_type maxlen = 0;
// read and store the data
while (record.read(cin)) {
maxlen = max(maxlen, record.name().size());
students.push_back(record);
}
// alphabetize the student records
sort(students.begin(), students.end(), Student_info::compare);
// write the names and grades
for (vector<Student_info>::size_type i = 0;
i != students.size(); ++i) {
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;
}
}
return 0;
}
Last modified: Mon Mar 17 14:09:53 2003