17.1 — Introduction to inheritance
A hierarchy is a diagram that shows how various objects are related. Most hierarchies either show a progression over time (386 -> 486 -> Pentium), or categorize things in a way that moves from general to specific (fruit -> apple -> red delicious). If you’ve ever taken biology, the famous domain, kingdom, phylum, class, order, family, genus, and species ordering defines a hierarchy (from general to specific).
Here’s another example of a hierarchy: a square is a rectangle, which is a quadrilateral, which is a shape. A right triangle is a triangle, which is also a shape. Put into a hierarchy diagram, that would look like this:![在这里插入图片描述](https://img-blog.csdnimg.cn/41d6ecce310b4673905fd1f19e2a31b6.png)
This diagram goes from general (top) to specific (bottom), with each item in the hierarchy inheriting the properties and behaviors of the item above it.
17.2 — Basic inheritance in C++
A BaseballPlayer class
Have BaseballPlayer inherit those attributes from Person. Remember that inheritance represents an is-a relationship. Is a BaseballPlayer a Person? Yes, it is. So inheritance is a good choice here.
Making BaseballPlayer a derived class
To have BaseballPlayer inherit from our Person class, the syntax is fairly simple. After the class BaseballPlayer declaration, we use a colon, the word “public”, and the name of the class we wish to inherit. This is called public inheritance. We’ll talk more about what public inheritance means in a future lesson.
#include <iostream>
#include <string>
class Person
{
public:
std::string m_name{};
int m_age{};
Person(const std::string& name = "", int age = 0)
: m_name{name}, m_age{age}
{
}
const std::string& getName() const { return m_name; }
int getAge() const { return m_age; }
};
// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
double m_battingAverage{};
int m_homeRuns{};
BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
: m_battingAverage{battingAverage}, m_homeRuns{homeRuns}
{
}
};
int main()
{
// Create a new BaseballPlayer object
BaseballPlayer joe{};
// Assign it a name (we can do this directly because m_name is public)
joe.m_name = "Joe";
// Print out the name
std::cout << joe.getName() << '\n'; // use the getName() function we've acquired from the Person base class
return 0;
}
Which prints the value:
Joe
Inheritance chains
Why is this kind of inheritance useful?
Inheriting from a base class means we don’t have to redefine the information from the base class in our derived classes. We automatically receive the member functions and member variables of the base class through inheritance, and then simply add the additional functions or member variables we want. This not only saves work, but also means that if we ever update or modify the base class (e.g. add new functions, or fix a bug), all of our derived classes will automatically inherit the changes!
For example, if we ever added a new function to Person, both Employee and Supervisor would automatically gain access to it. If we added a new variable to Employee, Supervisor would also gain access to it. This allows us to construct new classes in an easy, intuitive, and low-maintenance way!
Conclusion
Inheritance allows us to reuse classes by having other classes inherit their members.
17.3 — Order of construction of derived classes
When C++ constructs derived objects, it does so in phases. First, the most-base class (at the top of the inheritance tree) is constructed first. Then each child class is constructed in order, until the most-child class (at the bottom of the inheritance tree) is constructed last.
#include <iostream>
class Base
{
public:
int m_id {};
Base(int id=0)
: m_id { id }
{
std::cout << "Base\n";
}
int getId() const { return m_id; }
};
class Derived: public Base
{
public:
double m_cost {};
Derived(double cost=0.0)
: m_cost { cost }
{
std::cout << "Derived\n";
}
double getCost() const { return m_cost; }
};
int main()
{
std::cout << "Instantiating Base\n";
Base base;
std::cout << "Instantiating Derived\n";
Derived derived;
return 0;
}
This program produces the following result:
Instantiating Base
Base
Instantiating Derived
Base
Derived
As you can see, when we constructed Derived, the Base portion of Derived got constructed first. This makes sense: logically, a child can not exist without a parent. It’s also the safe way to do things: the child class often uses variables and functions from the parent, but the parent class knows nothing about the child. Instantiating the parent class first ensures those variables are already initialized by the time the derived class is created and ready to use them.
Order of construction for inheritance chains
It is sometimes the case that classes are derived from other classes, which are themselves derived from other classes. For example:
#include <iostream>
class A
{
public:
A()
{
std::cout << "A\n";
}
};
class B: public A
{
public:
B()
{
std::cout << "B\n";
}
};
class C: public B
{
public:
C()
{
std::cout << "C\n";
}
};
class D: public C
{
public:
D()
{
std::cout << "D\n";
}
};
Remember that C++ always constructs the “first” or “most base” class first. It then walks through the inheritance tree in order and constructs each successive derived class.
Here’s a short program that illustrates the order of creation all along the inheritance chain.
int main()
{
std::cout << "Constructing A: \n";
A a;
std::cout << "Constructing B: \n";
B b;
std::cout << "Constructing C: \n";
C c;
std::cout << "Constructing D: \n";
D d;
}
This code prints the following:
Constructing A:
A
Constructing B:
A
B
Constructing C:
A
B
C
Constructing D:
A
B
C
D
Conclusion
C++ constructs derived classes in phases, starting with the most-base class (at the top of the inheritance tree) and finishing with the most-child class (at the bottom of the inheritance tree). As each class is constructed, the appropriate constructor from that class is called to initialize that part of the class.
You will note that our example classes in this section have all used base class default constructors (for simplicity). In the next lesson, we will take a closer look at the role of constructors in the process of constructing derived classes (including how to explicitly choose which base class constructor you want your derived class to use).
17.4 — Constructors and initialization of derived classes
Initializing base class members
Fortunately, C++ gives us the ability to explicitly choose which Base class constructor will be called! To do this, simply add a call to the Base class constructor in the member initializer list of the derived class:
class Derived: public Base
{
public:
double m_cost {};
Derived(double cost=0.0, int id=0)
: Base{ id } // Call Base(int) constructor with value id!
, m_cost{ cost }
{
}
double getCost() const { return m_cost; }
};
Now, when we execute this code:
#include <iostream>
int main()
{
Derived derived{ 1.3, 5 }; // use Derived(double, int) constructor
std::cout << "Id: " << derived.getId() << '\n';
std::cout << "Cost: " << derived.getCost() << '\n';
return 0;
}
The base class constructor Base(int) will be used to initialize m_id to 5, and the derived class constructor will be used to initialize m_cost to 1.3!
Thus, the program will print:
Id: 5
Cost: 1.3
Note that it doesn’t matter where in the Derived constructor member initializer list the Base constructor is called – it will always execute first.
Now we can make our members private
#include <iostream>
class Base
{
private: // our member is now private
int m_id {};
public:
Base(int id=0)
: m_id{ id }
{
}
int getId() const { return m_id; }
};
class Derived: public Base
{
private: // our member is now private
double m_cost;
public:
Derived(double cost=0.0, int id=0)
: Base{ id } // Call Base(int) constructor with value id!
, m_cost{ cost }
{
}
double getCost() const { return m_cost; }
};
int main()
{
Derived derived{ 1.3, 5 }; // use Derived(double, int) constructor
std::cout << "Id: " << derived.getId() << '\n';
std::cout << "Cost: " << derived.getCost() << '\n';
return 0;
}
In the above code, we made m_id and m_cost private. This is fine, since we use the relevant constructors to initialize them, and use a public accessor to get the values.
This prints, as expected:
Id: 5
Cost: 1.3
We’ll talk more about access specifiers in the next lesson.
Another example
Inheritance chains
#include <iostream>
class A
{
public:
A(int a)
{
std::cout << "A: " << a << '\n';
}
};
class B: public A
{
public:
B(int a, double b)
: A{ a }
{
std::cout << "B: " << b << '\n';
}
};
class C: public B
{
public:
C(int a, double b, char c)
: B{ a, b }
{
std::cout << "C: " << c << '\n';
}
};
int main()
{
C c{ 5, 4.3, 'R' };
return 0;
}
In this example, class C is derived from class B, which is derived from class A. So what happens when we instantiate an object of class C?
First, main() calls C(int, double, char). The C constructor calls B(int, double). The B constructor calls A(int). Because A does not inherit from anybody, this is the first class we’ll construct. A is constructed, prints the value 5, and returns control to B. B is constructed, prints the value 4.3, and returns control to C. C is constructed, prints the value ‘R’, and returns control to main(). And we’re done!Thus, this program prints:
A: 5
B: 4.3
C: R
It is worth mentioning that constructors can only call constructors from their immediate parent/base class. Consequently, the C constructor could not call or pass parameters to the A constructor directly. The C constructor can only call the B constructor (which has the responsibility of calling the A constructor).
Destructors
When a derived class is destroyed, each destructor is called in the reverse order of construction. In the above example, when c is destroyed, the C destructor is called first, then the B destructor, then the A destructor.
Summary
When constructing a derived class, the derived class constructor is responsible for determining which base class constructor is called. If no base class constructor is specified, the default base class constructor will be used. In that case, if no default base class constructor can be found (or created by default), the compiler will display an error. The classes are then constructed in order from most base to most derived.
At this point, you now understand enough about C++ inheritance to create your own inherited classes!
17.5 — Inheritance and access specifiers
The protected access specifier
C++ has a third access specifier that we have yet to talk about because it’s only useful in an inheritance context. The protected access specifier allows the class the member belongs to, friends, and derived classes to access the member.
class Base
{
public:
int m_public {}; // can be accessed by anybody
protected:
int m_protected {}; // can be accessed by Base members, friends, and derived classes
private:
int m_private {}; // can only be accessed by Base members and friends (but not derived classes)
};
class Derived: public Base
{
public:
Derived()
{
m_public = 1; // allowed: can access public base members from derived class
m_protected = 2; // allowed: can access protected base members from derived class
m_private = 3; // not allowed: can not access private base members from derived class
}
};
int main()
{
Base base;
base.m_public = 1; // allowed: can access public members from outside class
base.m_protected = 2; // not allowed: can not access protected members from outside class
base.m_private = 3; // not allowed: can not access private members from outside class
return 0;
}
In the above example, you can see that the protected base member m_protected is directly accessible by the derived class, but not by the public.
So when should I use the protected access specifier?
Best practice
Favor private members over protected members.
Different kinds of inheritance, and their impact on access
// Inherit from Base publicly
class Pub: public Base
{
};
// Inherit from Base protectedly
class Pro: protected Base
{
};
// Inherit from Base privately
class Pri: private Base
{
};
class Def: Base // Defaults to private inheritance
{
};
If you do not choose an inheritance type, C++ defaults to private inheritance (just like members default to private access if you do not specify otherwise).
Public inheritance
class Base
{
public:
int m_public {};
protected:
int m_protected {};
private:
int m_private {};
};
class Pub: public Base // note: public inheritance
{
// Public inheritance means:
// Public inherited members stay public (so m_public is treated as public)
// Protected inherited members stay protected (so m_protected is treated as protected)
// Private inherited members stay inaccessible (so m_private is inaccessible)
public:
Pub()
{
m_public = 1; // okay: m_public was inherited as public
m_protected = 2; // okay: m_protected was inherited as protected
m_private = 3; // not okay: m_private is inaccessible from derived class
}
};
int main()
{
// Outside access uses the access specifiers of the class being accessed.
Base base;
base.m_public = 1; // okay: m_public is public in Base
base.m_protected = 2; // not okay: m_protected is protected in Base
base.m_private = 3; // not okay: m_private is private in Base
Pub pub;
pub.m_public = 1; // okay: m_public is public in Pub
pub.m_protected = 2; // not okay: m_protected is protected in Pub
pub.m_private = 3; // not okay: m_private is inaccessible in Pub
return 0;
}
Best practice
Use public inheritance unless you have a specific reason to do otherwise.
Protected inheritance
Protected inheritance is the least common method of inheritance. It is almost never used, except in very particular cases. With protected inheritance, the public and protected members become protected, and private members stay inaccessible.
Private inheritance
With private inheritance, all members from the base class are inherited as private. This means private members are inaccessible, and protected and public members become private.
Note that this does not affect the way that the derived class accesses members inherited from its parent! It only affects the code trying to access those members through the derived class.
class Base
{
public:
int m_public {};
protected:
int m_protected {};
private:
int m_private {};
};
class Pri: private Base // note: private inheritance
{
// Private inheritance means:
// Public inherited members become private (so m_public is treated as private)
// Protected inherited members become private (so m_protected is treated as private)
// Private inherited members stay inaccessible (so m_private is inaccessible)
public:
Pri()
{
m_public = 1; // okay: m_public is now private in Pri
m_protected = 2; // okay: m_protected is now private in Pri
m_private = 3; // not okay: derived classes can't access private members in the base class
}
};
int main()
{
// Outside access uses the access specifiers of the class being accessed.
// In this case, the access specifiers of base.
Base base;
base.m_public = 1; // okay: m_public is public in Base
base.m_protected = 2; // not okay: m_protected is protected in Base
base.m_private = 3; // not okay: m_private is private in Base
Pri pri;
pri.m_public = 1; // not okay: m_public is now private in Pri
pri.m_protected = 2; // not okay: m_protected is now private in Pri
pri.m_private = 3; // not okay: m_private is inaccessible in Pri
return 0;
}
In practice, private inheritance is rarely used.
A final example
Summary
The way that the access specifiers, inheritance types, and derived classes interact causes a lot of confusion. To try and clarify things as much as possible:
First, a class (and friends) can always access its own non-inherited members. The access specifiers only affect whether outsiders and derived classes can access those members.
Second, when derived classes inherit members, those members may change access specifiers in the derived class. This does not affect the derived classes’ own (non-inherited) members (which have their own access specifiers). It only affects whether outsiders and classes derived from the derived class can access those inherited members.
As a final note, although in the examples above, we’ve only shown examples using member variables, these access rules hold true for all members (e.g. member functions and types declared inside the class).
17.6 — Adding new functionality to a derived class
Adding new functionality to a derived class
class Derived: public Base
{
public:
Derived(int value)
: Base { value }
{
}
int getValue() const { return m_value; }
};
17.7 — Calling inherited functions and overriding behavior
By default, derived classes inherit all of the behaviors defined in a base class. In this lesson, we’ll examine in more detail how member functions are selected, as well as how we can leverage this to change behaviors in a derived class.
Calling a base class function
Redefining behaviors
Note that when you redefine a function in the derived class, the derived function does not inherit the access specifier of the function with the same name in the base class. It uses whatever access specifier it is defined under in the derived class. Therefore, a function that is defined as private in the base class can be redefined as public in the derived class, or vice-versa!
#include <iostream>
class Base
{
private:
void print() const
{
std::cout << "Base";
}
};
class Derived : public Base
{
public:
void print() const
{
std::cout << "Derived ";
}
};
int main()
{
Derived derived;
derived.print(); // calls derived::print(), which is public
return 0;
}
Adding to existing functionality
To have a derived function call a base function of the same name, simply do a normal function call, but prefix the function with the scope qualifier (the name of the base class and two colons). The following example redefines Derived::identify() so it first calls Base::identify() and then does its own additional stuff.
#include <iostream>
class Derived: public Base
{
public:
Derived(int value)
: Base { value }
{
}
int getValue() const { return m_value; }
void identify() const
{
Base::identify(); // call Base::identify() first
std::cout << "I am a Derived\n"; // then identify ourselves
}
};
Now consider the following example:
int main()
{
Base base { 5 };
base.identify();
Derived derived { 7 };
derived.identify();
return 0;
}
I am a Base
I am a Base
I am a Derived
When derived.identify() is executed, it resolves to Derived::identify(). However, the first thing Derived::identify() does is call Base::identify(), which prints “I am a Base”. When Base::identify() returns, Derived::identify() continues executing and prints “I am a Derived”.
This should be pretty straightforward. Why do we need to use the scope resolution operator (:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)