C++: Constructors and Destructors

If you've done any experimentation with classes and structs, you may have noticed that you can't set defaults for member variables. Say we had a 'Car' struct like the one that follows:

1
2
3
4
struct Car {
	string name;
	int cost;
};

You've probably noticed that you cannot do something like the following to set a default "name" and "cost":

1
2
3
4
struct Car {
	string name = "Car-a-tron 1000";
	int cost = 2000;
};

Doing so will probably result in an error like:

'Car::name' : only static const integral data members can be initialized within a class
'Car::cost' : only static const integral data members can be initialized within a class

This is the compiler's way of letting you know that you just can't do this. As the above error reads, you cannot initialize variables within a class (with the exception of "static const integral data members"). So what do we do if we want to set defaults for every 'Car' object?

This is where constructors come in. A constructor is essentially just a special member function which gets called when an object is created, and we can use these to do whatever initialization for the object that we need to do - for example setting our member variables to some values! We can create constructors by using the class or struct name as a function name, so to give some defaults for our member variables for 'Car' objects, we could use the following:

1
2
3
4
5
6
7
8
9
struct Car {
	Car()
	{
		name = "Car-a-tron 1000";
		cost = 2000;
	}
	string name;
	int cost;
};

In the above example, the 'Car' constructor would be called whenever a new 'Car' object is created - hence setting the defaults for 'name' and 'cost'. This technique is extremely common in C++ classes and structs, and usually provides functionality necessary for the objects to function correctly - i.e. initialization and stuff.

Moving from this, you can overload constructors just like you can overload functions! This is done just like overloading functions (that's right, constructors can take parameters!), and then you can specify any number of parameters you want when creating the object by simply using some brackets. So we might want something like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Car {
	Car()
	{
		name = "Car-a-tron 1000";
		cost = 2000;
	}
	Car(string n)
	{
		name = n;
		cost = 2000;
	}
	Car(string n, int c)
	{
		name = n;
		cost = c;
	}
	string name;
	int cost;
};

Even better than the above, we could make use of default parameters:

1
2
3
4
5
6
7
8
9
struct Car {
	Car(string n = "Car-a-tron 1000", int c = 2000)
	{
		name = n;
		cost = c;
	}
	string name;
	int cost;
};

Either approach would work, but I feel that the default parameter approach is cleaner in this particular example. As alluded to earlier, we can then (optionally) provide parameters when we create 'Car' objects - take for example the following:

1
2
3
Car number_one;
Car number_two("Bugatti Veyron");
Car number_three("Ferrari 458", 9999);

The above snippet makes use of all the available constructors. Even if we aren't technically overloading the constructors in this example if we go for the default parameters approach, we've demonstrated the concept.

Another method of initializing member variables using a constructor, which is somewhat more preferred amongst those who know how to use them, is using constructor initialization lists. These consist of a colon after the constructor name, and then a comma-separated list of "function-like" statements which can be used to set member variables (and also to call base class constructors, but you probably don't know what this means yet, so don't sweat it). This method is often considered neater and cleaner, and still allows for further logic to be present in the "main body" of the constructor in the curly brackets if this is needed. Parameters or constant values can be specified inside brackets next to the name of the member variable which you want to set, and as such we could re-create our "Car" constructor (the version which used the default parameters) using this cleaner constructor initialization method with something like the following:

1
2
3
4
5
struct Car {
	Car(string n = "Car-a-tron 1000", int c = 2000) : name(n), cost(c) {} //name = n, cost = c
	string name;
	int cost;
};

There also exists a thing called a destructor - this cannot be overloaded, and does not take any parameters. It is created by making a function with the name of the class/struct prefixed with a tilde ('~' sign), and is called whenever the object is destroyed. This isn't a whole lot of use to us right now since we haven't really talked too much about destroying objects, but it's something which usually comes in the same kind of field as 'constructors', and as such I feel as if I should cover it in this tutorial. Just know that they exist, and in the case of our 'Car' struct, may look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Car {
	Car(string n = "Car-a-tron 1000", int c = 2000)
	{
		name = n;
		cost = c;
	}
	~Car() //Destructor
	{
		//Clean up in here, as if this is called the object is being destroyed
	}
	string name;
	int cost;
};