We’ll start by completing the definition of our Quote
class:
class Quote {
public:
Quote() = default; // = default see § 7.1.4 (p. 264)
Quote(const std::string &book, double sales_price):
bookNo(book), price(sales_price) { }
std::string isbn() const { return bookNo; }
// returns the total sales price for the specified number of items
// derived classes will override and apply different discount algorithms
virtual double net_price(std::size_t n) const
{ return n * price; }
virtual ~Quote() = default; // dynamic binding for the destructor
private:
std::string bookNo; // ISBN number of this item
protected:
double price = 0.0; // normal, undiscounted price
};
The new parts in this class are the use of virtual
on the net_price
function and the destructor, and the protected
access specifier. We’ll explain virtual destructors in §15.7.1 (p. 622), but for now it is worth noting that classes used as the root of an inheritance hierarchy almost always define a virtual destructor.
Base classes ordinarily should define a virtual destructor. Virtual destructors are needed even if they do no work.
Derived classes inherit the members of their base class. However, a derived class needs to be able to provide its own definition for operations, such as net_price
, that are type dependent. In such cases, the derived class needs to override the definition it inherits from the base class, by providing its own definition.
In C++, a base class must distinguish the functions it expects its derived classes to override 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 override. When we call a virtual function through a pointer or reference, the call will be dynamically bound. Depending on the type of the object to which the reference or pointer is bound, the version in the base class or in one of its derived classes will be executed.
A base class specifies that a member function should be dynamically bound by preceding its declaration with the keyword virtual
. Any nonstatic
member function (§7.6, p. 300), other than a constructor, may be virtual. The virtual
keyword appears only on the declaration inside the class and may not be used on a function definition that appears outside the class body. A function that is declared as virtual
in the base class is implicitly virtual
in the derived classes as well. We’ll have more to say about virtual functions in §15.3 (p. 603).
Member functions that are not declared as virtual
are resolved at compile time, not run time. For the isbn
member, this is exactly the behavior we want. The isbn
function does not depend on the details of a derived type. It behaves identically when run on a Quote
or Bulk_quote
object. There will be only one version of the isbn
function in our inheritance hierarchy. Thus, there is no question as to which function to run when we call isbn()
.
A derived class inherits the members defined in its base class. However, the member functions in a derived class may not necessarily access the members that are inherited from the base class. Like any other code that uses the base class, a derived class may access the public
members of its base class but may not access the private
members. However, sometimes a base class has members that it wants to let its derived classes use while still prohibiting access to those same members by other users. We specify such members after a protected
access specifier.
Our Quote
class expects its derived classes to define their own net_price
function. To do so, those classes need access to the price
member. As a result, Quote
defines that member as protected
. Derived classes are expected to access bookNo
in the same way as ordinary users—by calling the isbn
function. Hence, the bookNo
member is private
and is inaccessible to classes that inherit from Quote
. We’ll have more to say about protected
members in §15.5 (p. 611).