C++ Inheritance and Constructors: A Beginner’s Guide from a Theoretical Perspective

Inheritance

1. What is the concept?

Inheritance is essentially an extension of an existing class’s characteristics, adding new functionalities. The new class created is called a derived class, while the class being inherited from is called the base class. In fact, inheritance is primarily a form of reuse at the class hierarchy level.

For example, if we create a class for animals with certain attributes, specific animals may have unique attributes. For instance, a panda loves to sleep in winter. In this case, we can create a class for pandas that inherits from the animal class, reusing its attributes while also adding its own unique attributes.

There are three types of class inheritance (which can be ignored):

• Public inheritance: The public members of the base class remain public in the derived class, the protected members remain protected, and the private members are not visible in the derived class.

• Protected inheritance: The public members of the base class become protected in the derived class, the protected members remain protected, and the private members are not visible in the derived class.

• Private inheritance: The public members of the base class become private in the derived class, the protected members become private, and the private members are not visible in the derived class.

2. What is the execution order of constructors and destructors?

1. The constructor of the virtual base class (if there are multiple virtual base classes, they are executed in the order of inheritance);

2. The constructor of the base class (if there are multiple ordinary base classes, they are executed in the order of inheritance);

3. The constructors of member objects of the class (in the order of initialization);

4. The constructor of the derived class itself.

Why is the constructor executed in this order?

This order can be summarized as:Parent first, child later. In simple terms, an object must ensure that all its components are fully constructed before it can construct itself.

A derived class object consists of several parts:Virtual base class part, Non-virtual base class part, Member object part, and Derived class specific part.

The design principle of C++ is:A class must ensure that all its parent classes and members are correctly constructed and initialized before it constructs itself. This is like building a house; you must lay a solid foundation (base class) before you can build the main structure (derived class).

Detailed explanation:

1. Constructor of the virtual base class

This is executed first. In multiple inheritance, especially when there is a “diamond inheritance” problem, the virtual base class is shared by multiple derived classes. To avoid the virtual base class being constructed multiple times, the C++ standard stipulates thatthe virtual base class is only constructed once by the bottom-most derived class. To achieve this, the construction of the virtual base class must be completed before all other base classes to ensure it is only processed once throughout the inheritance chain.

2. Constructor of the base class

After the virtual base class constructor is completed, the constructors of all non-virtual base classes are executed next. If there are multiple ordinary base classes (multiple inheritance), their construction orderstrictly follows the order declared in the derived class’s inheritance list.

For example:<span><span>class Dog : public Animal, public LivingThing</span></span>, <span><span>Animal</span></span>‘s constructor will be called before <span><span>LivingThing</span></span>‘s constructor. This is because the derived class object first needs to establish its base class sub-objects, and this order is explicitly specified by the programmer in the code.

3. Constructor of member objects

When a class contains objects of other classes as member variables, these member objects must also be constructed before the host object is constructed. The compiler will call these member objects’ constructors before executing the body of the derived class’s own constructor. Their construction orderstrictly follows the order they are declared in the class.

For example:

class Engine {};class Car {    Engine engine_;  // Declared first    string brand_;  // Declared later};

4. The derived class’s own constructor

This is the last step in the entire construction process. Only after all base classes and member objects have been fully constructed will the body of the derived class’s own constructor be executed. Here, you can complete the initialization work specific to the derived class, such as assigning values to its member variables or executing other logic.

Summary:

The C++ compiler enforces the principle of “constructing from the most basic parts to the upper levels”.

3. Why is using member initializer lists faster?

Concept of initializer lists

In a class’s constructor, instead of assigning values to member variables within the function body, you use a colon and an initializer list before the constructor’s curly braces.

The above defines the concept of initializer lists. In simple terms, it is a special method of assigning values to member variables in a constructor, occurring before the function body <span><span>{}</span></span>.

Let’s illustrate this with a specific example:

Example:<span><span>Rectangle</span></span> class

We define a <span><span>Rectangle</span></span> class with two member variables:<span><span>length</span></span> (length) and <span><span>width</span></span> (width).

Method 1: Assigning values in the constructor body (not recommended)

#include <iostream>
class Rectangle {public:    int length;    int width;
    // Assigning values in the constructor body    Rectangle(int l, int w) {        length = l;        width = w;        std::cout << "Assignment in constructor body" << std::endl;    }};
int main() {    Rectangle rect(10, 5);    std::cout << "Length: " << rect.length << ", Width: " << rect.width << std::endl;    return 0;}

This code runs correctly, but it actually performstwo steps:

  1. First, the <span><span>length</span></span> and <span><span>width</span></span> member variables are created and assigned default values (usually random garbage values).

  2. Then, within the constructor body <span><span>{}</span></span>, the assignment operator <span><span>=</span></span> assigns the new values (10 and 5) to them.

Method 2: Using initializer lists (recommended)

Now, let’s do the same thing using initializer lists.

#include <iostream>
class Rectangle {public:    int length;    int width;
    // Using initializer lists    Rectangle(int l, int w) :       length(l),       width(w)       {        std::cout << "Initialized using initializer lists" << std::endl;      }};
int main() {    Rectangle rect(10, 5);    std::cout << "Length: " << rect.length << ", Width: " << rect.width << std::endl;    return 0;}

In this version, the constructor is followed by a colon <span><span>:</span></span>, and after the colon is the initializer list.

