본문 바로가기

Programming/C++ Basic

[Basic C++] 14 – Data Structure in C++ : Class Constructor and Destructor

In this post, I am going to talk about constructors and deconstructors as the second part of C++ class series.

 


Table of contents

1. What is a constructor

2. What is a destructor

3. Class copy and copy constructor


Now we know what a C++ class is. A class is a data type to define an abstract idea existing in the real world like a car or a person, as C++ does not have such data types incorporated. Out first post for a class was about its syntax and how to make a simple class and objects out of it.

 

Figure 1 Class and objects

 

Figure 1 shows three car objects are mode as the car class. Each one of them has different values for the same variables declared in the class. In the early post, I showed you how to assign a value to a class variable after an object was declared using "dot operator" like,

 

class car
{
public:
	int horsepower;
} sample_car;

sample_car.horsepower = 170;

 

However, assigning each value every time you make an object might be tiring if you have too many class variables. In order to solve this problem, you define a constructor to initialize values by default or multiple assign values within a single line.


1. What is a constructor

A constructor is defined inside a class to initialize class members of the class as a non-static member function of the class. Class members can be variables, functions, or other objects. The initialization of class members means that you assign initial values to class variables or call functions when an object is created. There can be multiple constructors in the class in various ways depending on given conditions. This process is referred to as "Constructor Overloading".

 

1.1) Default constructor and Member initializer

Even if any constructors are not defined in the class, the C++ compiler defines a default constructor for you. However, if you default a user-defined constructor, which is called "overloaded constructor", the compiler does not define the default constructor. Thus you have to define the default one to call an object without initial values.

 

Code example 1

#include <iostream>
#include <string>

class car
{
public:
    int horsepower;
    std::string name;
    // Default Constructor
    car() {}

	// Memeber initialiez list 1
    car(int hp):
        horsepower(hp)
    {}

    // Memeber initialiez list 2
    car(int hp, std::string name):
        horsepower(hp), name(name)
    {}

    // Print class memebers
    void print_memebers()
    {
        std::cout <<"Hoursepower = "<< horsepower << std::endl;
        std::cout <<"Name = "<< name << std::endl;
    }
};

int main()
{
    // Object by the defult constructor
    car sample_car1;
    sample_car1.print_memebers();

    // Object by the constructor 1
    car sample_car2(185);
    sample_car2.print_memebers();

    // Object by the constructor 2
    car sample_car3(185, "Prius");
    sample_car3.print_memebers();
}

 

Figure 1 Result of code example 1

 

Figure 1 shows the result of code example 1. Within code example 1, three objects are created from the car class with three different constructors; a default constructor, constructor 1, and constructor 2. The syntax of a constructor is as followed,

 

class_name (parameters) :member_initializer_list(optional) {}

 

The name of a constructor must be the name of the current class. The default constructor does not contain any parameters nor member initializer lists, thus the class members return garbage values when called.

 

The member initializer lists are comma-separated lists followed by the colon character (:) at front. The syntax of member-initializers are as followed,

 

(1) class-or-identifier (expression-list (optional))

(2) class-or-identifier brace-init-list

(3) parameter-pack

 

Code example 1 used the first syntax to create constructors with member-initializers. You have to learn the C++ template and list-initialization first to understand the second and third syntax. Using overloaded constructors is a very important practice for you to prevent your program from wrongly accepting garbage values within your code.

 

2. What is a destructor

A constructor's job is to initialize an object when you create one with no initial values or certain values. The former is called the default constructor and it is automatically defined by the compiler. The latter is an overloaded constructor which is user-defined.

 

A destructor, as the name literally sounds, deinitializes an object at the end of the object's lifetime which is usually at the end of its scope. A stack-allocated object of a class is automatically deleted by the default destructor once it has reached its scope; however, a heap-allocated object has to be deleted manually to prevent a memory leak. Look code example 2.

 

Destructor Syntax

\\ Tilde operator - Name () {};
~class_name() {};

Code example 2

#include <iostream>
#include <string>

class car
{
private:
	int smart_system;
	int cost;
public:
	std::string obj_name;
	std::string maker_info;

	// Defult Constructor
	car() {}

	// Memeber lintializer Constructor
	car(std::string obj, int ss, int ct, std::string mk_info) :
		obj_name(obj), smart_system(ss), cost(ct), maker_info(mk_info)
	{
		std::cout <<obj_name<< " Initialized-----" << std::endl;
	}
    
    // Destructor
	~car() 
	{
		std::cout <<obj_name<< " Destroyed-----" << std::endl;;
	}

	void print_memebers()
	{
		std::cout << "Smart system no = " << smart_system << std::endl;
		std::cout << "Cost to manufactor = " << maker_info << std::endl;
		std::cout << "Name of the maker = " << cost <<std::endl;
		std::cout << std::endl;
	}
};

int main()
{
	car *car1 = new car("car1", 1, 16000, "Toyota");
	delete car1;
}

Figure 2 Result of code example 2

