March 10 (Wednesday) March 15 (Monday)
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;
}
main_orig.cpp
virtual
functions and abstract base classes
(K&M — Chapter 15)
We've discussed the concept of virtual
functions during
the previous class. Such functions lead to the concept of run-time
or dynamic binding, in which the actual function call is not know
until run-time. Typically, a base class implements some default
behaviour for the virtual function and each derived classes that
does not override this function, will inherit the implementation
as defined by the base class.
In many practical cases, however, it does not make sense for a base
class to implement a virtual function. For example, consider a generic
base class called Investment
and two derived classes
Cash
and Stock
. We are asked to implement
a virtual function which will determine the value of a particular
investment. However, for the Investment
class, there is
no way to calculate its value, because it is too generic or vague —
there is no information available in the class from which a value can
be calculated. Therefore, rather than implement a empty function for
its value()
method, we instead use a pure virtual
function:
class Investment { public: ... virtual double value() const = 0; // Pure virtual function. ... };
By assigning 0 to the function declaration, we are saying that it is not
possible for the Investment
class to determine its value —
the Investment
class is simply too abstract to allow for
a reasonable definition. As a result, because Investment
has a pure virtual function, we say that this class is an abstract
base class. It is not possible to create objects from an
abstract base classes, the compiler will complain. Similarly, objects
cannot be created from any derived classes that does not override
all the pure virtual functions inherited from an abstract base class.
Before a derived class can actually be instantiated, we must override
all pure virtual functions as defined in its base class (or ancestor of
its base class).
The Cash
and Stock
class can then override
this pure virtual function as follows:
class Cash : public Investment { public: double value() const { return amount_ * rate_; } private: double amount_; double rate_; }; class Stock : public Investment { public: double value() const { return num_units_ * stock_price_; } private: double num_units_; double stock_price_; };
Note that we can still create pointers to the abstract base class, but we cannot create objects of classes that contain pure virtual functions:
#include <iostream>
struct A {
virtual void hello() = 0;
};
struct B : public A {
virtual void hello() { std::cout << "I'm B" << std::endl; }
};
struct C : public A {
virtual void hello() { std::cout << "I'm C" << std::endl; }
};
int
main()
{
A a_object // Illegal!
A *a; // Okay.
a = new A; // Illegal!
a = new B; // Okay
a->hello(); // "I'm B"
a = new C; // Okay
a->hello(); // "I'm C"
}
pvf.cpp
Typically, the pointer to the base class (e.g.
A
in this case), will be used to point to objects
of classes derived from the base class for the purposes of invoking
virtual methods via the pointer.
As mentioned above, we have a base Investment
class
and two derived classes Cash
and Stock
.
Our constructor for the investment class is fairly straightforward as
all we had to do was initialize the name_
data member using
the constructor's member initialization list. Remember that the member
initialization list is the text in the constructor between the colon
and the start of the constructor's body (the body is empty in the case
of the Investment
constructor):
class Investment { public: Investment (const std::string &n) : name_(n) { } ... protected: std::string name_; };
Note the convention of using a underscore suffix on data members of the class. This helps to distinguish them from non-member data values (e.g. method parameters, local variables etc.).
During construction of a derived class, we often want to pass
parameters to the base class so that the base portions of the
class can be initialized correctly. To do this, we use the
member initialization list again. For example, to initialize
a Cash
object, we make a constructor that takes
a string
reference (along with details relevant
to the Cash
class), then we call the Investment
constructor in the member initialization list of Cash
's
constructor, passing the string
parameter to it:
class Cash : public Investment { public: Cash (const std::string &n, const double a, const double r) : Investment(n), amount_(a), rate_(r) { } ... private: double amount_; double rate_; };
This will correctly initialize the name_
data member
in Investment
base class. Note that even though the
name_
data member is protected
inside the
base class (and not private
), the Cash
constructor can not initialize the name_
field directly.
This is because the name_
attribute is inherited from the
Investment
class — it is not an immediate attribute of
the Cash
class, therefore the Investment
class is responsible for initializing it.
In the context of the investment problem that we were discussing above, we
can introduce a Portfolio
class to keep track of a collection
of investments. In the portfolio.h
header file, we use a
forward declaration to inform the compiler of the presence of
a class called Investment
. The syntax of this declaration
is quite trivial:
class Investment; class Portfolio { public: ... private: std::vector<Investment*> investments_; };
Because the Portfolio
class uses only a pointer to an
Investment
there is no need to actually #include
the full investment.h
header file. Instead, we can
simply provide a forward declaration and the compiler will be happy.
If portfolio.h
had code or method declarations that required
a full-fledged Investment
object (as opposed to just a
pointer or a reference), then we would have to #include
the investment.h
header file in portfolio.h
.
The portfolio.cpp
source file, however, could conceivably
create Investment
objects (or more precisely, objects of
classes derived from the Investment
class) and would likely
call member functions in the Investment
hierarchy in order
to calculate values such as the overall worth of the portfolio —
the portfolio.cpp
source file must therefore #include
"investment.h"
. Note that the header files for classes
derived from Investment
(e.g. stock.h
and cash.h
) must also #include
the
investment.h
header, but multiple inclusion of the same
header file is prevented because of the #ifndef
guards at
the top of every header file.
The usage of a forward declaration above is pedantic, at best. More
practically, forward declarations are required when you are creating
mutually referential classes. If class A
contains a pointer
to class B
and class B
contains a pointer
to class A
, then a forward declaration will be necessary
in order to break the chicken-and-the-egg problem that such mutually
referential classes create:
class B; class A { ... B *b; }; class B { ... A *a; };
Last modified: March 12, 2004 14:13:29 NST (Friday)