Lesson 2 - Class Inheritance

by Zoran Horvat

Subtyping

In previous lesson we have defined a structure called Shape to accommodate location and name of the geometric figure. But with that structure we cannot precisely describe any particular shape, like ellipse or rectangle. More fields are required to meet the needs. But these fields come in different packages when describing specific shapes. Ellipse requires two radius values, rectangle requires width and height. Some other shape would be defined with other values. To fully define each of the shapes means to step into its specific features, and those are ones not shared with other shapes.

Now we are standing at the point where definitions of desired structures start to disperse. One line of development would lead to a ellipse; another line to a rectangle, and so on. But still, ellipse and rectangle share their location and name definition as provided by the Shape structure. This problem is solved using the concept of subtyping. We will introduce other structures to extend the Shape structure and to accommodate more specific features of particular geometric shapes. But here comes the beautiful part, which is probably best described by the code. Namely, both Ellipse and Rectangle will now contain Shape structure as their internal part:

struct Ellipse
{
    struct Shape _base;
    float radiusX;
    float radiusY
};
struct Rectangle
{
    struct Shape _base;
    float width;
    float height;
};

The following picture shows memory layout of our new structures. Now it becomes clear why Shape structure had to be instantiated within each of the instances: now Ellipse and Rectangle both contain fields that are common to all shapes - location and name.

Structures layout

