Constructors:
1) Default constructors: a constructor that creates and object without parameters.
C++ provides us with default constructors for all the classes. However there might be times where we need to customize it for our own needs.
E.g. 1. Basic example
class B {
public:
B() { cout << "B" ; }
~B() {cout << "~B" ; }
};
class A: public B {
public:
A() { cout << "A" ; }
~A() { cout << "~A" ; }
};
int main() {
A a;
}
The program prints:
B, A, ~A, ~B
Because class A inherited B, its constructor is implicitly called when A is constructed. Then they're destroyed in reverse order when the variable goes out of scope.
E.g. 2. More complex example with inheritance
class F {
public:
F() { cout << "F" << endl; }
~F() { cout << "~F" << endl; }
};
class G : public F {
public:
G() { cout << "G" << endl; }
~G() { cout << "~G" << endl; }
};
class H : public F {
public:
H() { cout << "H" << endl; }
~H() { cout << "~H" << endl; }
};
class I : public G, public H {
public:
I() { cout << "I" << endl; }
~I() { cout << "~I" << endl; }
};
int main() {
I test;
}
The program prints:
F, G, F, H, I, ~I, ~H, ~F, ~G, ~F // The F class constructor is called twice because both G and H are derived classes
2) Constructors with parameters:
It's possible to overload the constructor with the same name but different parameters to use in different object-creation situations.
class Employee {
private:
string name;
int initial_salary;
Time clockIn;
public:
Employee() {
name = "staff";
salaray = 3000;
}
Employee(string employee_name, double initial_salary)
{
name = employee_name;
salary = initial_salary;
}
Employee(string employee_name, double initial_salary, int arrive_hour)
{
name = employee_name;
salary = initial_salary;
clockIn = Time(arrive_hour, 0, 0);
}
};
We can define even more versions of constructor of the Employee object with different parameters.
Note there is an inefficiency in initializing inside the constructor body, especially if the member variables are complex objects themselves. For example the Time member "clockIn" will be initialized twice, once with the default Time() constructor and once more with the parameterized constructor called explicitly. It's more efficient to use initializer lists to call the parameterized version right away.
Employee(string employee_name, double initial_salary, int arrive_hour)
:clockIn(arrive_hour, 0, 0)
{
name = employee_name;
salary = initial_salary;
}
Static member and methods:
- Static variables in a class is like a global class member that is visible to all instances of the class
- No matter how many objects of the class are created, there is only one copy of the static member.
- The static variable is independent of any instance of the object. It exists even if no objects of the class are instantiated
- A static member function can only access static data member, other static member functions and any other functions from outside the class.
- Static member functions have a class scope and they do not have access to the this pointer of the class.
- Static variables and methods are evoked with the scope resolution operator ::
E.g.
class A {
public:
static int x;
static void printX() { cout << x << endl; }
};
int A::x; // to rid of linker error we need to define an integer like this outside somewhere.
int main() {
int foo = A::x;
A::printX();
}
Initializer List:
Always use initializer lists when you can, they optimize the initialization process and make the code look cleaner.
E.g. Basic Example
Class C {
private:
int m_int;
public:
C(int x) : m_int(x) {}
};
E.g. Example with inheritance
class C {
protected:
int m_int;
public:
C(int x) : m_int(x) { cout << "C" << endl; }
};
class D : public C {
public:
D(int x) : C(x) { cout << "D" << endl; }
};
E.g. Difference between using and not using an initializer list
class F {
public:
F() { cout << "F" << endl; }
~F() { cout << "F" << endl; }
};
class G : public F {
public:
G(): F() { cout << "G" << endl; }
~G() { cout << "~G" << endl; }
};
int main() { G g; }
This prints (this is actually the same if we didn't explicitly call F() in the initializer list, but imagine if we wanted to pass some stuff) :
F
G
~G
~F
All is well. Now what if we didn't initialize F with initializer list in G but put it in the function body?
G() { F(); cout << "G" << x << endl; }
This gets us:
F
F // extra work is done here
~F // extra work is done here
G
~G
~F
So takeaway is to always use initializer lists when you can. But it's probably good to know the nitty-gritty details so we can use it with more confidence. Let's read on.
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.
E.g. The following will not compile
class D : public C {
public:
D(int x) : m_int(x) { cout << "D" << endl; } // will not compile because m_int is not a member of D
};
Initialization order:
From cppreference:
1) If the constructor is for the most-derived class, virtual bases are initialized in the order in which they appear in depth-first left-to-right traversal of the base class declarations (left-to-right refers to the appearance in base-specifier lists)
2) Then, direct bases are initialized in left-to-right order as they appear in this class's base-specifier list
3) Then, non-static data member are initialized in order of declaration in the class definition.
4) Finally, the body of the constructor is executed
So the order of the initializer list doesn't affect the order of initialization. The order of initialization is largely determined by their order of declaration in the class definition. Another way to think of it is - if initialization order was controlled by the appearance in the member initializer lists of different constructors, then the destructor wouldn't be able to ensure that the order of destruction is the reverse of the order of construction.
E.g. Here, even though the initializer lists is in the order of m_z, m_y, m_x, the actual order is m_x, m_y, m_z because of the order of declaration in the class definition.
class J {
private:
int m_x;
int m_y;
int m_z;
public:
J(int x, int y, int z) : m_z(z), m_y(y), m_x(x) {}
};
Knowing this, it would be a bad idea to do something like this:
class J {
private:
int m_x;
int m_y;
int m_z;
public:
J(int x, int y, int z) : m_y(y), m_z(z), m_x(m_y + m_z) {}
};
And as expected, printing the values gives us the following because m_x was initialized with garbage values.
x : 11021326, y: 2, z: 3
The solution in this case is to use the local values: (extra parentheses are needed)
J(int x, int y, int z) : m_y(y), m_z(z), m_x((y + z)) {}
Output:
x : 5, y: 2, z: 3
As initialization becomes more complex, we can even insert try-catch clauses in the initialization:
class T() {
T(double a) {}
// function-try block begins before the function body
T() try
: T(1.0) {
// function body
}
catch (...) {
// exception occurred on initialization
}
};
E.g. More complex example with inheritance