Free Hosting

Constructors and initialization of derived classes


In the past two lessons,we’ve looked at some basics about inheritance in C++ and explored the order that derived classes are initialized. In this lesson, we’ll take a closer look at the role of constructors in the initialization of derived classes. To do so, we will continue to use the simple Base and Derived class we developed in the previous lesson:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base
{
public:
    int m_nValue;
 
    Base(int nValue=0)
        : m_nValue(nValue)
    {
    }
};
 
class Derived: public Base
{
public:
    double m_dValue;
 
    Derived(double dValue=0.0)
        : m_dValue(dValue)
    {
    }
};
With non-derived classes, constructors only have to worry about their own members. For example, consider Base. We can create a Base object like this:
1
2
3
4
5
6
int main()
{
    Base cBase(5); // use Base(int) constructor
 
    return 0;
}
Here’s what actually happens when cBase is instantiated:
  1. Memory for cBase is set aside
  2. The appropriate Base constructor is called
  3. The initialization list initializes variables
  4. The body of the constructor executes
  5. Control is returned to the caller
This is pretty straightforward. With derived classes, things are slightly more complex:
1
2
3
4
5
6
int main()
{
    Derived cDerived(1.3); // use Derived(double) constructor
 
    return 0;
}
Here’s what actually happens when cDerived is instantiated:
  1. Memory for cDerived is set aside (enough for both the Base and Derived portions).
  2. The appropriate Derived constructor is called
  3. The Base object is constructed first using the appropriate Base constructor
  4. The initialization list initializes variables
  5. The body of the constructor executes
  6. Control is returned to the caller
The only real difference between this case and the non-inherited case is that before the Derived constructor can do anything substantial, the Base constructor is called first. The Base constructor sets up the Base portion of the object, control is returned to the Derived constructor, and the Derived constructor is allowed to finish up it’s job.
Initializing base class members
One of the current shortcomings of our Derived class as written is that there is no way to initialize m_nValue when we create a Derived object. What if we want to set both m_dValue (from the Derived potion of the object) and m_nValue (from the Base portion of the object) when we create a Derived object?
New programmers often attempt to solve this problem as follows:
1
2
3
4
5
6
7
8
9
10
11
class Derived: public Base
{
public:
    double m_dValue;
 
    Derived(double dValue=0.0, int nValue=0)
        // does not work
        : m_dValue(dValue), m_nValue(nValue)
    {
    }
};
This is a good attempt, and is almost the right idea. We definitely need to add another parameter to our constructor, otherwise C++ will have no way of knowing what value we want to initialize m_nValue to.
However, C++ prevents classes from initializing inherited member variables in the initialization list of a constructor. In other words, the value of a variable can only be set in an initialization list of a constructor belonging to the same class as the variable.
Why does C++ do this? The answer has to do with const and reference variables. Consider what would happen if m_nValue were const. Because const variables must be initialized with a value at the time of creation, the base class constructor must set it’s value when the variable is created. However, when the base class constructor finishes, the derived class constructors initialization lists are then executed. Each derived class would then have the opportunity to initialize that variable, potentially changing it’s value! By restricting the initialization of variables to the constructor of the class those variables belong to, C++ ensures that all variables are initialized only once.
The end result is that the above example does not work because m_nValue was inherited from Base, and only non-inherited variables can be changed in the initialization list.
However, inherited variables can still have their values changed in the body of the constructor using an assignment. Consequently, new programmers often also try this:
1
2
3
4
5
6
7
8
9
10
11
class Derived: public Base
{
public:
    double m_dValue;
 
    Derived(double dValue=0.0, int nValue=0)
        : m_dValue(dValue)
    {
        m_nValue = nValue;
    }
};
While this actually works in this case, it wouldn’t work if m_nValue were a const or a reference (because const values and references have to be initialized in the initialization list of the constructor). It’s also inefficient because m_nValue gets assigned a value twice: once in the initialization list of the Base class constructor, and then again in the body of the Derived class constructor.
So how do we properly initialize m_nValue when creating a Derived class object?
In all of the examples so far, when we instantiate a Derived class object, the Base class portion has been created using the default Base constructor. Why does it always use the default Base constructor? Because we never told it to do otherwise!
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 initialization list of the derived class:
1
2
3
4
5
6
7
8
9
10
11
class Derived: public Base
{
public:
    double m_dValue;
 
