On interfaces and object-oriented programming

In programming, the term interface means an arrangement that limits the programmer’s direct access a part of a program. The programmer may utilize the hidden/limited parts only by means of certain operations that have been defined beforehand. In practice, interface means that the programmer does not need to know exactly how something has been implemented, yet they are still able to use its services.

We can use string type variables and the data type string as an example here. The average programmer does not have the information or knowledge about the way the string type has been implemented in a C++ library. They do not understand the problems that have to be solved in order to create a structure in which you can save a yet-undetermined length of text. However, anyone who has understood the material of the previous section is able to utilize the string type in their program as it contains a group of functions and operators that can be used to complete all the necessary operations. These ready-to-use operations are the (public) interface of the string type.

You can think of the public interface as the kind of interface that defines what can and cannot be done. What could be the meaning of private interface?

Interfaces are an essential part of object-oriented programming, and object languages are handy in defining interfaces. C++, for example, offers mechanisms for defining public and private interfaces. Object-oriented programming operates on objects communicating with each other via their public interfaces, and the control flow of the entire program is essentially based on passing messages between objects, and the objects reacting to these messages.

The idea of object-oriented programming will be explained more clearly in the rest material of this round. First, we will take a look at classes and objects, and compare them to corresponding elements in Python. Next, we will take up pointers, because they are the most sensible way of creating many of the properties that go with object-oriented programming in C++. This will become apparent at a later stage of this course. Only after the abovementioned two theory sections will we be ready to implement our first actual object-oriented program.

Classes and objects

Let us remind ourselves how to define a simple class and how to use it in Python:

class Person:
    def __init__(self, name, age):
        self.__name = name
        self.__age = age

    def get_name(self):
        return self.__name

    def celebrate_birthday(self, next_age):
        self.__age = next_age

    def print(self):
        print(self.__name, ":", self.__age)

def main():
    pal = Person("Matt", 18)
    print(pal.get_name())
    pal.print()
    pal.celebrate_birthday(19)
    pal.print()

main()

What is the difference between a class and an object? A class is a data type, and object is a value or a variable whose data type is a class. Sometimes objects are also called instances of a class.

A similar class can be written in C++ like this:

#include <iostream>
#include <string>

using namespace std;

class Person {
  public:
    Person(string const& name, int age);
    string get_name() const;
    void celebrate_birthday(int next_age);
    void print() const;
  private:
    string name_;
    int    age_;
};  // Note semicolon!

int main() {
    Person pal("Matt", 18);
    cout << pal.get_name() << endl;
    pal.print();
    pal.celebrate_birthday(19);
    pal.print();
}

Person::Person(string const& name, int age):
    name_(name), age_(age)  {
}

string Person::get_name() const {
    return name_;
}

void Person::celebrate_birthday(int next_age) {
    age_ = next_age;
}

void Person::print() const {
    cout << name_ << " : " << age_ << endl;
}

There are two clear parts in the definition of a class in C++:

  • public, where you present the public interface of the class, i.e. the methods (member functions), by which the objects of the class can be operated
  • private, where you hide the variables (member variables, attributes) you use to describe the concept implemented as the class.

The member variables within the private part cannot be directly accessed from outside the class. If and when you want to use their values, you need to add a method to the public interface of the class, and that method will allow you access to member variables.

The member variables are used in the body of the method just like normal variables, but you do not have to define them separately, because each object has a member variable copy of their own.

In the above example, string parameter (name) is passed as a constant reference due to the reason explained in section 3.2 Parameter passing, at Constant parameters. However, the corresponding attribute (name_) is a usual string, not a constant nor a reference. Why?

Methods

C++ has two special methods: constructor and destructor.

The constructor method is always named after the class, and is given no return value type. The constructor is always automatically called when you create a new object. The constructor’s job is to initialize the created object.

The destructor is called automatically when an object comes to the end of its life time. The example program does not have a destructor. If the program had a destructor, its name would be ~Person, i.e. tilde + the name of the class. A constructor has no parameters.

In the example, you can see the reserved word const after the final parenthesis of the parameter list. You can use these methods just to examine but not change the state of the object (the values of the member variables). If the word const is missing, the method can change the values of member variables.

With the exception of the constructor and the destructor, methods are called usually with the notation:

object.method(parameteters);

where the method method is targeted to the object object with parameters parameters.

The call of a constructor takes place automatically behind the scenes every time you need to initialize a new object. The parameters of the constructor are written in parentheses after the name of the object you want to define. As you define a constructor function, you will initialize the member variables of the object using the initialization list:

AClass::AClass(parameter1, parameter2):
  attribute1_(parameter1), attribute2_(parameter2) {
}

