Constructors, destructors, getters, and setters are fundamental object-oriented programming (OOP) components. Constructors enable proper initialization of objects, while destructors handle cleanup operations. Getters and setters facilitate controlled access to class member variables. This lesson provides a comprehensive overview of these elements, emphasizing their significance in creating robust and maintainable code. We’ll explore different types of constructors, including default, parameterized, and those with default parameters or initialization lists. Additionally, we’ll discuss the role of destructors in resource management. Furthermore, we’ll explore the importance of getters and setters for encapsulation and data manipulation.

Constructors and destructors

First, let’s review why we need constructors and what their benefits are.

Constructor

A constructor is a member function with the same name as the class. It is automatically called when an object of a class is made. The main concept behind the constructor is to initialize the object data members and establish their initial state. Constructors provide flexibility to create objects differently, accepting different sets of parameters depending on the constructor type. There are many types of constructors, including:

  • Default constructors

  • Parameterized constructors

  • Default constructors with default parameters

  • Member initialization lists

Before we go into detail for each type, let’s first see why they’re so important. The code below is the Timer class implementation without any constructors. Let’s run the code.

Press + to interact
#include <iostream>
using namespace std;
class Timer
{
private:
// Member attributes
int sec, min, hour;
public:
// Displays time
void displayTimer()
{
cout << "Time: " << hour << " hours, " << min << " minutes, "
<< sec << " seconds" << endl;
}
};
int main()
{
Timer timer1; // Create a timer object with default constructor
timer1.displayTimer();
return 0;
}
  • Lines 12–16: A displayTimer() helper function is defined that prints the value of the timer.

  • Line 21: A timer1 object of the Timer class is defined.

  • Line 22: The values of the timer1 object are printed.

The code gives us a garbage value for each member, meaning that they are not yet initialized. To avoid that, we prefer that the values of the members be initialized when an object is created. To achieve that, we introduce the concept of constructors.

Default constructor

  • Definition: Default constructors are used to create objects without any arguments.

  • Syntax: ClassName() {}

  • Example code:

class Timer {
private:
int sec, min, hour;
public:
Timer()
{
sec = 0;
min = 0;
hour = 0;
}
};
int main()
{
Timer T; // this will call automatically Timer()
return 0;
}
Default constructor example

Parameterized constructor

  • Definition: Parameterized constructors accept arguments to initialize object attributes.

  • Syntax: ClassName(type1 arg1, type2 arg2, ...) {}

  • Example code:

class Timer {
private:
int sec, min, hour;
public:
Timer(int h, int m, int s)
{
hour = h;
min = m;
sec = s;
}
};
int main()
{
Timer T(1,2,3); // Call Timer(1,2,3): where 1 will be received in h, 2 in m and 3 in s
return 0;
}
Parameterized constructor example

Constructors with default parameters

  • Definition: Constructors with default parameter values allow objects to be created with fewer arguments.

  • Syntax: ClassName(type1 arg1 = defaultValue1, type2 arg2 = defaultValue2, ...) {}

  • Example code

class Timer
{
private:
int sec, min, hour;
public:
Timer(int h = 0, int m = 0, int s = 0)
{
hour = h;
min = m;
sec = s;
}
};
int main()
{
Timer T1; // Will automatically call Timer(0,0,0)
Timer T2(1); // Will automatically call Timer(1,0,0)
Timer T3(1,2); // Will automatically call Timer(1,2,0)
Timer T4(1,2,3); // Will automatically call Timer(1,2,3)
return 0;
}
Constructor with default parameters

4. Constructors with initialization lists

  • Definition: Constructors with initialization lists offer a convenient way to directly initialize class member variables. This approach is particularly recommended when there is a constant attribute within the class that should never be modified after initialization. By utilizing member initialization lists, values can be assigned to object components as they are declared and initialized simultaneously. This ensures efficient and effective initialization of class attributes.

  • Syntax: ClassName(type1 arg1, type2 arg2, ...) : memberVariable1(arg1), memberVariable2(arg2), ... {}

  • Example code:

class Timer {
private:
int sec, min, hour;
const string name; // the name of the object should not be changed
public:
Timer(int h, int m, int s, string _n) : hour(h), min(m), sec(s), name(_n)
{
// Constructor code
}
};
int main()
{
Timer t(1,2,3,"t"); // will in the backend initialize (hour=1, min = 2, sec = 3, and some_constant = 4)
}
Constructor with initialization lists

Running example of all the constructors

Here are the examples of all four types of constructors and how to use them in one example.

#include <iostream>
using namespace std;
class Timer
{
private:
// Member attributes
int sec, min, hour;
public:
// Default constructor
Timer()
{
hour = 0;
min = 0;
sec = 0;
}
// Displays time
void displayTimer()
{
cout << "Time: " << hour << " hours, " << min << " minutes, "
<< sec << " seconds" << endl;
}
};
int main()
{
Timer T; // This will call default constructor
T.displayTimer();
return 0;
}
Default constructor

Tip: Execute the above four files separately and see their impact.

Destructors

  • Definition: Destructors are special member functions that are automatically called when an object is destroyed.

  • Syntax: ~ClassName() {}

  • Example code:

