Classes and Features

Here we look at some of the other ways in C++ classes can be defined and used

Download this file for both scenarios from this repo

How classes are constructed

#pragma once #include <string> using namespace std; class Engine { public: Engine(int capacity, int power ) :itsCapacity(capacity), itsPower( power) { std::cout << "Engine::Engine(int capacity, int power)" << std::endl; } private: int itsCapacity; int itsPower; }; class Car { public: Car(string regNo) :itsRegNo(regNo) { std::cout << "Car::Car(string regNo)" << std::endl; } private: string itsRegNo; Engine itEngine; };

Place your breakpoints on lines 10 and 23. When you try to build this code

The git project has tags 1.1 through to 1.x, git pull each tag to see how the code has changed

.cpp file

// class-and-delegation.cpp : This file contains the 'main' function. Program execution begins and ends there. // #include <iostream> #include <vector> #include "Car.h" int main() { Car cc{ "Volvo S60" }; }

Once you’ve cloned the code, checkout versions of the software from v1.1 to v1.11. I have tried to push the code in a systematic manner. Where I have appended a letter after the version it’s usually because I forgot to do something in the code for what I am trying to explain.

Version

Description of what you should see

Version

Description of what you should see

1.2

Your starting point. Put breakpoints at lines 10 and 23 of Car.h. Try and build then execute the code.

It fails because the constructor initialisation list has not been set up correctly. Move on to v1.2.

1.3

The code in v1.2 will not build. It fails because when an aggregate object is being initialised, each contained object’s constructors must be called. If no constructor for the contained object is specified, then the an attempt is made to call the default constructor.

The Car contains an Engine object, so when initialising the Car the compiler looks for the default constructor of the Engine because none has been specified in the construction of the Car.

Notice the changes we have made, a new default constructor in Engine, and look at the constructor initialisation list in Car.

Execute the code and observe the order in which the constructors are called, and then executed. You should notice that the calling order is Car(…) –> Engine(…), but the method execution order is Engine(…){ … } → Car(…){ .. }.

Constructors are executed in reverse order when working with aggregate objects. All contained objects of an aggregate object are executed before the aggregate itself.

1.4

We’ve changed Car’s constructor initialisation list. Car now is fully and correctly initialised because it calls the correct constructor on the Engine.

1.5

We’ve improved the code in the constructor initialisation list so that the hard coded values are replaced with parameters.

1.6

We’ve introduced a class called SportsCar. It inherits from Car. The code in will not build.

It fails because when you call a constructor on a child class and do not specify the base class constructor using the constructor initialisation list, the compiler calls the default constructor of the base class. Notice the changes we have made, a new default constructor in Car, and look at the constructor initialisation list in SportsCar.

Execute the code and observe the order in which the constructors are called, and then executed. You should notice that the calling order is SportsCar(…) –> Car(…), but the method execution order is Car(…){ … } → SportsCar(…){ .. }.

Constructors are executed in reverse order. All parent classes of a class that is built up through an inheritance hierarchy must be executed before the actual leaf class can be executed.

In the C++ you do not initialise the object data members in the constructor method body, this is too late. The constructor body is called once the object has been initialised. Initialisation of data members should be done on the constructor initialisation list. Also note that the this pointer of the object is not valid whilst the constructor initialisation is being executed.

Examine the SportsCar object, notice that the Car section of the object has not been initialised correctly.

1.7

We’ve improved the code to call the base constructor. Notice that the call is done in the constructor initialisation list as with aggregate components.

1.8

In main() we have introduced the following line of code

Car& cobj = sc;

You should see that you are getting an error on this line of code. Now look at this line of code in the header file “Car.h”

class SportsCar : Car

Unlike other OO languages, C++ supports three types of inheritance - public, protected and private (default). The line of code above creates a private inheritance relationship between the SportsCar and Car. The SportsCar has inherited everything from Car but Car as a type is not visible to the outside world for any objects created as SportsCar. In other words you cannot cast SportsCar to a Car.

1.9

We’ve corrected the issue in 1.8. The inheritance relationship has now been made public

class SportsCar : public Car

The SportsCar can now be treated as a Car. It now follows the rule of “everywhere you can use the parent you cal use the child”

1.10

We’ve introduced to new methods move() into Car and SportsCar. There is nothing special about the definitions.

In main we have added the following lines of code.

Car cval = sc; cval.move(); Car& cobj = sc; cobj.move(); Car *cptr = &sc; cptr->move();

Execute the code and note which methods get called.

You will be surprised to see that Car::move() is called for all the variables. The code does not seem to work in a polymorphic manner. Why?

Car cval = sc; cval.move()

This does not work because of object slicing. The C++ compiler cuts the type information of any descendants of Car, the SportsCar becomes a Car and loses all type information of SportsCar

 

Car& cobj = sc;

cobj.move();

Car *cptr = &sc;

cptr->move();

In these two examples, the reference and pointer types link to the Car type but the object has not been sliced, it remains as a SportsCar. C++ type system is by default non polymorphic, so when T::move() called it always results in the T::move() of of the current reference and pointer type

We need to enable polymorphic behaviour in the types.

1.11

Notice the change in the header file.

Adding virtual to a method on a base type (does nto have to be the top of the tree will result in that becoming polymorphic. The base type now has a virtaul table.

Using a template function to determine types