University of Leeds

Constructors

Introduction

Consider the previous example. What would happen if we did not use the mutator method to set the radius before using the accessor to get the area?

1
2
3
4
5
6
7
8
9
#include <iostream>
#include "Circle.h"

int main() {
  Circle circle;
  float area = circle.get_area();
  std::cout << "The circle has an area of " << area << " m^2.\n";
  return 0;
}

Compiling the code using the Apple LLVM compiler, the following output would be displayed in the terminal.

The circle has an area of inf m^2.

Compiling the code using the GCC compiler, the following output would be displayed.

The circle has an area of 0 m^2.

inf is the C++ equivalent of infinity and usually occurs when an operation returns a floating-point number too large to store in a floating-point variable (float or double). So why has this occurred?

Variable initialisation

The root cause of the inf in this case is because we have not initialised the _radius variable in our Circle class. Since the variable is uninitialised, it will essentially have a garbage value. A variable simply represents a location in memory (RAM) and there is no guarantee that the memory location will be empty. A float is 4 bytes and so the variable will essentially have a random 32-bit binary value. When this is interpreted as a floating-point number, it may end up being huge. When we calculate the area, we square this huge value, causing it to become too big to store in a float and hence returning inf (or 0 depending on the compiler).

The list below shows some of the random values stored in the uninitialised radius member variable after the code (compiled using the Apple LLVM compiler) was executed several times.

-5.50993e+23

-6.83838e+21

-5.05653e+20

-3.00742e+27

When the code was compiled using GCC, the same random value was output each time, suggesting that the variable was put in the same location in memory each time.

Using a constructor

When creating a class, it is therefore a good idea to ensure that all member variables are initialised. One way to do this is by using a constructor. A constructor is a special class method that runs when an object of that class is created. We can therefore assign member variables with default values in the constructor. This ensures that when the object is then subsequently used, the variables have a value.

It is worth pointing out that this is not actually initialisation in the formal sense. Strictly speaking, initialisation takes place when a variable is assigned a value when it is declared i.e.

int a = 1;

In the case of our class, the member variables are declared in the class definition in the header file and are not actually initialised. It is only when the constructor runs that the variables are assigned a value i.e.

int a;   // declared in class definition
a = 1;   // assigned a value in the constructor

C++ class constructor

The example below demonstrates how to use a constructor in C++.

Circle.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#ifndef CIRCLE_H
#define CIRCLE_H

#define PI 3.14159265

class Circle {
 public:
  Circle();                       // constructor declaration
  void set_radius(float radius);  // mutator
  float get_area();               // accessor
 private:
  // member variables are private and set/get via accessor/mutator
  float _radius;
  float _area;
};

#endif

Circle.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include "Circle.h"

// class constructor definition
Circle::Circle() {
  _radius = 1.0;
}
// mutator method to set radius
void Circle::set_radius(float radius) { _radius = radius; }
// accessor method to get area
float Circle::get_area() {
  _area = PI * _radius * _radius;
  return _area;
}

The constructor is declared in the header file and the actual definition appears in the implementation (.cpp) file. Inside the constructor definition we then simply assign a 'default' value to radius member variable. This then assures that even if we forget to set the radius of the circle when we use the class, it will have a default value avoiding the potential issues brought about by uninitialised values.

Initialisation in class definition

Every few years, the C++ language is extended with new features and released as a new standard (e.g. C++98, C++11, C++14, C++17). Since C++11, it has been possible to initialise variables in the class definition. In the example below, we can assign a value to the radius member variable in the header file. The constructor in the .cpp file can then be empty of any initialisation commands.

Circle.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#ifndef CIRCLE_H
#define CIRCLE_H

#define PI 3.14159265

class Circle {
 public:
  Circle();                       // constructor declaration
  void set_radius(float radius);  // mutator
  float get_area();               // accessor
 private:
  // member variables are private and set/get via accessor/mutator
  float _radius = 1.0;
  float _area;
};

#endif

To do this, we must ensure we use C++11 (or newer). Some versions of compiler may default to C++11 (or newer), but to be safe we can explicitly tell the compiler which C++ standard to use with a compiler flag. For example, the following compiler flag will tell the compiler to use the C++11 standard. The year number can changed in the flag to select one of the other versions.

-std=c++11

List initialisation

Another new feature introduced in C++11 is the ability to initialise member variables using lists and this is deemed to be the best practice.

Circle.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include "Circle.h"

// constructor using uniform list initialisation
Circle::Circle() : _radius{1.0} {}
// mutator method to set radius
void Circle::set_radius(float radius) { _radius = radius; }
// accessor method to get area
float Circle::get_area() {
  _area = PI * _radius * _radius;
  return _area;
}

Here we initialise the radius member variable in the constructor implementation in the .cpp file. Note the use of the curly braces {}. The curly braces are used in uniform initialisation. You may sometimes see the same kind of syntax but with normal brackets (). This is an example of direct initialisation. The use of uniform initialisation is deemed best practice. More information can be found here.

Specifying initialisation values

Up to this point, we have been providing default values to the Circle class in the constructor. The user can then use the set_radius mutator method to set the radius. This involves the user having to type two commands, first create a Circle object and then use the mutator methods.

It is also possible to allow values to be passed into the constructor that enable users to create objects with specific values. An example is below.

Circle.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#ifndef CIRCLE_H
#define CIRCLE_H

#define PI 3.14159265

class Circle {
 public:
  Circle(float radius);           // constructor with value
  void set_radius(float radius);  // mutator
  float get_area();               // accessor
 private:
  // member variables are private and set/get via accessor/mutator
  float _radius;
  float _area;
};

#endif

Circle.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include "Circle.h"

// constructor using uniform list initialisation
Circle::Circle(float radius) : _radius{radius} {}
// mutator method to set radius
void Circle::set_radius(float radius) { _radius = radius; }
// accessor method to get area
float Circle::get_area() {
  _area = PI * _radius * _radius;
  return _area;
}

main.cpp

1
2
3
4
5
6
7
8
9
#include <iostream>
#include "Circle.h"

int main() {
  Circle circle(2.0);
  float area = circle.get_area();
  std::cout << "The circle has an area of " << area << " m^2.\n";
  return 0;
}

Here the Circle constructor now has an argument that allows to user to pass in a specific value for the radius. In main.cpp it is then possible to pass in the desired value of radius when creating the object, meaning it can be done in one line.

Summary

Be aware of the various ways to use constructors, especially the post-C++11 ways. This is especially important when you start pulling random code from the Internet without knowing what version of compiler it was written with! Do not be surprised when it does not work if you grab random code from different sources and then mangle it together. Even more so, do not expect anyone else to then clean up your mess!