Figure 2 shows that the car1 object was created by "new" keyword on the heap and was later deleted by "delete" keyword, thus the memory was reclaimed manually. Be aware of this new-delete chain when dynamic allocation is used, otherwise, your program ends up underdoing significant memory leaks in the end.


3. Class copy and Copy constructors

Constructors that take objects of the same type as the argument to initialize an object using another object of the same class are copy constructors. When you copy a simple class object to another object, the compiler defines the default copy constructor, thus you do not need to define another one. However, using the default constructor to copy complex objects related to dynamic allocation, you have to define your own copy constructor to prevent your program from crashing.

 

The syntax of a copy constructor is as below,

Class_name (const class_name &other_object);

Here are a few examples of copy constructors. The first shows a shallow copy of an object from another object of the same class. The second, on the contrary, shows a deep copy of an object using pointers - dynamic allocation - in order to prevent errors.

 

Code example 2 - Shallow copy on stack

#include <iostream>
#include <string>

class car
{
private:
	int smart_system;
	int cost;
public:
	std::string maker;

	// Defult Constructor
	car() {}

	// Memeber lintializer Constructor
	car(int ss, int ct, std::string mk):
		smart_system(ss), cost(ct), maker(mk)
	{}

	void print_memebers()
	{
		std::cout << "Smart system no = " << smart_system<< std::endl;
		std::cout << "Cost to manufactor = " << maker<< std::endl;
		std::cout << "Name of the maker = " << cost <<std::endl;
		std::cout << std::endl;
	}
};

int main()
{
	car car1(1, 25000, "Toyota");
	std::cout << &car1 << std::endl;
	car1.print_memebers();
	car car2(5, 35000, "Toyota");
	std::cout << &car2 << std::endl;
	car2.print_memebers();
	car car3 = car1;
	std::cout << &car3 << std::endl;
	car3.print_memebers();
}

Figure 2 Result of code example 2

 

Three objects are declared from "car" class; car1, car2, and car3. The objects car 1 and car 2 are declared from the member initializer while the car 3 object is declared using the default constructor. Then car 2 is copied to car 3, so car 3 has the same class member values as car 2. Judging by the memory addresses of the objects, they are allocated next to each other on the stack. In addition, the car3 object was created on the different stack memory locations, but only the class members were copied.

 

On the other hand, you can create a user-defined copy constructor to copy objects. A r-object must be passed as  aconst reference.

 

Code example 4 - Custom copy constructor

#include <iostream>
#include <string>

class car
{
private:
	int smart_system=0;
	int cost=0;
public:
	std::string obj_name = "Default";
	std::string maker_info = "Defult";

	// Defult Constructor
	car() 
	{ 
		std::cout << obj_name << ": Default constructor is called.\n"; 
	}

	// Memeber lintializer Constructor
	car(std::string obj, int ss, int ct, std::string mk_info) :
		obj_name(obj), smart_system(ss), cost(ct), maker_info(mk_info)
	{
		std::cout << obj_name << ": Overloaded Constructor is called.\n" ;
	}

	// Copy consturctor
	car(const car &other)
		: maker_info(other.maker_info),
		smart_system(other.smart_system), cost(other.cost)
	{
		std::cout <<other.obj_name << " is coppied to " << obj_name << " Copy constructor is called.\n";
		obj_name = other.obj_name;
	}

	~car()
	{
		std::cout << obj_name << ": Deconstructor is called\n";
	}

	void print_memebers()
	{
		std::cout << "Object name = " << obj_name << std::endl;
		std::cout << "Smart system no = " << smart_system << std::endl;
		std::cout << "Cost to manufactor = " << maker_info << std::endl;
		std::cout << "Name of the maker = " << cost << std::endl;
		std::cout << std::endl;
	}
};

void create_object()
{
	car car1;
	car car2("Prius", 1, 35000, "Toyota");
	car car3("Dodge", 3, 63000, "Ford");
	car car4(car3);
}

int main()
{
	create_object();
}

Figure 3 Result of code example 4

Code example 4 shows how a stack-allocated class with a default constructor, overloaded constructor, custom copy constructor, and destructor work. Car1 object was initialed by the default constructor, Car2, and Car3 objects were initialized by the overloaded constructor, and Car4 objected was initialised by the copy constructor with Car3 object class members.

 

The object declarations were placed within a function so that you could see what would happen to the objects at the end of their scope. The four objects were called at the beginning of the function call, and they were all terminated from stack memory by the destructor. 

 

Next, let's see how heap-allocated objects work with copy operation. 

 

Code example 5 - Shallow copy on heap

#include <iostream>
#include <string>

class car
{
private:
	int smart_system;
	int cost;
public:
	std::string obj_name;
	std::string maker_info;

	// Defult Constructor
	car() {}

	// Memeber lintializer Constructor
	car(std::string obj, int ss, int ct, std::string mk_info) :
		obj_name(obj), smart_system(ss), cost(ct), maker_info(mk_info)
	{
		std::cout <<obj_name<< " Initialized-----" << std::endl;
	}
    
       // Destructor
	~car() 
	{
		std::cout <<obj_name<< " Destroyed-----" << std::endl;;
	}

