The key ideas in object-oriented programming are data abstraction, inheritance, and dynamic binding. Using data abstraction, we can define classes that separate interface from implementation (Chapter 7). Through inheritance, we can define classes that model the relationships among similar types. Through dynamic binding, we can use objects of these types while ignoring the details of how they differ.
Classes related by inheritance form a hierarchy. Typically there is a base class at the root of the hierarchy, from which the other classes inherit, directly or indirectly. These inheriting classes are known as derived classes. The base class defines those members that are common to the types in the hierarchy. Each derived class defines those members that are specific to the derived class itself.
To model our different kinds of pricing strategies, we’ll define a class named Quote
, which will be the base class of our hierarchy. A Quote
object will represent undiscounted books. From Quote
we will inherit a second class, named Bulk_quote
, to represent books that can be sold with a quantity discount.
These classes will have the following two member functions:
•
isbn()
, which will return the ISBN. This operation does not depend on the specifics of the inherited class(es); it will be defined only in classQuote
.
•
net_price(size_t)
, which will return the price for purchasing a specified number of copies of a book. This operation is type specific; bothQuote
andBulk_quote
will define their own version of this function.
In C++, a base class distinguishes functions that are type dependent from those that it expects its derived classes to inherit without change. The base class defines as virtual
those functions it expects its derived classes to define for themselves. Using this knowledge, we can start to write our Quote
class:
class Quote {
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};
A derived class must specify the class(es) from which it intends to inherit. It does so in a class derivation list, which is a colon followed by a comma-separated list of base classes each of which may have an optional access specifier:
class Bulk_quote : public Quote { // Bulk_quote inherits from Quote
public:
double net_price(std::size_t) const override;
};
Because Bulk_quote
uses public
in its derivation list, we can use objects of type Bulk_quote
as if they were Quote
objects.
A derived class must include in its own class body a declaration of all the virtual functions it intends to define for itself. A derived class may include the virtual
keyword on these functions but is not required to do so. For reasons we’ll explain in § 15.3 (p. 606), the new standard lets a derived class explicitly note that it intends a member function to override a virtual that it inherits. It does so by specifying override
after its parameter list.
Through dynamic binding, we can use the same code to process objects of either type Quote
or Bulk_quote
interchangeably. For example, the following function prints the total price for purchasing the given number of copies of a given book:
// calculate and print the price for the given number of copies, applying any discounts
double print_total(ostream &os,
const Quote &item, size_t n)
{
// depending on the type of the object bound to the item parameter
// calls either Quote::net_price or Bulk_quote::net_price
double ret = item.net_price(n);
os << "ISBN: " << item.isbn() // calls Quote::isbn
<< " # sold: " << n << " total due: " << ret << endl;
return ret;
}
This function is pretty simple—it prints the results of calling isbn
and net_price
on its parameter and returns the value calculated by the call to net_price
.
Nevertheless, there are two interesting things about this function: For reasons we’ll explain in § 15.2.3 (p. 601), because the item
parameter is a reference to Quote
, we can call this function on either a Quote
object or a Bulk_quote
object. And, for reasons we’ll explain in § 15.2.1 (p. 594), because net_price
is a virtual function, and because print_total
calls net_price
through a reference, the version of net_price
that is run will depend on the type of the object that we pass to print_total
:
// basic has type Quote; bulk has type Bulk_quote
print_total(cout, basic, 20); // calls Quote version of net_price
print_total(cout, bulk, 20); // calls Bulk_quote version of net_price
The first call passes a Quote
object to print_total
. When print_total
calls net_price
, the Quote
version will be run. In the next call, the argument is a Bulk_quote
, so the Bulk_quote
version of net_price
(which applies a discount) will be run. Because the decision as to which version to run depends on the type of the argument, that decision can’t be made until run time. Therefore, dynamic binding is sometimes known as run-time binding.
In C++, dynamic binding happens when a virtual function is called through a reference (or a pointer) to a base class.