The initialization list is written in the definition of the constructor after the colon and before the constructor’s body (curly brackets, {}). You should list all of the attributes of the class in the same order they are in at the class interface, and after that, you should initialize them with initial values.

If a class has a destructor, it will automatically be called at the end of the object’s life time. The life time of the local objects ends at the end of their scope (i.e. a program block where an object is visible).

The class is a tool for creating abstracts (concepts) in a program. The details of how a class has been implemented are hidden from its user, who can only use the class via the public interface.

There will be concepts in the program that are defined by their possible uses: ”A person is what you can do to them.” Experience has shown that the program becomes clearer if you use concepts defined by their functionality like the one above.

Operator methods

Let us consider a one more special case of methods, i.e. operators.

With classes you can define your own types (abstract data types), and for them it is natural to define functions, the meaning of which is similar to some existing operators such as +, +=, ==, etc.

For example, consider a class called Fraction for describing fractions. If the class has two integer attributes numerator_ and denominator_, then we can define a function called operator== for comparing the equivalence to another fraction as follows:

bool Fraction::operator==(Fraction const& other) const
{
    return numerator_ == other.numerator_ && denominator_ == other.denominator_;
}

when assuming that the fractions are as reduced as possible.

Now we can compare two fractions in the same way as any numeric types:

Fraction f1(2, 3);
Fraction f2(3, 4);
if(f1 == f2) ...

when assuming that the constuctor of Fraction takes two parameters: one for the numerator and the other for the denominator.

How to implement a class in a separate file

In the example above, we implemented the class in the same file as the main program, just like we always did with Python programs on the previous course. Let us now create a new version and separate the class into two files, since this is the usual way in C++. Let us first have a look at the renewed contents of main.cpp:

#include "person.hh"
#include <iostream>

using namespace std;

int main() {
    Person pal("Matt", 18);
    cout << pal.get_name() << endl;
    pal.print();
    pal.celebrate_birthday(19);
    pal.print();
}

We will notice that when the class definition was removed, the include directive was added to the beginning of the file:

#include "person.hh"

This is the way how a class that was implemented elsewhere can be used in the main program. Now, let us have a look at the contents of the file in question, i.e. the file person.hh:

#include <string>

using namespace std;

class Person {
  public:
    Person(string const& name, int age);
    string get_name() const;
    void celebrate_birthday(int next_age);
    void print() const;
  private:
    string name_;
    int    age_;
};  // Note semicolon!

The file with the extension .hh is a so-called header file or a definition part. Here we only tell what kind of a class we are talking about. As you can see, the file does not have the implementations of class methods.

The method implementations are in the corresponding implementation file person.cpp:

#include "person.hh"  // Note! Implementation file includes the corresponding header (definition) file!
#include <iostream>
#include <string>

using namespace std;

Person::Person(string const& name, int age):
    name_(name), age_(age)  {
}

string Person::get_name() const {
    return name_;
}

void Person::celebrate_birthday(int next_age) {
    age_ = next_age;
}

void Person::print() const {
    cout << name_ << " : " << age_ << endl;
}

At this point, you might ask how the method implementations will be a part of the program, considering the file main.cpp uses the directive include to only include the header file person.hh. The file person.cpp needs to be added to the program at the compiling phase. We will get into this in the next part called ”Creating and compiling a class in Qt Creator”.

In C++, it is customary to divide a class into two parts: the definition and the implementation. They are written in their separate files. Therefore, for each class you will write two files:

  • The definition part includes the definition of the class and is saved in a file with the extension .hh, named after the class but with a lowercase first letter. See the file person.hh in the example above.
  • The implementation part includes the implementations of the methods of the class and is saved in a file with the extension .cpp, named after the class but with a lowercase first letter. See the file person.cpp in the example above. (The terminology can be a bit confusing. We mentioned above ”the implementations of the methods”. Referring to earlier discussion, these are actually (almost) the same as ”function definitions”, as opposed to ”function declaration”.)

When you use the C++ libraries with the directive include, you use angle brackets (<>). When you use your own files with the directive include, you write the filename within quotation marks (””).

Creating and compiling a class in Qt Creator

When you want to add a new class to an already existing project in Qt Creator, as you choose ”New File or Project,” you can click on ”C++” and ”C++ Class” in the next window. This makes Qt Creator automatically create both of the files you need (.cpp and .hh). In addition, Qt Creator will automatically add the new implementation file among the files that will be compiled in the project.

If you wish, you can have a look at the project file (with the extension .pro) in Qt Creator and see the point SOURCES, which tells the compiler which files to include in compilation. This will update automatically when you create a class in the abovementioned way. As you can see, the point SOURCES contains only implementation files, not header files.

