Blog
Initialization order
Dawid Trendota 20.08.2018
The order in which objects are initialized in C++ is a long and river-wide theme. It is worth getting acquainted with it more closely, because the constructor and destructor are indispensable tools in the workshop of any C++ programmer. It is also one of the most frequently asked recruitment questions for both junior and senior positions.
I divided the article into several parts. Because developers are reluctant to use articles that do not contain source code, each fragment will be based on an example.
Initialize class instances
Let’s start with a simple example before we reach for more complex structures.
<iostream>#include using namespace std; Chocolate class { public: Chocolate() { < "Chocolate ctor" < endl; } ~Chocolate() { cout < "Chocolate dtor" cout < endl; } }; int main(int argc, char *argv) { Ch[]ocolate chocolate; return 0; } </iostream>
Of course, the chocolate object will be first created and then destroyed. Like all variables with automatic storage duration, the time of his life is determined by the buckles around him. The outcome of the programme is unlikely to come as a surprise.
Chocolate ctor Chocolate dtor
No one expected us to go first. What are the relationships between a class and its components?
Initialization of class components
We will use the Chocolate class from the previous example. This time it will be part of the ChocolateCake class.
<iostream>#include using namespace std; Chocolate class { public: Chocolate() { < "Chocolate ctor" < endl; } ~Chocolate() { cout < "Chocolate dtor" < endl; } }; class ChocolateCake { public: ChocolateCake() { cout cout < "ChocolateCake ctor" < endl; } ~ChocolateCake() { cout < "ChocolateCake dtor" < endl; } private: Chocolate chocolate; }; int main(int argc, char *argv) { Cho[]colateCake cake; return 0; } </iostream>
This case is already a little more complex. This time, we create an object that contains a different one. We expect to create both. And if you create it and destroy it. Just in what order? Let’s approach the topic intuitively, do we have access to its components in the object builder?
Of course they do.
So they had to be initialised earlier. What about destruction? Do we have access to class fields in the destructor? We have too. So they need to be destroyed later. The outcome of the programme should therefore not come as a surprise.
Chocolate ctor ChocolateCake ctor ChocolateCake dtor Chocolate dtor
It is worth remembering here a certain rule of thumb. Destruction is reversed than construction. Another topic that we will discuss will be the relationship of initialization in the event of inheritance.
Initialize base classes
Let us deprive our class of its constituents in order to simplify the next example.
<iostream>#include using namespace std; ChocolateCake class { public: ChocolateCake() { < "ChocolateCake ctor" < endl; } ~ChocolateCake() { cout < "ChocolateCake dtor" < endl; } }; class BirthdayCake : public ChocolateCake { public: BirthdayCake() { cout cout < "BirthdayCake ctor" < endl; } ~BirthdayCake() { cout < "BirthdayCake dtor" < endl; } }; int main(int argc, char *argv) { Bir[]thdayCake birthdayCake; return 0; } </iostream>
Our new BirthdayCake class inherits from ChocolateCake. Of course, both constructors must be called. The order can be re-inferred. Do we have access to base class fields and methods in the BirthdayCake constructor?
Of course they do.
The constructor of this class had to be called before. The order of destruction can also be justified. Or invoke the rule regarding the order of construction/destruction. The result of the programme is as follows:
ChocolateCake ctor BirthdayCake ctor BirthdayCake dtor ChocolateCake dtor
A summary of the
Let’s combine the previous examples. Let’s create two classes, derived and base. Each of them will have a component. It’s a task you’d expect to see in a conversation.
<iostream>#include using namespace std; Chocolate class { public: Chocolate() { < "Chocolate ctor" < endl; } ~Chocolate() { cout < "Chocolate dtor" < endl; } }; class ChocolateCake { public: ChocolateCake() { cout cout < "ChocolateCake ctor" < endl; } ~ChocolateCake() { cout < "ChocolateCake dtor" < endl; } private: Chocolate chocolate; }; class Candles { public: Candles() { cout < "Candles ctor" < endl; } ~Candles() { cout < "Candles dtor" < endl; } }; class BirthdayCake : public ChocolateCake { public: BirthdayCake() { cout < "BirthdayCake ctor" < endl; } ~BirthdayCake() { cout < "BirthdayCake dtor" < endl; } private: Candles candles; }; int main(int argc, char *argv) { Bir[]thdayCake birthdayCake; return 0; } </iostream>
We will be asked about the result of the program, or more precisely about the order in which constructors and destructors are called. Combining the conclusions of previous examples, we will easily come to such an order:
Design:
- base class component constructors
- base class constructor
- class component constructors
- class constructor
Destruction:
- class destructor
- class destructors
- base class destructor
- component destructors of the base class
It is already a pure formality to indicate the outcome of the programme.
Chocolate ctor ChocolateCake ctor Candles ctor BirthdayCake ctor BirthdayCake dtor Candles dtor ChocolateCake dtor Chocolate dtor
Does this example exhaust the subject? Unfortunately not quite. There are a few open points left:
– global variables,
– static variables,
– static class fields,
– the order in which multiple component fields are initialised,
– variables in the thread’s local memory,
– multiple and virtual inheritance.
However, this is already the material for the next article.