15.2. Defining Base and Derived Classes
In many ways, base and derived classes are defined like other classes we have already seen. However, there are some additional features that are required when defining classes in an inheritance hierarchy. This section will present those features.
15.2.1. Defining a Base Class
Like any other class, a base class has data and function
// Item sold at an undiscounted price // derived classes will define various discount strategies class Item_base { public: Item_base(const std::string &book = "", double sales_price = 0.0): isbn(book), price(sales_price) { } std::string book() const { return isbn; } // returns total sales price for a 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 ~Item_base() { } private: std::string isbn; // identifier for the item protected: double price; // normal, undiscounted price }; For the most part, this class looks like others we have seen. It defines a constructor along with the functions we have already described. That constructor uses default arguments (Section 7.4.1, p. 253), which allows it to be called with zero, one, or two arguments. It initializes the data members from these arguments.
The new
|
|
A base class usually should define as virtual any function that a derived class will need to redefine. |
In a base class, the
public
and
private
labels have their ordinary meanings:
Sometimes a class used as a base class has members that it wants to allow its derived classes to access, while still prohibiting access to those same members by other users. The protected access label is used for such members. A protected member may be accessed by a derived object but may not be accessed by general users of the type.
Our Item_base class expects its derived classes to redefine the net_price function. To do so, those classes will need access to the price member. Derived classes are expected to access isbn in the same way as ordinary users: through the book access function. Hence, the isbn member is private and is inaccessible to classes that inherit from Item_base .
Exercises Section 15.2.1
|
The protected access label can be thought of as a blend of private and public :
Like private members, protected members are inaccessible to users of the class.
Like public members, the protected members are accessible to classes derived from this class.
In addition, protected has another important property:
A derived object may access the protected members of its base class only through a derived object. The derived class has no special access to the protected members of base type objects.
As an example, let's assume that Bulk_item defines a member function that takes a reference to a Bulk_item object and a reference to an Item_base object. This function may access the protected members of its own object as well as those of its Bulk_item parameter. However, it has no special access to the protected members in its Item_base parameter:
void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b)
{
//
attempt to use
protected
member
double ret = price; //
ok: uses
this->price
ret = d.price; //
ok: uses
price
from a
Bulk_item
object
ret = b.price; //
error: no access to
price
from an
Item_base
}
The use of d.price is okay, because the reference to price is through an object of type Bulk_item . The use of b.price is illegal because Bulk_item has no special access to objects of type Item_base .
Key Concept: Class Design and Protected MembersIn the absence of inheritance, a class has two kinds of users: members of the class itself and the users of that class. This separation between kinds of users is reflected in the division of the class into private and public access levels. Users may access only the public interface; class members and friends may access both the public and private members.
Under inheritance, there is now a third kind of user of a class: programmers who will define new classes that are derived from the class. The provider of a derived class often (but not always) needs access to the (ordinarily
private
) base-class implementation. To allow that access while still preventing general access to the implementation, an additional access label,
protected
, is provided. The data and function members in a
protected
section of a class
When designing a class to serve as a base class, the criteria for designating a member as public do not change: It is still the case that interface functions should be public and data generally should not be public . A class designed to be inherited from must decide which parts of the implementation to declare as protected and which should be private . A member should be made private if we wish to prevent subsequently derived classes from having access to that member. A member should be made protected if it provides an operation or data that a derived class will need to use in its implementation. In other words, the interface to the derived type is the combination of both the protected and public members. |
To define a derived class, we use a
class derivation list
to specify the base class(es). A class derivation list
class
classname: access-label base-class
where
access-label
is one of
public, protected
, or
private
, and
base-class
is the name of a previously defined class. As we'll see, a derivation list might
We'll have more to say about the access label used in a derivation list in Section 15.2.5 (p. 570). For now, what's useful to know is that the access label determines the access to the inherited members. When we want to inherit the interface of a base class, then the derivation should be public .
A derived class inherits the members of its base class and may define additional members of its own. Each derived object contains two parts: those members that it inherits from its base and those it defines itself. Typically, a derived class (re)defines only those aspects that differ from or extend the behavior of the base.
In our bookstore application, we will derive Bulk_item from Item_base , so Bulk_item will inherit the book, isbn , and price members. Bulk_item must redefine its net_price function and define the data members needed for that operation:
// discount kicks in when a specified number of copies of same book are sold // the discount is expressed as a fraction used to reduce the normal price class Bulk_item : public Item_base { public: // redefines base version so as to implement bulk purchase discount policy double net_price(std::size_t) const; private: std::size_t min_qty; // minimum purchase for discount to apply double discount; // fractional discount to apply };
Each Bulk_item object contains four data elements: It inherits isbn and price from Item_base and defines min_qty and discount . These latter two members specify the minimum quantity and the discount to apply once that number of copies are purchased. The Bulk_item class also needs to define a constructor, which we shall do in Section 15.4 (p. 580).
Ordinarily, derived classes redefine the virtual functions that they inherit, although they are not requried to do so. If a derived class does not redefine a virtual, then the version it uses is the one defined in its base class.
A derived type must include a declaration for each inherited member it intends to redefine. Our Bulk_item class says that it will redefine the net_price function but will use the inherited version of book .
With one exception, the declaration (Section 7.4, p. 251)of a virtual function in the derived class must exactly match the way the function is defined in the base. That exception applies to virtuals that return a reference (or pointer) to a type that is itself a base class. A virtual function in a derived class can return a reference (or pointer) to a class that is public ly derived from the type returned by the base-class function.
For example, the Item_base class might define a virtual function that returned an Item_base* . If it did, then the instance defined in the Bulk_item class could be defined to return either an Item_base* or a Bulk_item* . We'll see an example of this kind of virtual in Section 15.9 (p. 607).
|
Once a function is declared as virtual in a base class it remains virtual; nothing the derived classes do can change the fact that the function is virtual. When a derived class redefines a virtual, it may use the virtual keyword, but it is not required to do so. |
A derived object consists of multiple parts: the (non static) members defined in the derived class itself plus the subobjects made up of the (non static) members of its base class. We can think of our Bulk_item class as consisting of two parts as represented in Figure 15.1.
|
There is no requirement that the compiler lay out the base and derived parts of an object contiguously. Hence, Figure 15.1 is a conceptual, not physical, representation of how classes work. |
As with any member function, a derived class function can be defined inside the class or outside, as we do here for the net_price function:
//
if specified number of items are purchased, use discounted price
double Bulk_item::net_price(size_t cnt) const
{
if (cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
This function generates a discounted price: If the given quantity is more than min_qty , we apply the discount (which was stored as a fraction) to the price .
|
Because each derived object has a base-class part, classes may access the public and protected members of its base class as if those members were members of the derived class itself. |
A class must be defined before it can be used as a base class. Had we declared, but not defined, Item_base , we could not use it as our base class:
class Item_base; // declared but not defined // error: Item_base must be defined class Bulk_item : public Item_base { ... };
The reason for this restriction should already be easy to see: Each derived class contains, and may access, the members of its base class. To use those members, the derived class must konw what they are. One
A base class can itself be a derived class:
class Base { /* ... */ };
class D1: public Base { /* ... */ };
class D2: public D1 { /* ... */ };
Each class inherits all the members of its base class. The most derived type inherits the members of its base, which in
If we need to declare (but not yet define) a derived class, the declaration contains the class name but does not include its derivation list. For example, the following forward declaration of Bulk_item results in a compile-time error:
//
error: a forward declaration must not include the derivation list
class Bulk_item : public Item_base;
The correct forward declarations are:
//
forward declarations of both derived and nonderived class
class Bulk_item;
class Item_base;
By default, function calls in C++ do not use dynamic binding. To trigger dynamic binding, two conditions must be met: First, only member functions that are specified as virtual can be dynamically bound. By default, member functions are not virtual; nonvirtual functions are not dynamically bound. Second, the call must be made through a reference or a pointer to a base-class type. To understand this requirement, we need to understand what happens when we use a reference or pointer to an object that has a type from an inheritance hierarchy.
Exercises Section 15.2.3
|
Because every derived object contains a base part, we can bind a base-type reference to the base-class part of a derived object. We can also use a pointer to base to point to a derived object:
// function with an Item_base reference parameter double print_total(const Item_base&, size_t); Item_base item; // object of base type // ok: use pointer or reference to Item_base to refer to an Item_base object print_total(item, 10); // passes reference to an Item_base object Item_base *p = &item; // p points to an Item_base object Bulk_item bulk; // object of derived type // ok: can bind a pointer or reference to Item_base to a Bulk_item object print_total(bulk, 10); // passes reference to the Item_base part of bulk p = &bulk; // p points to the Item_base part of bulk
This code uses the same base-type pointer to point to an object of the base type and to an object of the derived type. It also calls a function that expects a reference to the base type, passing an object of the base-class type and also passing an object of the derived type. Both uses are fine, because every derived object has a base part.
Because we can use a base-type pointer or reference to refer to a derived-type object, when we use a base-type reference or pointer, we don't know the type of the object to which the pointer or reference is bound: A base-type reference or pointer might refer to an object of base type or an object of derived type. Regardless of which actual type the object has, the compiler treats the object as if it is a base type object. Treating a derived object as if it were a base is safe, because every derived object has a base subobject. Also, the derived class inherits the operations of the base class, meaning that any operation that might be performed on a base object is available through the derived object as well.
|
The crucial point about references and pointers to base-class types is that the static type the type of the reference or pointer, which is knowable at compile timeand the dynamic type the type of the object to which the pointer or reference is bound, which is knowable only at run timemay differ. |
Binding a base-type reference or pointer to a derived object has no effect on the underlying object. The object itself is unchanged and remains a derived object. The fact that the actual type of the object might differ from the static type of the reference or pointer addressing that object is the key to dynamic binding in C++.
When a virtual function is called through a reference or pointer, the compiler generates code to decide at run time which function to call. The function that is called is the one that corresponds to the dynamic type. As an example, let's look again at the print_total function:
// calculate and print price for given number of copies, applying any discounts void print_total(ostream &os, const Item_base &item, size_t n) { os << "ISBN: " << item.book() // calls Item_base::book << "\tnumber sold: " << n << "\ttotal price: " // virtual call: which version of net_price to call is resolved at run time << item.net_price(n) << endl; }
Because the item parameter is a reference and net_price is virtual, the version of net_price that is called in item.net_price(n) depends at run time on the actual type of the argument bound to the item parameter:
Item_base base;
Bulk_item derived;
//
print_total
makes a virtual call to
net_price
print_total(cout, base, 10); //
calls
Item_base::net_price
print_total(cout, derived, 10); //
calls
Bulk_item::net_price
In the first call, the item parameter is bound, at run time, to an object of type Item_base . As a result, the call to net_price inside print_total calls the version defined in Item_base . In the second call, item is bound to an object of type Bulk_item . In this call, the version of net_price called from print_total will be the one defined by the Bulk_item class.
Key Concept: Polymorphism in C++The fact that the static and dynamic types of references and pointers can differ is the cornerstone of how C++ supports polymorphism. When we call a function defined in the base class through a base-class reference or pointer, we do not know the precise type of the object on which the function is executed. The object on which the function executes might be of the base type or it might be an object of a derived type. If the function called is nonvirtual, then regardless of the actual object type, the function that is executed is the one defined by the base type. If the function is virtual, then the decision as to which function to run is delayed until run time. The version of the virtual function that is run is the one defined by the type of the object to which the reference is bound or to which the pointer points. From the perspective of the code that we write, we need not care. As long as the classes are designed and implemented correctly, the operations will do the right thing whether the actual object is of base or derived type. On the other hand, an object is not polymorphicits type is known and unchanging. The dynamic type of an object (as opposed to a reference or pointer) is always the same as the static type of the object. The function that is run, virtual or nonvirtual, is the one defined by the type of the object.
|
Regardless of the actual type of the argument passed to print_total , the call of book is resolved at compile time to Item_base::book .
|
Even if Bulk_item defined its own version of the book function, this call would call the one from the base class. |
Nonvirtual functions are always resolved at compile time based on the type of the object, reference, or pointer from which the function is called. The type of item is reference to const Item_base , so a call to a nonvirtual function on that object will call the one from Item_base regardless of the type of the actual object to which item refers at run time.
In some cases, we want to override the virtual mechanism and force a call to use a particular version of a virtual function. We can do so by using the scope operator:
Item_base *baseP = &derived;
//
calls version from the base class regardless of the dynamic type of
baseP
double d = baseP->Item_base::net_price(42);
This code forces the call to net_price to be resolved to the version defined in Item_base . The call will be resolved at compile time.
|
Only code inside member functions should ever need to use the scope operator to override the virtual mechanism. |
Why might we wish to override the virtual mechanism? The most common reason is when a derived-class virtual calls the version from the base. In such cases, the base-class version might do work common to all types in the hierarchy. Each derived type adds only whatever is particular to its own type.
For example, we might define a Camera hierarchy with a virtual display operation. The display function in the Camera class would display information common to all Camera s. A derived class, such as PerspectiveCamera , would need to display both that common information and the information unique to PerspectiveCamera . Rather than duplicate the Camera operations within PerspectiveCamera 's implementation of display , we could explicitly invoke the Camera version to display the common information. In a case such as this one, we'd know exactly which instance to invoke, so there would be no need to go through the virtual mechanism.
|
When a derived virtual calls the base-class version, it must do so explicitly using the scope operator. If the derived function neglected to do so, then the call would be resolved at run time and would be a call to itself, resulting in an infinite recursion. |
Like any other function, a virtual function can have default arguments. As usual, the value, if any, of a default argument used in a given call is determined at compile time. If a call omits an argument that has a default value, then the value that is used is the one defined by the type through which the function is called, irrespective of the object's dynamic type. When a virtual is called through a reference or pointer to base, then the default argument is the value specified in the declaration of the virtual in the base class. If a virtual is called through a pointer or reference to derived, the default argument is the one declared in the version in the derived class.
Using different default arguments in the base and derived versions of the same virtual is almost
Access to members defined within a derived class is controlled in exactly the same way as access is handled for any other class (Section 12.1.2, p. 432). A derived class may define zero or more access labels that specify the access level of the members following that label. Access to the members the class inherits is controlled by a combination of the access level of the member in the base class and the access label used in the derived class' derivation list.
Exercises Section 15.2.4
|
|
Each class controls access to the members it defines. A derived class may further restrict but may not loosen the access to the members that it inherits. |
The base class itself specifies the minimal access control for its own members. If a member is private in the base class, then only the base class and its friends may access that member. The derived class has no access to the private members of its base class, nor can it make those members accessible to its own users. If a base class member is public or protected , then the access label used in the derivation list determines the access level of that member in the derived class:
In public inheritance , the members of the base retain their access levels: The public members of the base are public members of the derived and the protected members of the base are protected in the derived.
In protected inheritance , the public and protected members of the base class are protected members in the derived class.
In private inheritance , all the members of the base class are private in the derived class.
As an example, consider the following hierarchy:
class Base {
public:
void basemem(); //
public
member
protected:
int i; //
protected
member
// ...
};
struct Public_derived : public Base {
int use_base() { return i; } //
ok: derived classes can access
i
// ...
};
struct Private_derived : private Base {
int use_base() { return i; } //
ok: derived classes can access
i
};
All classes that inherit from Base have the same access to the members in Base , regardless of the access label in their derivation lists. The derivation access label controls the access that users of the derived class have to the members inherited from Base :
Base b;
Public_derived d1;
Private_derived d2;
b.basemem(); //
ok:
basemem
is
public
d1.basemem(); //
ok:
basemem
is
public
in the derived class
d2.basemem(); //
error:
basemem
is
private
in the derived class
Both Public_derived and Private_derived inherit the basemem function. That member retains its access level when the inheritance is public , so d1 can call basemem . In Private_derived , the members of Base are private ; users of Private_derived may not call basemem .
The derivation access label also controls access from indirectly derived classes:
struct Derived_from Private : public Private_derived {
//
error:
Base::i
is
private
in
Private_derived
int use_base() { return i; }
};
struct Derived_from_Public : public Public_derived {
//
ok:
Base::i
remains
protected
in
Public_derived
int use_base() { return i; }
};
Classes derived from Public_derived may access i from the Base class because that member remains a protected member in Public_derived . Classes derived from Private_derived have no such access. To them all the members that Private_base inherited from Base are private .
A public ly derived class inherits the interface of its base class; it has the same interface as its base class. In well-designed class hierarchies, objects of a public ly derived class can be used wherever an object of the base class is expected.
Classes derived using either
private
or
protected
do not inherit the base-class interface. Instead, these
As we'll see in Section 15.3 (p. 577), whether a class uses interface or implementation inheritance has important implications for users of the derived class.
|
By far the most common form of inheritance is public . |
Key Concept: Inheritance versus CompositionThe design of inheritance hierarchies is a complex topic in its own right and well beyond the scope of this language primer. However, there is one important design guide that is so fundamental that every programmer should be familiar with it.
When we define one class as
Another common relationship among types is a so-called "Has A" relationship. Our bookstore classes have a price and they have an ISBN. Types
|
When inheritance is
private
or
protected
, the access level of members of the base may be more
class Base {
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base { . . . };
|
The derived class can restore the access level of an inherited member. The access level cannot be made more or less restrictive than the level originally specified within the base class. |
In this hierarchy,
size
is
public
in
Base
but
private
in
Derived
. To make
size public
in
Derived
we can add a
using
declaration for it to a
public
section in
Derived
. By changing the definition of
Derived
as
class Derived : private Base {
public:
//
maintain access levels for members related to the size of the object
using Base::size;
protected:
using Base::n;
// ...
};
Just as we can use a using declaration (Section 3.1, p. 78) to use names from the std namespace, we may also use a using declaration to access a name from a base class. The form is the same except that the left-hand side of the scope operator is a class name instead of a namespace name.
In Section 2.8 (p. 65) we learned that classes defined with the
struct
and
class
keywords have different default access levels. Similarly, the default inheritance access level
class Base { /* ... */ };
struct D1 : Base { /* ... */ }; //
public
inheritance by default
class D2 : Base { /* ... */ }; //
private
inheritance by default
It is a common
class D3 : public Base {
public:
/* ... */
};
//
equivalent definition of
D3
struct D3 : Base { //
inheritance
public
by default
/* ... */ //
initial member access
public
by default
};
struct D4 : private Base {
private:
/* ... */
};
//
equivalent definition of
D4
class D4 : Base { //
inheritance
private
by default
/* ... */ //
initial member access
private
by default
};
|
Although private inheritance is the default when using the class keyword, it is also relatively rare in practice. Because private inheritance is so rare, it is usually a good idea to explicitly specify private , rather than rely on the default. Being explicit makes it clear that private inheritance is intended and not an oversight. |
Exercises Section 15.2.5
|
As with any other class, a base or derived class can make other class(es) or function(s) friends (Section 12.5, p. 465). Friends may access the class' private and protected data.
|
Friendship is not inherited. Friends of the base have no special access to members of its derived classes. If a base class is granted friendship, only the base has special access. Classes derived from that base have no access to the class granting friendship. |
Each class controls friendship to its own members:
class Base {
friend class Frnd;
protected:
int i;
};
//
Frnd
has no access to members in
D1
class D1 : public Base {
protected:
int j;
};
class Frnd {
public:
int mem(Base b) { return b.i; } //
ok:
Frnd is friend to
Base
int mem(D1 d) { return d.i; } //
error: friendship doesn't inherit
};
//
D2
has no access to members in
Base
class D2 : public Frnd {
public:
int mem(Base b) { return b.i; } //
error: friendship doesn't inherit
};
If a derived class wants to grant access to its members to the friends of its base class, the derived class must do so explicitly: Friends of the base have no special access to types derived from that base class. Similarly, if a base and its derived types all need access to another class, that class must
If a base class defines a static member (Section 12.6, p. 467) there is only one such member defined for the entire hierarchy. Regardless of the number of classes derived from the base class, there exists a single instance of each static member. static members obey normal access control: If the member is private in the base class, then derived classes have no access to it. Assuming the member is accessible, we can access the static member either through the base or derived class. As usual, we can use either the scope operator or the dot or arrow member access operators.
struct Base {
static void statmem(); //
public
by default
};
struct Derived : Base {
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj)
{
Base::statmem(); //
ok:
Base
defines
statmem
Derived::statmem(); //
ok:
Derived
in herits
statmem
//
ok: derived objects can be used to access static from base
derived_obj.statmem(); //
accessed through
Derived
object
statmem(); //
accessed through this class
Exercises Section 15.2.7
|