We will consider compilation more precisely in the context of modularity. It will also be the time to become more acquainted with the phases of compilation. For the beginning of the course, you can happily let Qt Creator worry about the compiling automatization.

The class Clock, the 1st version

Let us implement another example class Clock in two different versions. The first version contains nothing drastically new compared to what you have learned earlier. It is supposed to serve as an introduction to the next example. We will compare the first and the second version with each other.

The header file of the class is as:

class Clock {
  public:
    Clock(int hour, int minute);
    void tick_tock();  // Time increases with one minute
    void print() const;

  private:
    int hours_;
    int minutes_;
};

In the main program, the class can be used as:

#include "clock.hh"

int main() {
    Clock time(23, 59);
    time.print();
    time.tick_tock();
    time.print();
}

The implementation file of the class is as:

#include "clock.hh"
#include <iostream>
#include <iomanip>

using namespace std;

Clock::Clock(int hour, int minute):
    hours_(hour), minutes_(minute) {
}

void Clock::tick_tock() {
    ++minutes_;
    if ( minutes_ >= 60 ) {
        minutes_ = 0;
        ++hours_;
    }
    if ( hours_ >= 24 ) {
        hours_ = 0;
    }
}

void Clock::print() const {
    cout << setw(2) << setfill('0') << hours_
         << "."
         << setw(2) << minutes_
         << endl;
}

The only thing to note from the implementation of the first version is how the method print uses the operations from the library iomanip to format the print layout. Let us not worry about them.

The class Clock, the 2nd version

In the second version, we will change the class Clock a little. First, please note the changes in attributes that can be seen in the header file:

class Clock {
  public:
    Clock(int hour, int minute);
    void tick_tock();
    void print() const;

  private:
    // Minutes since the previous midnight
    int minutes_since_midnight__;
    int fetch_hour() const;
    int fetch_minutes() const;
};

The corresponding changes can be seen in the implementation file, as well:

#include "clock.hh"
#include <iostream>
#include <iomanip>

using namespace std;

Clock::Clock(int hour, int minute):
    minutes_since_midnight__(60 * hour + minute) {
}

void Clock::tick_tock() {
    ++minutes_since_midnight__;
    if ( minutes_since_midnight__ >= 24 * 60 ) {
        minutes_since_midnight__ = 0;
    }
}

void Clock::print() const {
    cout << setfill('0') << setw(2) << fetch_hour()
         << "."
         << setw(2) << fetch_minutes()
         << endl;
}

int Clock::fetch_hour() const {
    // When you divide an integer by an integer,
    // the result is a rounded down integer.
    return minutes_since_midnight__ / 60;
}

int Clock::fetch_minutes() const {
    return minutes_since_midnight__ % 60;
}

The new implementation has some interesting changes when compared to the original version:

  • The example shows one of the good characteristics of classes (or rather, interfaces): The private interface (implementation) of the class has been changed altogether, but because the public interface was kept compatible with the original, there is no need to change the code that utilizes the class (main).

  • The method print no longer has a direct reference to the member variable of the class; instead, the time to be printed is found with the help of two new methods, fetch_hour and fetch_minute. This means that the method print no longer needs to know the format of the time in the private part.

    Therefore, a new (informal) interface has been created within the class: Some of the methods of the class do not operate with the member variables directly but instead through the methods fetch_hour and fetch_minute.

    The benefit of this solution is that if you change the implementation of the class (meaning the private part and the methods operating on that part), you do not have to touch the implementation of the print method as long as you take care that fetch_hour and fetch_minute work the same way as previously.

  • Please note that you can use the method to call another method of the same class: The notation of the call is the same as when you call a normal (non-method) function:

    method(parameters);
    

    The above kind of method call will target to the same object you used before the full stop when you called the original method.

More examples

Materials for this round contains also the example examples/03/students. It can be useful in doing the exercises concerning objects on this round.

You need not understand implementations of the methods of the given classes. For example, it does not matter if you do not understand advance method or the second constructor of Date class.

The benefits of using classes

When you use classes to implement concepts in a program, you will nearly always achieve some benefits:

  • It becomes possible to change your implementation in the private interface, while the public interface will stay compatible with the earlier implementation.
  • You can be sure about the integrity of data. Constructors and mutators will take care that the object does not receive erroneous values.
  • The classes make the program clearer, more understandable and easier to maintain.
  • Often, classes are reusable.
  • Classes help you to control the complexity of a program, because they allow you to combine logical parts of the program.

In fact, these benefits do not only come with classes; all the other mechanisms that allow you to create clear interfaces in your program provide the same benefits as well. For example, a function also forms an interface. As long as you understand what parameters you are supposed to give to the function and which value it will return, you do not need to know anything about the implementation. Therefore, all of the benefits mentioned above are also true of functions.