	void print_memebers()
	{
		std::cout << "Smart system no = " << smart_system << std::endl;
		std::cout << "Cost to manufactor = " << maker_info << std::endl;
		std::cout << "Name of the maker = " << cost <<std::endl;
		std::cout << std::endl;
	}
};

int main()
{
	car* car1;
	car1 = new car("car1", 1, 25000, "Toyota");
	std::cout << "Maker of car1 = " << car1->maker_info << std::endl;
	std::cout <<"car1 heap address: "<< car1 << std::endl;
	std::cout <<"car1 pointer stack address: "<< &car1 << std::endl;
	car* car2;
	car2 = new car("car2", 2, 36000, "Ford");
	std::cout << "Maker of car2 = " << car2->maker_info << std::endl;
	std::cout << "car2 heap address: " << car2 << std::endl;
	std::cout << "car2 pointer stack address: " << &car2 << std::endl;
	std::cout << "\n";
	// Copy car 2 to car 1
	car1 = car2;
	std::cout << "car2 is copied to car 1.\n";
	std::cout <<"car1 heap address: "<< car1 << std::endl;
	std::cout <<"car1 poiner stack address: "<< &car1 << std::endl;
	std::cout <<"car2 heap address: "<< car2 << std::endl;
	std::cout <<"car2 pointer stack address: "<< &car2 << std::endl;
	std::cout << "\n";
	std::cout << "Maker of car1 = " << car1->maker_info << std::endl;
	std::cout << "Maker of car2 = " << car2->maker_info << std::endl;

	//delete car1;
	//delete car2;
}

Figure 4 Result of code example 5

Figure 4 shows the result of code example 5 (shallow copy on heap). Two objects, car 1 and car 2, were created on heap. The values of car1 and car2 are the heap addresses of the objects and &car1 and &car2 are the stack address of the pointers.

 

Within the car class, the default constructor, member initializer list, and destructor were declared. Every time an object was created, the constructors and destructors printed messages. One member function was declared to check the class members.

 

The delete commands were disabled in the example temporary; however, what would occur if they were turned on?. The purpose of "delete" keyword is to reclaim heap memory. In this case, at the time that car2 was copied to car1, which means car1 had the same value of car2, two pointers, car1 and car2, pointed the same address, car2. Thus the car1 object was left abandoned (We call it garbage.). (Figure 5)

 

Figure 5 Pointer copy operation

 

In other words, the car2 object had two pointers located on the stack storing the same memory address of the object. Therefore, the delete operations for car1 and car2 ended up reclaiming the same address twice, and it led to a runtime error. (Figure 6)

 

Figure 6 Deleting dynamic variable twice

 

What really happened here was that the copy operation actually copied "car2 pointer" to "car1 pointer", not the object2 to object1. Therefore, the copy operation must be like below syntax,

// Dereferenced object pointer means the object itself.
*car1 = *car2;

Figure 7 Result of object copy operation
Figure 9 Heap copy of objects

The object2 was copied to the object1, not the object pointer (Figure 8), and their addresses were still the same after the copy operation. Thus now we can delete both "car1" and "car2  to reclaim the memory. 

 

You can also use a user-defined copy constructor to copy objects. Code example 6 shows the defult constructor, member initializer constructor, copy constructor, and destructor. Two objects of "car" class were created on the heap, but the second object "car2" was the copy of "car1" object. 

 

 

Code example 6

#include <iostream>
#include <string>

class car
{
private:
	int smart_system = 0;
	int cost = 0;
public:
	std::string obj_name = "Default";
	std::string maker_info = "Defult";

	// Defult Constructor
	car()
	{
		std::cout << obj_name << ": Default constructor is called.\n";
	}

	// Memeber lintializer Constructor
	car(std::string obj, int ss, int ct, std::string mk_info) :
		obj_name(obj), smart_system(ss), cost(ct), maker_info(mk_info)
	{
		std::cout << obj_name << ": Overloaded Constructor is called.\n";
	}

	// Copy Consturctor
	car(const car& other)
		: maker_info(other.maker_info),
		smart_system(other.smart_system), cost(other.cost)
	{
		std::cout << other.obj_name << " is coppied to " << obj_name << " Copy constructor is called.\n";
		obj_name = other.obj_name;
	}
	
	// Destructor
	~car()
	{
		std::cout << obj_name << ": Deconstructor is called\n";
	}

	void print_memebers()
	{
		std::cout << "Object name = " << obj_name << std::endl;
		std::cout << "Smart system no = " << smart_system << std::endl;
		std::cout << "Cost to manufactor = " << maker_info << std::endl;
		std::cout << "Name of the maker = " << cost << std::endl;
		std::cout << std::endl;
	}
};

int main()
{
	car *car1 = new car("Prius", 35000, 1, "Toyota");
	car1->print_memebers();
	car* car2 = new car(*car1);
	car2->print_memebers();
}

 

 


REFERENCES

[1] https://en.cppreference.com/w/

[2] https://www.youtube.com/channel/UCQ-W1KE9EYfdxhL6S4twUNw