  • <span><span>length(l)</span></span> means initializing the member variable <span><span>length</span></span> with the parameter <span><span>l</span></span>.

  • <span><span>width(w)</span></span> means initializing the member variable <span><span>width</span></span> with the parameter <span><span>w</span></span>.

This time, the entire process becomes one step: when the <span><span>Rectangle</span></span> object is created, its member variables <span><span>length</span></span> and <span><span>width</span></span> are directly assigned 10 and 5 without first having default values.

4. Which initialization operations must use initializer lists?

  1. When initializing a reference member variable;

  2. When initializing a const member variable;

  3. When calling a base class constructor that has a set of parameters;

  4. When calling a member class constructor that has a set of parameters;

The compiler will sequentially process the initializer list to insert initialization operations within the constructor, and before any user code is executed. The order of items in the list is determined by the order of member declarations in the class, not by the arrangement in the initializer list.

1. Why must reference and <span><span>const</span></span> members be in the initializer list?

The core reason behind both cases is:They must have a definite, unchangeable value upon creation.

We can think of the object creation process as a two-step process:

  1. Allocate memory and create member variables: In this step, the object’s member variables are created, but they have not yet been assigned values (or have been assigned default values).

  2. Execute the code within the constructor body: In this step, we can operate on the already existing member variables within the constructor’s curly braces <span><span>{}</span></span>, such as assigning values.

Now, let’s see what happens with reference and <span><span>const</span></span> members in these two steps:

  • Reference member variables: A reference is like an alias, and it must be immediately “bound” to a specific variable at the moment of definition. It has no “free” state. If it is not bound through the initializer list during the first step (creation), then trying to assign it in the second step (within the constructor body) actually modifies the variable to which the reference is bound, rather than assigning a value to the reference itself. Since a reference cannot be changed once bound, the compiler will prohibit this operation.

    Analogy: Imagine you give a friend a nickname “Xiao Ming”. This nickname must be bound to his real name “Wang Da Chui” at the moment you first introduce him. You cannot create the nickname “Xiao Ming” first and then say “Xiao Ming” equals “Wang Da Chui”; that would not make sense. The initializer list is like thisinitial introduction step.

  • <span><span>const</span></span> member variables: <span><span>const</span></span> member variables are constants, and their values cannot be modified after creation. If they are not assigned values through the initializer list during the first step (creation), they will be in an uninitialized state. Then, trying to assign a value in the second step (within the constructor body) essentially attempts to modify the value of a constant after it has been created. This violates the definition of <span><span>const</span></span><span><span>, so the compiler will throw an error.</span></span>

    Analogy: You set a rule that the password for your house door is eternally unchangeable. This password must be set when you build the house. You cannot first build the door and leave it without a password, then go back and modify it, because its definition is “eternally unchangeable”. The initializer list is like thissetting the password while building the door step.

2. Why must derived classes call the base class constructor in the initializer list?

This is due to the construction order of “base class first, derived class later”. When a derived class object is created, the base class part must be fully created and initialized before the derived class’s own members can be created.

If the base class constructor requires parameters, then the derived class mustpass these parameters to the base class before executing its own constructor. The only place that can achieve this “before executing itself” is the initializer list.

  • Why not in the constructor body?If it were allowed to call the base class constructor within the derived class’s constructor body, it would create a paradox: you must first enter the derived class’s constructor before you can call the base class’s constructor. But this contradicts C++’s rule—the base class must be constructed first.

    Analogy: You want to build a three-story villa. The building code states: you must lay the foundation (base class) first before you can build the first and second floors (derived classes). If you want to go back and lay the foundation while building the first floor, that is impossible. The initializer list is likehanding the construction workers the list of materials needed for the foundation before building the first floor. It ensures that the foundation is completed before the first floor.

3. Why must member objects call their parameterized constructors in the initializer list?

This reason is very similar to why derived classes call base class constructors; they both represent a “composition” rather than an “inheritance” relationship.

When a class contains objects of another class as members, this member object will be created before the host object’s (the object containing it) constructor executes.

  • Why not in the constructor body?If you try to initialize a member object within the constructor body, it means that this member objecthas already been created (using the default constructor), and you are just trying to reassign it. However, many classes (like <span><span>Engine</span></span>) do not have a default constructor or do not have an assignment operation that accepts parameters. Therefore, you must tell it which parameterized constructor to use at the moment it is created. This task can only be accomplished by the initializer list.

    Analogy: You are assembling a toy car. This car needs an engine. When you are assembling the body (the host class <span><span>Car</span></span>), you need to prepare the engine (member object <span><span>Engine</span></span>). If the engine you need is parameterized (for example, requiring a specified horsepower), you must have it ready before putting the engine into the car body. The initializer list is like you preparing the engine before assembling the car body.

In summary, the role of the initializer list is to provide the necessary parameters at the time the object and its members are created, ensuring they can be correctly and simultaneously initialized. The code within the constructor body is used to perform additional operations on them after all members have been created and initialized.

Summary:

The C++ compiler enforces the principle of “constructing from the most basic parts to the upper levels”.

Leave a Comment