Content
Derived Classes and Inheritance Ordering of Constructors and Destructors Name Lookup and Hiding <- Go BackUniversity of Michigan at Ann Arbor
Last Edit Date: 03/01/2023
Disclaimer and Term of Use:
We do not guarantee the accuracy and completeness of the summary content. Some of the course material may not be included, and some of the content in the summary may not be correct. You should use this file properly and legally. We are not responsible for any results from using this file
This personal note is adapted from Professor Amir Kamil, Andrew DeOrio, James Juett, Sofia Saleem, and Saquib Razak. Please contact us to delete this file if you think your rights have been violated.
This work is licensed under a Creative Commons Attribution 4.0 International License.
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.
Let us first consider the following definitions of Chicken
and Duck
ADTs.
1 class Chicken { 2 public: 3 Chicken(const string &name_in) 4 : age(0), name(name_in), 5 roads_crossed(0) { 6 cout << "Chicken ctor" << endl; 7 } 8 9 string get_name() const { 10 return name; 11 } 12 13 int get_age() const { 14 return age; 15 } 16 17 void cross_road() { 18 ++roads_crossed; 19 } 20 21 void talk() const { 22 cout << "bawwk" << endl; 23 } 24 25 private: 26 int age; 27 string name; 28 int roads_crossed; 29 }; |
1 class Duck { 2 public: 3 Duck(const string &name_in) 4 : age(0), name(name_in), 5 num_ducklings(0) { 6 cout << "Duck ctor" << endl; 7 } 8 9 string get_name const { 10 return name; 11 } 12 13 int get_age() const { 14 return age; 15 } 16 17 void have_babies() { 18 num_ducklings += 7; 19 } 20 21 void talk() const { 22 cout << "quack" << endl; 23 } 24 25 private: 26 int age; 27 string name; 28 int num_ducklings; 29 }; |
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.
As a the graph shown above, where Chicken
is a Bird
and a Duck
is also a Bird
, so they can be encoded with inheritance.
Basically, we need to wirte 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.
1 class Bird { 2 public: 3 Bird(const string &name_in) 4 : age(0), name(name_in) { 5 cout << "Bird ctor" << endl; 6 } 7 8 string get_name() const { 9 return name; 10 } 11 12 int get_age() const { 13 return age; 14 } 15 16 void have_birthday() { 17 ++age; 18 } 19 20 void talk() const { 21 cout << "tweet" << endl; 22 } 23 24 private: 25 int age; 26 string name; 27 };
Here, all birds have a name and an age, and the generic sound a bird makes is “tweet”.
Then we can write Chicken
and Duck
as following:
class Chicken : public Bird{ ... }; |
class Duck : 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, where it is an implementation detail and not part of the interface that Chicken
derives from Bird
.
A derived class contains the member variables from the base class and may define additional ones.
You can call member functions of the base class on any derived class instances.
A derived class may "hide" a member function from the base class by defining its own version with the same signature.
A derived class constructor must call some version of the base class constructor in its constructor-initializer-list. (Unless there's a default constructor for the base class.)
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:
1 class Foo { 2 public: 3 Foo() { // constructor 4 cout << "Foo ctor" << endl; 5 } 6 7 ~Foo() { // destructor 8 cout << "Foo dtor" << endl; 9 } 10 }; 11 12 void func() { 13 Foo x; 14 } 15 16 int main() { 17 cout << "before call" << endl; 18 func(); 19 cout << "after call" << endl; 10 }
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.
Try it out:
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.
1 class Bird { 2 public: 3 Bird(const string &name_in) 4 : age(0), name(name_in) { 5 cout << "Bird ctor" << endl; 6 } 7 8 ~Bird() { 9 cout << "Bird dtor" << endl; 10 } 11 12 }; 13 14 class Chicken : public Bird { 15 public: 16 Chicken(const string &name_in) 17 : Bird(name_in), roads_crossed(0) { 18 cout << "Chicken ctor" << endl; 19 } 20 21 ~Chicken() { 22 cout << "Chicken dtor" << endl; 23 } 24 }; 25 26 int main() { 27 cout << "construction:" << endl; 28 Chicken myrtle("Myrtle"); 29 cout << "destruction:" << endl; 30 }
Try it out:
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.
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.
Take the following code as an example:
1 class Base { 2 public: 3 int x; 4 void foo(const string &s); 5 }; 6 7 class Derived : public Base { 8 public: 9 void x(); 10 void foo(int i); 11 }; 12 13 int main() { 14 Derived d; 15 int a = d.x; 16 d.foo("hello"); 17 }
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. We can achieve this by doing the following:
1 class Chicken : public Bird { 2 public: 3 ... 4 5 void talk() const { 6 if (get_age() >= 1) { // age >= 1 also works, but age cannot be private 7 cout << "bawwk" << endl; 8 } else { 9 // baby chicks make more of a tweeting rather than clucking noise 10 Bird::talk(); // call Bird's version of talk() 11 } 12 } 13 };
In line 6, we have a get_age()
public function for us to get the age of the bird, which can provide us a condition. We can also use age >= 1
if we change the member type of age from private
to protected
.
Note: Declare age
to be protected
allows derived classes of Bird
to access the member but not the outside world.