This process of extending one type to accommodate additional values is referred to as subtyping. It is said that Ellipse is a subtype of Shape. Rectangle is another subtype of Shape. Shape is called the supertype or base type of Ellipe and of Rectangle. For this reason, Shape-typed field in Ellipse and Rectangle structures was named _base (underscore was added to avoid mixing it up with base keyword, which is used in some object-oriented languages like C# to denote base type).

Constructors and Destructors in Subtypes

When Shape structure was designed we have coded two constructors and a destructor. These functions had special purpose to initialize and release instances. Now we can ask a question: How should constructors and destructors operate on subtypes? Subtypes rely on their supertypes and so should it be with their constructors and destructors. Supertype defines a subset of values contained in the derived type. Supertype's constructor operates only on those supertype-provided values in order to initialize them for use. Subtype's constructor requires base part of the record already initialized, so we will enforce base type's constructor to be called first. Conversely, releasing resources added by the subtype may require base part of the record still operational. Therefore, subtype's destructor will fully execute and only then base type's destructor will be called to finish the job.

We are now ready to define the Ellipse structure and accompanying functions.

/* Listing of ellipse.h */
#include "shape.h"

#ifndef ELLIPSE_H
#define ELLIPSE_H

struct Ellipse
{
    struct Shape _base;
    float radiusX;
    float radiusY;
};

void Ellipse_Constructor(struct Ellipse *_this);
void Ellipse_Constructor1(struct Ellipse *_this,
                         float locationX, float locationY, float radiusX, floatradiusY);
void Ellipse_Destructor(struct Ellipse *_this);

#endif
// Listing of ellipse.c
#include "ellipse.h"

void Ellipse_Constructor(struct Ellipse *_this)
{
    Shape_Constructor((struct Shape*)_this);
    _this->radiusX = 0.0F;
    _this->radiusY = 0.0F;
}

void Ellipse_Constructor1(struct Ellipse *_this, float locationX, float locationY, float radiusX, float radiusY)
{
    Shape_Constructor1((struct Shape*)_this, locationX, locationY);
    _this->radiusX = radiusX;
    _this->radiusY = radiusY;
}

void Ellipse_Destructor(struct Ellipse *_this)
{
    // Resources releasing code would be put here
    Shape_Destructor((struct Shape*)_this);
}

Observe implementation of the first constructor. It calls base type's constructor and simply passes this pointer to it. This may look awkward but recall from the Ellipse structure declaration that Shape structure was instantiated as the first element within the Ellipse structure. This means that pointer to Ellipse is the same as pointer to its _base member, which is actually a Shape structure instance. After ellipse's constructor has called base type's constructor, it simply initializes remainder of the Ellipse record, which boils down to setting the radiuses to neutral value.

Destructor is the simplest part of the ellipse type and it is so because Ellipse does not add any dynamically allocated memory that should be released by the destructor. Therefore, ellipse's destructor simply relegates the call to base type's destructor to do all the work.

Rectangle structure is defined in basically the same way. We will provide here only the declarations.

/* Listing of rectangle.h */
#include "shape.h"

#ifndef RECTANGLE_H
#define RECTANGLE_H

struct Rectangle
{
    struct Shape _base;
    float width;
    float height;
};

void Rectangle_Constructor(struct Rectangle *_this);
void Rectangle_Constructor1(struct Rectangle *_this, float locationX, float locationY,
                            float width, float height);
void Rectangle_Destructor(struct Rectangle *_this);

#endif

Below is the listing of main.c file which initializes one ellipse and one rectangle and then modifies their locations.

/* Listing of main.c */
#include <stdlib.h>
#include "ellipse.h"
#include "rectangle.h"
#include "shape.h"
#include <stdio.h>

int main(char args[])
{

    struct Ellipse *ellipse = NULL;
    struct Rectangle *rectangle = NULL;

    ellipse = (struct Ellipse*)malloc(sizeof(struct Ellipse));
    Ellipse_Constructor1(ellipse, 1.0F, 2.0F, 3.0F, 4.0F);
    Shape_SetName((struct Shape*)ellipse, "Ellipse");

    rectangle = (struct Rectangle*)malloc(sizeof(struct Rectangle));
    Rectangle_Constructor1(rectangle, 3.0F, 4.0F, 5.0F, 6.0F);
    Shape_SetName((struct Shape*)rectangle, "Rectangle");

    Shape_PrintOut((struct Shape*)ellipse);
    Shape_PrintOut((struct Shape*)rectangle);

    Shape_SetLocation((struct Shape*)ellipse, 5.0F, 6.0F);
    Shape_SetLocation((struct Shape*)rectangle, 7.0F, 8.0F);

    Shape_PrintOut((struct Shape*)ellipse);
    Shape_PrintOut((struct Shape*)rectangle);

    Ellipse_Destructor(ellipse);
    free(ellipse);

    Rectangle_Destructor(rectangle);
    free(rectangle);

}

Here is the program output:

            
Ellipse's location is (1.00, 2.00).
Rectangle's location is (3.00, 4.00).
Ellipse's location is (5.00, 6.00).
Rectangle's location is (7.00, 8.00).
                
    

It is obvious that program is working correctly, at least when talking about shape locations. Ellipse and rectangle are initialized at locations (1, 2) and (3, 4), but after two calls made to Shape_SetLocation function, they have moved to (5, 6) and (7, 8), respectively. Moreover, when calling Shape_PrintOut and Shape_SetLocation functions which both accept pointer to Shape as an argument, we are explicitly casting pointer type to Shape, like in this line:

Shape_PrintOut((struct Shape*)ellipse);

By doing so, we have applied the principle called pointer type substitution. Pointer to subtype - Ellipse - is passed into function argument which is defined as pointer to corresponding supertype - Shape. So much about syntax. But to explain how does this code really work, we will have to take a look at the memory layout once again. Picture below shows a pointer named _this, which is an argument of a function receiving Shape (e.g. Shape_SetLocation function). Caller decides to pass a pointer to Rectangle structure instead. However, function knows only about "general" shape, with only name and location. Same case is when pointer to Ellipse is passed - function is oblivious of the radiusX and radiusY fields. Net result is that Shape_SetLocation function operates only on name, locationX and locationY fields, ignoring the presence of any other field, if there.

Pointer type substitution

Now it is obvious why Shape structure was defined as first field in the Ellipse and Rectangle structures. In order to pass pointer to subtype when pointer to supertype is expected, supertype-defined content must occupy beginning of the overall instance content.

Implementation in Classes

In previous sections we have thoroughly discussed object-oriented design of two structure types derived from Shape. In this section we will simply rewrite them into C++ classes: Ellipse and Rectangle, both subclasses of the Shape class.

First listing will present declaration of the Ellipse class, contained in the ellipse.hpp header file. As already seen when Shape class was designed, class declaration is stunningly similar to structure and accompanying functions declaration.

// Listing of ellipse.hpp
#include "shape.hpp"

#ifndef ELLIPSE_H
#define ELLIPSE_H

class Ellipse: public Shape
{
public:
    Ellipse();
    Ellipse(float locationX, float locationY, float radiusX, floatradiusY);
private:
    float radiusX;
    float radiusY;
};

#endif

Class definition is provided in ellipse.cpp file.

// Listing of ellipse.cpp
#include "ellipse.hpp"

Ellipse::Ellipse() // Implicitly calls Shape() parameterless constructor
{
}

Ellipse::Ellipse(float locationX, float locationY, float radiusX, float radiusY): Shape(locationX, locationY)
{
    this->radiusX = radiusX;
    this->radiusY = radiusY;
}

This piece of code raises some questions, syntactical rather than substantial. Parameterless constructor does not call parameterless Shape constructor explicitly. By convention, C++ invokes default (parameterless) constructor of the base class every time when not stated otherwise. This "otherwise" case is demonstrated by the second constructor, which explicitly calls Shape's constructor with two float arguments to initialize ellipse’s location to specific point. Also, we didn't have to specify the destructor because it would be empty - C++ compiler will add it for us.

Rectangle class is defined almost the same as the Ellipse class and we will leave its implementation to the reader to exercise. And here is the main function which utilizes these two new classes.

// Listing of main.cpp
#include "shape.hpp"
#include "ellipse.hpp"
#include "rectangle.hpp"
#include <iostream>

using namespace std;

int main()
{

    Ellipse *ellipse = new Ellipse(1.0F, 2.0F, 3.0F, 4.0F);
    ellipse->SetName("Ellipse");
    ellipse->PrintOut();

    Rectangle *rectangle = new Rectangle(2.0F, 3.0F, 4.0F, 5.0F);
    rectangle->SetName("Rectangle");
    rectangle->PrintOut();

    delete ellipse;
    delete rectangle;

}

When this code is executed, output produced looks same as ever:

            
Ellipse's location is (1.00, 2.00)
Rectangle's location is (2.00, 3.00)
                
    

Observe how SetName and PrintOut methods have been called directly on objects of Ellipse and Rectangle classes. No casting was required to obtain pointer to base class. This is fundamental principle in object-oriented languages. Methods defined in the base class are present in the derived class as well. It is said that derived classes have inherited these methods from the base class.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub