Derived Classes and Inheritance
In addition to encapsulation and information hiding, C++ classes provide two features that are fundamental to object-oriented programming:
Inheritance: the ability for a class to reuse the interface or functionality of another class.
Subtype polymorphism: the ability to use an object of a more specific type where an object of a more general type is expected.
We will discuss inheritance today, deferring subtype polymorphism until next time.
To motivate the concept of inheritance, consider the following
definitions of Chicken
and Duck
ADTs:
class Chicken {
public:
Chicken(const string &name_in)
: age(0), name(name_in),
roads_crossed(0) {
cout << "Chicken ctor" << endl;
}
string get_name() const {
return name;
}
int get_age() const {
return age;
}
void cross_road() {
++roads_crossed;
}
void talk() const {
cout << "bawwk" << endl;
}
private:
int age;
string name;
int roads_crossed;
};
|
class Duck {
public:
Duck(const string &name_in)
: age(0), name(name_in),
num_ducklings(0) {
cout << "Duck ctor" << endl;
}
string get_name const {
return name;
}
int get_age() const {
return age;
}
void have_babies() {
num_ducklings += 7;
}
void talk() const {
cout << "quack" << endl;
}
private:
int age;
string name;
int num_ducklings;
};
|
The two ADTs are nearly identical – both have age
and name
member variables, with their corresponding get functions, and both
have a talk()
member function that makes the appropriate chicken
or duck sound. In terms of differences, chickens tend to cross roads
(since they don’t fly very well), so we keep track of how often they
do that. On the other hand, ducks are often accompanied by their
ducklings, so we keep track of how many they have.
Intuitively, it makes sense for Chicken
and Duck
to share a
lot of functionality, since chickens and ducks are both types of
birds.
This “is-a” relationship, where a Chicken
is a Bird
and a
Duck
is also a Bird
, can be encoded with inheritance. We
write Bird
as a base class, then write Chicken
and Duck
as classes that inherit or derive from Bird
. We place the
common functionality in Bird
, which then gets inherited by the
derived classes.
class Bird {
public:
Bird(const string &name_in)
: age(0), name(name_in) {
cout << "Bird ctor" << endl;
}
string get_name() const {
return name;
}
int get_age() const {
return age;
}
void have_birthday() {
++age;
}
void talk() const {
cout << "tweet" << endl;
}
private:
int age;
string name;
};
Here, all birds have a name and an age, and the generic sound a bird makes is “tweet”.
For more a more specific kind of bird, we obtain the functionality of the base class by deriving from it:
class Chicken : public Bird {
...
};
The syntax for deriving from a base class is to put a colon after the
name of the derived class, then the public
keyword, then the name
of the base class. This results in public inheritance, where it is
part of the interface of Chicken
that it derives from Bird
.
Without the public
keyword, it would be private inheritance
[1], where it is an implementation detail and not part of the
interface that Chicken
derives from Bird
.
Now that Chicken
derives from Bird
, it inherits the
functionality of the Bird
class, and the public interface of
Bird
is also supported by Chicken
:
Chicken c("Myrtle");
c.have_birthday();
cout << c.get_name() << " " << c.get_age(); // prints Myrtle 1
Functionality that is specific to a particular kind of bird goes in the class for that bird:
class Chicken : public Bird {
public:
...
void cross_road() {
++roads_crossed;
}
void talk() const {
cout << "bawwk" << endl;
}
private:
int roads_crossed;
};
Here, we have the additional data member roads_crossed
; name
and age
are inherited from Bird
. (They are not directly
accessible from the Chicken
class, however, since they are
private. We will come back to this later.)
Figure 36 illustrates the layout of Bird
and Chicken
in memory.
int main() {
Bird big_bird("Big Bird");
Chicken myrtle("Myrtle");
}
The memory of Chicken
actually consists of a Bird
piece, plus
the additional Chicken
members. Thus, the data members defined by
Bird
are also included in a Chicken
.
In order to properly initialize a derived-class object, its constructor must ensure that its base-class subobject is also appropriately initialized. The base class may have private member variables, which cannot be accessed from the derived class, so the derived class does not initialize the inherited members directly. Instead, it invokes a base-class constructor in the member-initializer list of its own constructors:
class Chicken : public Bird {
public:
Chicken(const string &name_in)
: Bird(name_in), roads_crossed(0) {
cout << "Chicken ctor" << endl;
}
...
private:
int roads_crossed;
};
In C++, a derived-class constructor always invokes a constructor for the base class. If an explicit invocation does not appear in the member-initializer list, there is an implicit call to the default constructor. If the base class has no default constructor, an error results:
class Chicken : public Bird {
public:
Chicken(const string &name_in)
: roads_crossed(0) { // ERROR: implicit call to Bird(), which doesn't exist
cout << "Chicken ctor" << endl;
}
...
};
For completeness, the following is an implementation of Duck
as a
derived class of Bird
:
class Duck : public Bird {
public:
Duck(const string &name_in)
: Bird(name_in), num_ducklings(0) {
cout << "Duck ctor" << endl;
}
void have_babies() {
num_ducklings += 7;
}
void talk() const {
cout << "quack" << endl;
}
private:
int num_ducklings;
};
By writing Bird
and deriving from it in both Chicken
and
Duck
, we have avoided duplication of the shared functionality.
Ordering of Constructors and Destructors
We have already seen that in most cases, a constructor is invoked when a class-type object is created. Similarly, a destructor is invoked when a class-type object’s lifetime is over. For a local variable, this is when the variable goes out of scope. The following illustrates an example:
class Foo {
public:
Foo() { // constructor
cout << "Foo ctor" << endl;
}
~Foo() { // destructor
cout << "Foo dtor" << endl;
}
};
void func() {
Foo x;
}
int main() {
cout << "before call" << endl;
func();
cout << "after call" << endl;
}
The class Foo
has a custom destructor, written as ~Foo()
,
which runs when a Foo
object is dying. Here, we just have both the
constructor and destructor print messages to standard out. The
following is printed when the code is run:
before call
Foo ctor
Foo dtor
after call
We will cover destructors in more detail later in the course. For now, we concern ourselves solely with the order in which constructors and destructors execute when we have derived classes.
When there are multiple objects that are constructed and destructed, C++ follows a “socks-and-shoes” ordering: when we put on socks and shoes in the morning, we put on socks first, then our shoes. In the evening, however, when we take them off, we do so in the reverse order: first our shoes, then our socks. In the case of a derived class, C++ will always construct the base-class subobject before initializing the derived-class pieces. Destruction is in the reverse order: first the derived-class destructor runs, then the base-class one. The following illustrates this order:
class Bird {
public:
Bird(const string &name_in)
: age(0), name(name_in) {
cout << "Bird ctor" << endl;
}
~Bird() {
cout << "Bird dtor" << endl;
}
...
};
class Chicken : public Bird {
public:
Chicken(const string &name_in)
: Bird(name_in), roads_crossed(0) {
cout << "Chicken ctor" << endl;
}
~Chicken() {
cout << "Chicken dtor" << endl;
}
...
};
int main() {
cout << "construction:" << endl;
Chicken myrtle("Myrtle");
cout << "destruction:" << endl;
}
The following results from running the code:
construction:
Bird ctor
Chicken ctor
destruction:
Chicken dtor
Bird dtor
When creating a Chicken
object, the invocation of the Bird
constructor is the first thing that happens in the Chicken
constructor. This is true regardless of whether or not an explicit
call to the Bird
constructor appears, and regardless of ordering
of the member-initializer list. Then the rest of the Chicken
constructor runs. When a Chicken
object is dying, first the code
in the Chicken
destructor runs. Then the code in the Bird
destructor automatically runs. (It is generally erroneous to invoke
the base-class destructor explicitly, since the compiler always does
so implicitly.)
The following code creates both a Chicken
and a Duck
object:
int main() {
cout << "construction:" << endl;
Chicken myrtle("Myrtle");
Duck donald("Donald");
cout << "destruction:" << endl;
}
Assuming that Duck
has a destructor that prints out "Duck
dtor"
, the code prints the following:
construction:
Bird ctor
Chicken ctor
Bird ctor
Duck ctor
destruction:
Duck dtor
Bird dtor
Chicken dtor
Bird dtor
We see the same ordering in Duck
construction and destruction.
Furthermore, we see that because myrtle
is constructed before
donald
, it is destructed after donald
– socks-and-shoes
ordering here as well.
Name Lookup and Hiding
When a member access is applied to an object, the compiler follows a specific process to look up the given name:
The compiler starts by looking for a member with that name in the compile-time or static type of the object. (We will discuss static and dynamic types next time.)
If no member with that name is found, the compiler repeats the process on the base class of the given type. If the type has no base class, a compiler error is generated.
If a member with the given name is found, the compiler then checks whether or not the member is accessible and whether the member can be used in the given context. [2] If not, a compiler error is generated – the lookup process does not proceed further.
If name lookup finds a set of overloaded functions within the same class, the compiler performs overload resolution to determine which overload is the most appropriate. We will discuss function overloading in more detail next time.
As an example, consider the following member accesses:
Chicken myrtle("Myrtle");
myrtle.get_age();
myrtle.talk();
myrtle.age;
myrtle.undefined;
For
myrtle.get_age()
, the compiler first looks for aget_age
member defined in theChicken
class. SinceChicken
does not define such a member, the compiler looks for aget_age
member in its baseBird
class. There is indeed aget_age
member inBird
, so the compiler then checks that the member is accessible and is a function that can be called with no arguments. These checks succeed, so the lookup process terminates successfully.For
myrtle.talk()
, the compiler looks for atalk
member inChicken
. There is one, so it then checks to make sure it is accessible and is a function that can be called with no arguments. This succeeds, and at runtime, it isChicken::talk()
that is called. The functionBird::talk()
is hidden, since the name lookup process never gets to it.For
myrtle.age
, the compiler looks for anage
member inChicken
. There is none, so the compiler looks inBird
. There is such a member, so the compiler checks whether it is accessible from the given context. The member is private, so this check fails, and the compiler reports an error.For
myrtle.undefined
, the compiler looks for anundefined
member inChicken
. There is none, so the compiler looks inBird
. There is no such member, andBird
has no base class, so the compiler reports an error.
As another example, consider the following code:
class Base {
public:
int x;
void foo(const string &s);
};
class Derived : public Base {
public:
void x();
void foo(int i);
};
int main() {
Derived d;
int a = d.x;
d.foo("hello");
}
When looking up d.x
, the compiler finds a member x
in
Derived
. However, it is a member function, which cannot be
assigned to an int
. Thus, the compiler reports an error – it does
not consider the hidden x
that is defined in Base
.
Similarly, when looking up d.foo
, the compiler finds a member
foo
in Derived
. Though it is a function, it cannot be called
with a string literal, so the compiler reports an error. Again, the
compiler does not consider the foo
that is defined in Base
;
that member is hidden by the foo
defined in Derived
.
To summarize, C++ does not consider the context in which a member is used until after its finds a member of the given name. This is in contrast to some other languages, which consider context in the lookup process itself.
On occasion, we wish to access a hidden member rather than the member
that hides it. The most common case is when a derived-class version of
a function calls the base-class version as part of its functionality.
The following illustrates this in Chicken
:
class Chicken : public Bird {
public:
...
void talk() const {
if (age >= 1) { // ERROR: age is private in Bird
cout << "bawwk" << endl;
} else {
// baby chicks make more of a tweeting rather than clucking noise
Bird::talk(); // call Bird's version of talk()
}
}
};
By using the scope-resolution operator, we are specifically asking for
the Bird
version of talk()
, enabling access to it even though
it is hidden. [3]
Interestingly, we can apply the scope-resolution operator as
part of dot or arrow syntax in order to access the base-class
version: myrtle.Bird::talk()
. However, this is very
uncommon in real code, and we will not do it at all in this
course.
The code above does have a problem: it accesses the age
member of
Bird
, which, though it is not hidden, is private and so not
accessible to Chicken
. There are two solutions to this problem:
We can declare
age
to be protected, which allows derived classes ofBird
to access the member but not the outside world.We can use the public
get_age()
function instead.
The former strategy is not desirable; since the member variable
age
is an implementation detail, making it protected exposes
implementation details to the derived classes. Instead, we should use
a get function to abstract the implementation detail. We could choose
to make such a function protected, so that it is part of the interface
that derived classes have access to but not the outside world. For
age
, we already have a public get function, so we can just use
that:
class Chicken : public Bird {
public:
...
void talk() const {
if (get_age() >= 1) {
cout << "bawwk" << endl;
} else {
// baby chicks make more of a tweeting rather than clucking noise
Bird::talk(); // call Bird's version of talk()
}
}
};