    Derived(double dValue=0.0, int nValue=0)
        : Base(nValue), // Call Base(int) constructor with value nValue!
            m_dValue(dValue)
    {
    }
};
Now, when we execute this code:
1
2
3
4
5
6
int main()
{
    Derived cDerived(1.3, 5); // use Derived(double) constructor
 
    return 0;
}
The base class constructor Base(int) will be used to initialize m_nValue to 5, and the derived class constructor will be used to initialize m_dValue to 1.3!
In more detail, here’s what happens:
  1. Memory for cDerived is allocated.
  2. The Derived(double, int) constructor is called, where dValue = 1.3, and nValue = 5
  3. The compiler looks to see if we’ve asked for a particular Base class constructor. We have! So it calls Base(int) with nValue = 5.
  4. The base class constructor initialization list sets m_nValue to 5
  5. The base class constructor body executes
  6. The base class constructor returns
  7. The derived class constuctor initialization list sets m_dValue to 1.3
  8. The derived class constructor body executes
  9. The derived class constructor returns
This may seem somewhat complex, but it’s actually very simple. All that’s happening is that the Derived constructor is calling a specific Base constructor to initialize the Base portion of the object. Because m_nValue lives in the Base portion of the object, the Base constructor is the only constructor that can initialize it’s value.
Another example
Let’s take a look at another pair of class we’ve previously worked with:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <string>
class Person
{
public:
    std::string m_strName;
    int m_nAge;
    bool m_bIsMale;
 
    std::string GetName() { return m_strName; }
    int GetAge() { return m_nAge; }
    bool IsMale() { return m_bIsMale; }
 
    Person(std::string strName = "", int nAge = 0, bool bIsMale = false)
        : m_strName(strName), m_nAge(nAge), m_bIsMale(bIsMale)
    {
    }
};
 
// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
    double m_dBattingAverage;
    int m_nHomeRuns;
 
    BaseballPlayer(double dBattingAverage = 0.0, int nHomeRuns = 0)
       : m_dBattingAverage(dBattingAverage), m_nHomeRuns(nHomeRuns)
    {
    }
};
As we’d previously written it, BaseballPlayer only initializes it’s own members and does not specify a Person constructor to use. The means every BaseballPlayer we create is going to use the default Person constructor, which will initialize the name to blank and age to 0. Because it makes sense to give our BaseballPlayer a name and age when we create them, we should modify this constructor to add those parameters.
Here’s our new BaseballPlayer class with a constructor that calls the Person constructor to initialize the inherited Person member variables.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
    double m_dBattingAverage;
    int m_nHomeRuns;
 
    BaseballPlayer(std::string strName = "", int nAge = 0, bool bIsMale = false,
        double dBattingAverage = 0.0, int nHomeRuns = 0)
        : Person(strName, nAge, bIsMale), // call Person(std::string, int, bool) to initialize these fields
            m_dBattingAverage(dBattingAverage), m_nHomeRuns(nHomeRuns)
    {
    }
};
Now we can create baseball players like this:
1
2
3
4
5
6
int main()
{
    BaseballPlayer cPlayer("Pedro Cerrano", 32, true, 0.342, 42);
 
    return 0;
}
To prove that it works:
1
2
3
4
5
6
7
8
9
10
11
int main()
{
    BaseballPlayer cPlayer("Pedro Cerrano", 32, true, 0.342, 42);
 
    using namespace std;
    cout << cPlayer.m_strName << endl;
    cout << cPlayer.m_nAge << endl;
    cout << cPlayer.m_nHomeRuns;
 
    return 0;
}
This outputs:
Pedro Cerrano
32
42
As you can see, the name and age in the base class were properly initialized, as was the number of home runs in the derived class.
Inheritance chains
Classes in an inheritance chain work in exactly the same way.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
using namespace std;
 
class A
{
public:
    A(int nValue)
    {
        cout << "A: " << nValue << endl;
    }
};
 
class B: public A
{
public:
    B(int nValue, double dValue)
    : A(nValue)
    {
        cout << "B: " << dValue << endl;
    }
};
 
class C: public B
{
public:
    C(int nValue, double dValue, char chValue)
    : B(nValue, dValue)
    {
        cout << "C: " << chValue << endl;
    }
};
 
int main()
{
    C cClass(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 is not inherited, 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 cClass is destroyed, the C destructor is called first, then the B destructor, then the A destructor.
Summary
Although it is true that the most base class is initialized first, this actually only happens after each constructor has called the parent constructor in turn. This gives us the opportunity to specify which of the parent’s constructors we want to use to initialize inherited members. Once the base constructor has finished constructing the base portion of the class, control returns to the derived constructor and it executes as normal.
One of the primary advantages of using a base class constructor to initialize the base class members is that if the base class constructor is ever changed, both the base class and all inherited classes will automatically use the changes! This helps keep maintenance and duplicate code down.

0 comments:

Blogger Template by Clairvo