Press + to interact
#include<string>
#include<iostream>
using namespace std;
class Timer
{
private:
// Member attributes
int sec, min, hour;
string name;
public:
// Destructor
Timer(string n): name(n)
{
cout << name << " is created "<<endl;
}
~Timer()
{
cout << name << " is destroyed "<<endl;
}
};
Timer TGlobal("TGlobal");
int main()
{
Timer T1("T1");
{
Timer T2("T2");
{
Timer T3("T3");
static Timer TStatic("TStatic");
} // T3 will be destroyed but not T4, because it is a static object
} // T2 will be destroyed
Timer T5("T5");
return 0;
} // T5 will be destroyed first and then T1
// (within a scope the objects are destroyed in reverse order of creation)
// Then TStatic and TGlobal will be destroyed,
// as global/static variables are destroyed in the end
// again in reverse order

Tip: Execute the above code and see the order in which the objects are created and destroyed.

Explanation

By observing the object creation and destruction sequence upon execution, we can understand how objects behave within different scopes and how global and static objects are handled during the program’s lifetime.

  • The global object TGlobal is created and displayed as the TGlobal is created string. It resides in the global segment and will be destroyed at the end of the program.

  • Inside the main() function, the T1 local object is created and displayed as the T1 is created string.

  • Within the nested scopes, T2 is created, followed by T3. Both objects are displayed as the T2 is created and T3 is created strings, respectively.

  • The static object TStatic is created within the innermost scope. It persists beyond the scope due to its static nature and is displayed as the TStatic is created string.

  • As the program reaches the end of each scope, the objects are destroyed in the reverse order of their creation. First, T3 is destroyed, then T2. Since TStatic is a static object, it remains intact.

  • Moving back to the outer scopes, T2 is destroyed next, followed by T1. Within each scope, the objects are destroyed in the reverse order of their creation.

  • Finally, T5 is created and displayed as the T5 is created string. At the end of the program, T5 is destroyed, followed by T1. Finally, the static object TStatic and the global object TGlobal are destroyed because global and static variables are destroyed at the end of the program in reverse order.

Let’s visualize the code:

Press + to interact
canvasAnimation-image
1 of 19

Getters and setters

Getters and setters play a crucial role in object-oriented programming by providing controlled access to class member variables. Getters are methods that retrieve the values of these variables, while setters are methods that allow for their modification. By encapsulating the variables and exposing them through getters and setters, we can maintain data integrity and control how the data is accessed and modified.

Getters, also known as accessor methods, are used to retrieve the values of private member variables. By convention, getters are typically declared as constant member functions, indicated by the const keyword at the end of the function declaration. This ensures that the getter functions do not modify the state of the object they are called on.

Similarly, setters give the user the access to modify the attributes of the object. It is the responsibility of the programmer of the class that any illegal update should be avoided. For example, the Timer class has private member attributes, including sec (seconds), min (minutes), and hour (hours). To ensure valid resetting, we can include range checks in the setters. For instance, when setting the hour, we can verify that the value falls within the range of 0 to 23. Similarly, we can validate the minute and second values to ensure they are between 0 and 59.

Here’s an updated example code incorporating getters and setters with proper range checking for the Timer class:

Press + to interact
#include <iostream>
using namespace std;
class Timer {
private:
int sec, min, hour;
public:
// Constructor
Timer(int h = 0, int m = 0, int s = 0) {
setHour(h);
setMin(m);
setSec(s);
}
// Getters (const functions)
int getHour() const {
return hour;
}
int getMin() const {
return min;
}
int getSec() const {
return sec;
}
// Setters
void setHour(int h) {
if (h >= 0 && h <= 23) {
hour = h;
} else {
hour = 0;
cout << "Invalid hour value!" << endl;
}
}
void setMin(int m) {
if (m >= 0 && m <= 59) {
min = m;
} else {
min = 0;
cout << "Invalid minute value!" << endl;
}
}
void setSec(int s) {
if (s >= 0 && s <= 59) {
sec = s;
} else {
sec = 0;
cout << "Invalid second value!" << endl;
}
}
// Displays time
void displayTimer() const {
cout << "Time: " << hour << " hours, " << min << " minutes, "
<< sec << " seconds" << endl;
}
};
int main() {
Timer t(1, 30, 50);
t.displayTimer(); // Output: Time: 1 hours, 30 minutes, 50 seconds
// Get values using getters
cout << "Hour: " << t.getHour() << endl; // Output: Hour: 1
cout << "Minute: " << t.getMin() << endl; // Output: Minute: 30
cout << "Second: " << t.getSec() << endl; // Output: Second: 50
return 0;
}

In the above example, the Timer class includes getters (getHour(), getMin(), getSec()) and setters (setHour(), setMin(), setSec()) as its member variables. The getters are declared as const functions, ensuring they do not modify the state of the Timer object. They allow us to retrieve the values of the hour, min, and sec variables, respectively.

By using getters and setters, we encapsulate the internal state of the Timer object and provide controlled access to the data. The setters perform input validation to ensure the assigned values are within valid ranges, while the getters allow other parts of the program to retrieve the values without directly accessing or modifying the member variables. This promotes data encapsulation, code maintainability, and flexibility in working with the Timer objects.

Remember: Although the destructor may initially seem less crucial, its significance becomes evident in upcoming lessons when dealing with dynamically allocated data within a class. In such scenarios, the destructor plays a vital role as it becomes responsible for releasing heap memory, ensuring proper memory management. This is a critical aspect to consider when working with objects that allocate memory dynamically. As we progress to the next chapter on operator overloading, you'll witness the imperative need for a destructor in specific situations, further emphasizing its importance in C++ programming.