The Working Constructor
As you saw in the last lesson, working constructor is the short-hand description of a constructor that takes as many user-supplied arguments as possible. In the Time class the working constructor looks like this:
Time(int hours, int minutes);
In the .cpp file, you might have code that looks something like this, using the initializer list, also from the last lesson:
Time::Time(int hours, int minutes)
: m_hours(hours), m_minutes(minutes)
{
// validate the constructor arguments
assert(hours >= 0 && hours < 24);
assert(minutes >= 0 && minutes < 60);
}
It is important that your constructor intitializes your object so that it is in a valid state. In the example shown here, we've used assert() on the assumption that it is a programming error if an invalid Time is constructed. If, however, you were constructing Time objects using external data, it is possible you would want to throw and catch an exception instead.
Conversion Constructors
A conversion constructor is a constructor (usually 1-argument) that implicitly converts between one type and another. Here's an overloaded conversion constructor that converts between fractional hours and hours and minutes:
Time(double hours);
The implementation of the constructor (converting first to seconds, then extracting the hours and minutes) could look like this:
Time::Time(double hours)
{
assert(hours >= 0 && hours < 24);
const int kSecondsPerHour = 3600;
const int kHoursPerMinute = 60;
auto time = static_cast<int>(hours * kSecondsPerHour);
m_hours = time / kSecondsPerHour;
m_minutes = time % kSecondsPerHour / kHoursPerMinute;
}
Implicit vs. Explicit
Conversion constructors can be implicit (which is the default), or explicit. The implicit conversion constructor is called any time the compiler needs a Time object, but finds a double that it can convert. Consider this fragment of code:
Time bed_time(23, 30); // 11:30
bed_time = 5.2; // WHAAAAT?
You would expect that line 2 would be a syntax error, but, surprisingly, it is not. Instead, the conversion constructor is silently (implicitly) called, and bed_time is changed to 5:12 am. Probably not what you expected.
You can add explicit as a modifier to the prototype to prevent this:
explicit Time(double hours); // 7.51 -> 7:35
The keyword explicit only goes in the class definition. It is not repeated in the .cpp file. Sometimes, as you'll see when you cover symmetric overloaded operators in CS 250, you'll want to allow implicit conversion. For instance the string(const char*) constructor is not explicit. Most of the time, however, explicit is preferred.
Assign, Copy & Destroy
You may assign one object to another, just as you can assign one int variable to another, even though the data members are private. As with the built-in types, the result is a copy of the data in the original members.
With class types, this is carried out by the overloaded assignment operator.
Time& operator=(const Time& rhs);
The assignment operator is not used when you pass an object by value, or initialize a new object variable with another. Instead, the copy constructor is called:
Time(const Time& rhs);
When an object is destroyed, its destructor is called. If your class allocates dynamic memory in the constructor, for instance, you would free it in the destructor. The destructor looks like the default constructor preceded by the tilde:
~Time();
C++ automatically writes a synthesized assignment operator, a synthesized copy constructor, and a synthesized destructor which work for simple types such as those in this course. In CS 250 you'll learn how to write your own assignment operators, copy constructors, and destructors to create more dynamic types.
You can prevent pass by value or assignment by adding the following to the definitions.
Time& operator=(const Time& rhs) = delete;
Time(const Time& rhs) = delete;
Static Data Members
Suppose you are creating a space-wars type video game, and one of your player types is an XFighter class. How do you easily keep track of how many XFighter ships are currently available?
That's easy: with a static, or shared data member as so:
class XFighter
{
public:
XFighter(int vin) : m_VIN(vin) { numShips++;}
~XFighter { numShips--;}
private:
static int numShips;
int m_VIN;
};
The static data member numShips is private to the XFighter class, but there is only one copy of the member, not one for each ship, like the vehicle identification number (m_VIN). When an XFighter is constructed, the constructor increments the shared numShips, and, when a ship is destroyed, the destructor decrements it.
There is one wrinkle with this. In C++, you must initialize the static member as a global object in your .cpp file with something like this:
int XFighter::numShips = 0;
This cannot go in the header file.
Static Member Functions
How do you use the numShips variable, since it's private? That is, how do you find out how many ships exist? With a static member function, like this:
static int xFighters() { return numShips; }
A static member function can only access static data members, or call other static member functions. It cannot access regular data members or call regular member functions. If the function is defined outside of the header, then you do not repeat the keyword static in the definition:
int XFighter::xFighters() { return numShips; }
Place static member functions in the public section of your class and call them using the class name and the scope operator, not an instance and the dot operator:
cout << XFighter::xFighters() << endl;
static const Data Members
When you have a constant that only applies to the situation represented by your class you can add it as a static const data member.
class Bizarro
{
public:
static const int kAnswer{-42};
static constexpr double kE{3.14159};
static const double kPi;
};
In the Bizarro world, almost everything is backwards. The "answer to everything" is -42, not 42 as in our world. The mathematical constant e is 3.14159, and the constant PI is the square root of PI in our own world.
For integral types, you may initialize static const data members inside the class (as with kAnswer); no other initialization is required. Starting in C++11 you can also do this for other types, using the keyword constexpr, instead of const (as with kE), provided that the value can be calculated at compile time.
However, if a type needs to calculate a value at runtime, (as kPi does), you'll still need to provide a separate definition in the .cpp file.
const double Bizarro::kPi = sqrt(acos(-1.0));
The data members are static (there is only one copy stored), they are const, (they cannot be changed), and they are public (you can use them outside of the class.)
cout << Bizarro::kAnswer << endl;
cout << Bizarro::kPi << endl;
cout << Bizarro::kE << endl;