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 operatedprivate
, 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 can be divided into four categories:
- Constructor:
The constructor function 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.
- Selector:
Selectors are methods that you use to examine but not change the state of the object (the values of the member variables).
You can make a method as a selector by adding the reserved word
const
after the final parenthesis of the parameter list. This will prevent any attempt to change the state of the object in the method in question.- Mutator:
- Mutators are such methods that let you change the state of an object, i.e. the values of the member variables.
- Destructor:
- The destructor is called automatically when an object comes to the end of its life time. The example program does not have a destructor.
With the exception of the constructor and the destructor, methods are called usually with the notation:
object.method(parameteters);
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 functions¶
It was earlier listed four categories for methods. In addition to them, there is a special case: 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.
After that 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.
Let us first have a look at the renewed contents of main.cpp
:
#include <iostream>
#include "person.hh"
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 <iostream>
#include <string>
#include "person.hh" // Obs! Implementation file includes the corresponding header (definition) file!
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 fileperson.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 fileperson.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¶
Let us implement a second 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.
#include <iostream>
#include <iomanip>
using namespace std;
class Clock {
public:
Clock(int hour, int minute);
void tick_tock(); // Time increases with one minute
void print() const;
private:
int hours_;
int minutes_;
};
int main() {
Clock time(23, 59);
time.print();
time.tick_tock();
time.print();
}
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 most important thing to remember from 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.
In the second version, we will change the class Clock
a little.
First, please note the attributes.
#include <iostream>
#include <iomanip>
using namespace std;
class Clock {
public:
Clock(int hour, int minute);
void tick_tock();
int fetch_hour() const;
int fetch_minutes() const;
void print() const;
private:
// Minutes since the previous midnight
int minutes_since_midnight__;
};
int main() {
Clock time(23, 59);
time.print();
time.tick_tock();
time.print();
}
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;
}
}
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;
}
void Clock::print() const {
cout << setfill('0') << setw(2) << fetch_hour()
<< "."
<< setw(2) << fetch_minutes()
<< endl;
}
The new version 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
andfetch_minute
. This means that the methodprint
no longer needs to know the format of the time in theprivate
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
andfetch_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 theprint
method as long as you take care thatfetch_hour
andfetch_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.
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.