Laboratory Training 3

Creation and Use of C++ Classes

1 Training Tasks

1.1 Class for Representing a Point in Three-Dimensional Space

Create a class for representing a point in three-dimensional space (3D points). The class must meet the following requirements:

  • have a constructor without parameters;
  • have a constructor with three parameters;
  • contain data members of double type to represent the coordinates of a point;
  • implement public data access functions (setters and getters).

To calculate the distance, create an operator function operator-() ("minus") with two parameters (3D points) and a result of type double. Declare this function as a friend of the class.

In the main() function, you should create two objects of 3D point type by applying different constructors. Use access functions to set and read values. Calculate the distance between two points by using the minus operation, then change the coordinates of one of the points and calculate the distance between the two points by explicitly calling the operator-() function.

1.2 Class for Representing a Simple Fraction

Create a class that represents a simple fraction. Implement constructors, a function for reduction of fractions, and overload operations +, -, *, /, <, <=, >, >=, as well as stream input and output. In the output operator function, implement the most correct output: get and output the whole part for improper fractions, do not output the denominator if it is equal to 1 or the numerator is equal to 0, etc.

Demonstrate class features in the main() function.

Read two fractions and demonstrate the reduction of fractions and all overloaded operations in the main() function.

Note: you should not store the integer part of the fraction separately, since it can always be calculated from the numerator and denominator.

1.3 Classes for Representing Students and Groups

Create classes for presenting data about students and groups of students. The "Student" class should contain the following private data members:

  • student ID number (unsigned int);
  • last name (pointer to character); the corresponding string will be created in a free store as needed;
  • grades for the last session in the form of an array of integers from 0 to 100 (grades by subject); an array should be created in a free store;
  • a pointer to an object of class "Group".

Define the following members in the "Student" class:

  • constructor without parameters and constructor with parameters;
  • copy constructor;
  • destructor;
  • data access functions (setters and getters);
  • overloaded assignment operator.

The operator function of output data about the student to the stream should be implemented as an external function (class friend). To ensure sorting, overload the necessary comparison (relation) operator functions.

A constant should be defined for the maximum possible number of students (for example, 50)

The following data elements should be defined in the "Group" class:

  • group index;
  • an array of pointers to students of the maximum possible length;
  • the actual number of pointers in the array.

Define the following elements in the "Group" class:

  • constructor without parameters and constructor with parameters;
  • group index data access functions (setter and getter);
  • overloaded assignment operator;
  • the function of sorting according to the specified criterion;
  • finding students by certain attribute.

Instead of a getter for an array of pointers, you can overload the subscript operator of getting an item by index. In addition, an operator function of output data is required.

In the main() function, create a "Group" object, add an array of students and demonstrate all implemented functions, in particular, sorting and searching according to task 3.1 of the previous laboratory training.

1.4 Class for Representing a Two-Dimensional Array

Develop a class to represent two-dimensional array of integers (matrix) of arbitrary size. Create constructors and destructors, overload operations of addition, subtraction and multiplication (according to the rules of working with matrices), getting items by index, as well as stream input and output. Create your own exception classes and generate relevant objects, if it is impossible to perform a particular operation.

Create a separate function that gets reference to an array and performs actions listed in the table. The function should not be friend or member function.

Index of variant
(from students list)
Rule of a source array transformation
1
15
All elements with odd values should be doubled
2
16
All elements with even values should be replaced with their squares
3
17
All elements with null value should be replaced with ones
4
18
All elements with even values should be doubled
5
19
All elements should be replaced with their absolute magnitudes
6
20
All elements with even values should be tripled
7
21
All positive elements should be replaced with integer parts of their Briggs (base ten) logarithms
8
22
All negative elements should be replaced with their squares
9
23
All positive elements should be replaced with integer parts of their Napierian (natural) logarithms
10
24
All positive elements should be replaced with integer parts of their square roots
11
25
All positive elements with even values should be doubled
12
26
All negative elements with odd values should be tripled
13
27
All negative elements with odd values should be doubled
14
28
All positive elements with even values should be tripled

Demonstrate all class features in the main() function. Solve individual task with catching of possible exceptions.

1.5 Calculation of the Sum of Entered Values

Create a class with a private data member, getter, and constructor with one argument. This class also should contain a static private data member that stores the sum of all data members of previously created objects. Each call of the constructor includes adding of a new value to the static field. Public static function of the same class should return this sum.

The main() function should contain creation of several objects and output of the calculated sum.

2 Instructions

2.1 Prerequisites for the Emergence of an Object-Oriented Approach

In the first decades of programming, a typical program included reading data from punched tape, punched cards, or magnetic media, performing calculations, branching depending on data and results, cycles, etc., and outputting the results to a printer. In order to get the long-awaited numerical results, this is enough. This approach is called data-driven programming.

Large programs that performed complex calculations had to be divided into relatively independent parts – separate procedures that perform fixed work within the task. Informationally independent blocks could be created within such procedures. This idea was embodied in procedural programming languages such as ALGOL-60 and PASCAL.

Significant successes in the application of programming in calculation tasks stimulated its spread in other areas: banking, production management, trade, ticket booking, etc. These new tasks differed significantly from traditional calculations:

  • on the one hand, the volume of direct calculations and the complexity of the formulas in the new problems is significantly lower;
  • on the other hand, the complexity of decision-making logic, processing rules, etc. is significantly higher;
  • programs that tried to apply in new areas involved receiving data from various sources during operation; new data directly affected the calculation processes and led to completely different results.

Attempts to apply the traditional approach to new problems mostly failed.

Overcoming the crisis in programming allowed the spread of object-oriented approach in the eighties of the twentieth century. This was primarily related to a new approach to the decomposition of a complex system.

Decomposition is the division into relatively independent parts. Usually, there are several options for decomposition. In particular, it can be procedural and object-oriented decomposition.

  • procedural decomposition (traditional) involves dividing a complex system by its functions, which can be represented as "black boxes" with a fixed set of input data and fixed results;
  • object-oriented decomposition involves dividing the system into relatively independent objects for which the state and set of possible operations on these objects can be determined.

The main advantage of object-oriented decomposition is that it is as close as possible to the structure of real systems, in which it is quite natural to distinguish the constituent parts in the form of separate objects. Instead of building a program from sequential calls of procedures, it is envisaged to create and register software objects that receive messages (events) from the outside world, process these events, and also send messages to other objects.

The C++ language is one of the first and at the same time the most powerful object-oriented programming languages.

2.2 Definition of Classes

Class in C++ is the primary a way to create user defined type. Class is similar to structure. Class is a structured data type that definition includes a set of data members of different types and functions for working with data members.

Data members are variables that determine the state of an object.

Member functions are functions that have direct access to the object. Member functions determine the behavior of the object. Unlike global functions, you must create an object first, and then you call member functions in the context of the object.

One of the important principles of object-oriented approach is the encapsulation (data hiding). The idea behind encapsulation is hiding implementation details of the object from the outside user. To implement encapsulation C++ provides three levels of access: public, protected, and private. Appropriate keywords followed by colon are used to group class members of the same access level.

  • Class members declared as private can be used by member functions and friends (classes or functions) of the class.
  • Class members declared as protected can additionally be used by classes derived from the class.
  • Class members declared as public can be used by any function.

For example, we can define class called Country:

class Country
{
private:
    char   name[40];
    double area;
    int population;
public:
    char*  getName();
    double getArea();
    int    getPopulation();
    void   setName(const char* value);
    void   setArea(double value);
    void   setPopulation(int value);
    double density();
};

Member functions getName(), getArea(), getPopulation(), setName(), setArea(), and setPopulation() provide access to private data members. Such functions are called access functions. They are also called getters and setters.

Member function density() returns density of population.

Member functions declared in class body can be defined outside a class body. The belonging to particular class can be denoted using scope resolution operator (::). Here is an implementation of density() function:

double Country::density()
{
    return population / area;
}

As you can see, the member function has free access to data members.

Member function can be defined inside of class. This function is implicitly inline function. Class definition can be changed:

class Country
{
private:
    char   name[40];
    double area;
    int    population;
public:
    char*  getName()                  { return name; }
    double getArea()                  { return area; }
    int    getPopulation()            { return population; }
    void   setName(const char* value) { strcpy(name, value); };
    void   setArea(double value)      { area = value; }
    void   setPopulation(int value)   { population = value; }
    double density();
};

Now you can use the class name to define and object and then call member functions:

void main()
{
    Country someCountry;
    someCountry.setName("France");
    someCountry.setArea(551500);
    someCountry.setPopulation(57981000);
    cout << someCountry.density();
}

Note: C++11 has changed the rules for initializing data members of primitive types: compilers require explicit initialization, so the above code will cause warnings; the mechanisms of such initialization will be discussed later.

Member functions obtain access to other class members using the so called this pointer. Member functions get that pointer as an implicit argument. The this pointer points to the object for which member function was called. This pointer is implicitly passed to each member function during a call. For example, the implementation of the density() function may be as follows:

double Country::density()
{
    return this->population / this->area;
}

In this case, the use of this does not make practical sense, but demonstrates the mechanism of usage data members within member functions. In most cases, this pointer is used to avoid name conflicts. For example, by convention, the setter parameter names coincide with the data element names. In this case, the name without this is the name of the parameter (local variable), and with this is the name of the data member:

void   setName(const char* name)     { strcpy(this->name, name); };
void   setArea(double area)          { this->area = area; }
void   setPopulation(int population) { this->population = population; }

Sometimes it is necessary to return the reference or pointer to the object:

Country& Country::getCountry() 
{
    . . . 
    return *this;
}

There is a special kind of member functions, the so-called constant member functions. Constant member function cannot modify data members. To declare constant member function, you must place const keyword after the argument list:

class Country
{
...
public:
    double getArea() const { return area; }
...
};

Consonant member function can be called for a constant object:

     const Country France;
     cout << France.getArea();
     France.setArea(1000000); // Error!

Class can contain both constant and non-constant member functions with same argument lists.

In our example, getters and the density() function can be defined with the const modifier. But in this case, the getName() function should return a pointer to a constant string (const char*). In general, the use of pointers to a constant object is good practice. You can also declare the corresponding setter parameter. The class will look like this:

class Country
{
private:
    char   name[40];
    double area;
    int    population;
public:
    const char* getName() const { return name; }
    double      getArea() const { return area; }
    int         getPopulation() const { return population; }
    void        setName(const char* name) { strcpy(this->name, name); };
    void        setArea(double area) { this->area = area; }
    void        setPopulation(int population) { this->population = population; }
    double      density() const;
};

double Country::density() const
{
    return population / area;
}

The above example shows that for member functions that are implemented outside the body of the class, the const modifier should be specified in both the declaration and the implementation.

The mutable type specifier before the data member is used to ensure that this element could be changed from constant member functions.

If the member function has a const modifier, it receives this pointer this that points to constant object.

A class declaration is the class name with a semicolon:

class Country;

Now you can create a pointer to the declared class, specify them as friends, but not create an object.

2.3 Object Initialization. Constructors and Destructors

Unlike the procedural approach in which certain actions, in particular, calling functions, are carried out in the order defined by the author of the program, object-oriented approach provides a class for creating objects and provides the ability to call member functions in any order. This means that the created object should be ready to work immediately and all data members should be initialized.

Classes have a special member functions called constructors. Constructors are designed to initialize objects' data. Constructors are automatically called when an object is created. For different objects, there are different options for calling constructors:

  • for global variables – before the main() function is started, in the description order;
  • for local variables – during the execution of the function (block) in the specified order;
  • for objects located in dynamic memory – immediately after allocating memory.

The name of constructor must coincide with class name. You can declare any number of constructors. Different constructors of a single class can be distinguished by the number or type of the parameters. We can define several constructors in a previously created class:

class Country
{
private:
    char   name[40];
    double area;
    int    population;
public:
    Country(double area) { this->area = area; }
    Country(const char* name);
    Country(const char* name, double area);
    Country(const char* name, double area, int population);
    . . .
};

Other variants can be also added.

A constructor with one parameter of type double is implemented in the body of the class. Starting with version C++ 11, the compiler will generate a warning about the absence of the initial values of some data members. We will correct this situation later.

Constructors can be implemented outside class body:

Country::Country(const char *name)
{
    strcpy(this->name, name);
    area = 1; // it is essential that the area does not have a value of 0 
              // and there is no error in the density() function)
    population = 0;
}

Country::Country(const char *name, double area)
{
    strcpy(this->name, name);
    this->area = area;)
    population = 0;
}

Country::Country(const char *name, double area, int population)
{
    strcpy(this->name, name);
    this->area = area;
    this->population = population;
}

Data members can also be initialized in a so-called initializer list. This list can be placed between constructor's header and constructor's body. It starts with a colon and then data members with the initial values in parentheses are placed, separated by commas. For example, the last constructor can be implemented as follows:

Country::Country(const char * name, double area, int population)
     : area(area), population(population)
{
    strcpy(this->name, name);
}

Starting with version C++11, you can call one constructor from another in the initializer list. This will simplify the implementation of constructors, which can now be placed in the body of the class, as well as redesign the constructor with the area parameter:

class Country
{
    ...
public:
    Country(double area) : Country("", area, 0) { } // All members are now initialized
    Country(const char* name) : Country(name, 1, 0) { }
    Country(const char* name, double area) : Country(name, area, 0) { }
    ...
};

Note. It is possible to be limited in general to one constructor, using the mechanism of the default parameters:

Country::Country(const char* name, double area = 1, int population = 0)
    : area(area), population(population)
{
    strcpy(this->name, name);
}

This implementation of the constructor has its drawbacks, because it reduces the visibility of the program.

The simplest constructor is the default constructor. It has no parameters. If a constructor without parameters is not explicitly defined, the compiler creates such a constructor automatically. If at least one explicit constructor is defined in the class, the compiler does not create a default constructor automatically. In the example above, a constructor without parameters is not automatically created, so if such a constructor is required, it should be added manually. You can specify values whose default values must be different from 0, for example:

class Country
{
    ...
public:
    Country() : Country("", 1, 0) { }
    ...
};

Starting with version C++11 it is possible to specify the initial value (default member initializer) directly when creating the data member:

class Country
{
private:
    char name[40] = {};
    double area = 1;
    int population = 0;
    . . .
};

You can create a constructor without parameters with an empty body:

Country() { }

When you create an object, constructor is called implicitly or explicitly:

Country firstCountry;                    // call constructor without parameters
Country secondCountry(603700);           // call constructor Country(double area)
Country thirdCountry = "France";         // call constructor Country(const char* name)
Country fourthCountry("Poland", 312696); // call constructor with two parameters
Country *pCountry = new Country("Germany", 357000, 81338000); // with three parameters,
                                         // the location of the object in a free store

As can be seen from the example, a constructor with one parameter can be called by initializing the object with the value of the actual parameter using the assignment operator.

If, for example, some external function involves obtaining a parameter of the class object type, it can, for example, be called by creating an anonymous object (with an explicit call to the constructor):

void addToDatabase(Country c); // function prototype
// ...
addToDatabase(Country("Sweden", 450000, 8745000));

You can perform the initialization of the object by another object of the same type. In this case, the special constructor is called, so-called copy constructor. This constructor, if it is not overridden, is automatically created. It performs item-up copying of data of the previously created object to the new one. For example:

Country Ukraine = secondCountry;

You can create a copy constructor explicitly. This is necessary, for example, when there is a pointer to certain data in the heap among the data elements of the class. The previous version of the Country class does not require the creation of an explicit copy constructor. But, for example, the name of the country can be placed in a free store. Since the length of the names of countries is very different, this approach will save memory. The new version of the Country class will be as follows:

class Country
{
private:
    char*  name = nullptr;
    double area = 1;
    int    population = 0;
public:
    Country() { }
    Country(double area) { this->area = area; }
    Country(const char* name) : Country(name, 1, 0) { }
    Country(const char* name, double area) : Country(name, area, 0) { }
    Country(const char* name, double area, int population);
    const char* getName() const { return name; }
    double      getArea() const { return area; }
    int         getPopulation() const { return population; }
    void        setName(const char* name);
    void        setArea(double area) { this->area = area; }
    void        setPopulation(int population) { this->population = population; }
    double      density() const { return population / area; };
};

Country::Country(const char * name, double area, int population)
{
    this->name = new char[strlen(name) + 1];
    strcpy(this->name, name);
    this->area = area;
    this->population = population;
}

void Country::setName(const char * name)
{
    if (this->name != nullptr)
    {
        delete[] this->name;
    }
    this->name = new char[strlen(name) + 1];
    strcpy(this->name, name);
}

Now in all constructors that receive the name of the country, the name will be located in free store. In addition, the implementation of the setName() function involves checking whether the name has already been located in memory and data deallocation if necessary.

Note: the code uses the nullptr constant to indicate a pointer that does not point to anything (zero, or NULL in previous versions); this constant appeared in C++ 11 and is safer in terms of pointer types.

If we now create a new object by copying the old one (for example, Country Ukraine = secondCountry), all data members will be copied field by field. This means that in two objects, two name pointers will point to the same string in memory. A name change in one object causes a name change in another, which is not good idea. It is necessary to implement some more correct way of copying. That is, we need to explicitly create a copy constructor.

To explicitly create a copy constructor for the Country class, the following declaration should be added:

class Country
{
    . . .
public:
    . . .
    Country(const Country& c);
    . . .
};
      

The implementation of the copy constructor will be as follows:

Country::Country(const Country &c)
{
    name = new char[strlen(c.name) + 1];
    strcpy(this->name, c.name);
    area = c.area;
    population = c.population;
}

Now the name of each country will be stored separately.

C++11 offers enhanced constructor capabilities. These possibilities will be considered later.

Destructor is a special member function with the name ~ClassName. Destructor is automatically called each time an object must be destroyed. Destructor cannot have arguments. Destructors of global and local variables are called in the reverse order of constructor calls:

  • for global variables – after performing the main() function;
  • for local variables – when leaving a function (block);

For objects that have been located in free store, destructors are called by the delete operation before the deallocation.

If we do not create an explicit destructor, a destructor with an empty body is automatically created. In our case, we should explicitly create a destructor to free dynamic store:

class Country
{
    . . .
public:
    . . .
    ~Country();
    . . .
};

. . .

Country::~Country()
{
    if (name != nullptr)
    {
        delete[] name;
    }
}

As can be seen from the previous example, destructors can also be implemented outside the class body. In the destructor, as in the setName() function, it is very important to check whether the string has been created before.

Objects as parameters are passed to functions by value. The copy constructor is called by transferring argument. When returning an object type result, a destructor is called:

MyClass f(MyClass t) // Call copy constructor
{
    return t;
}                    // Call destructor

If you want to prevent copying the object and calling the appropriate constructor, you should declare a reference-type argument:

Country& readCountry(Country& c) // Constructor is not called
{
    double area;
    int population;
    cin >> area >> population;
    c.setArea(area);
    c.setPopulation(population);
    return c;
} // Destructor is not called

If you want to protect the object from modification, the reference can be declared with the const modifier.

2.4 Class Scope. Nested Classes

2.4.1 Class Scope

Each class determines its own scope. Members of class declared inside a class are declared in a class scope. You can access names of a class scope using . or -> applied to object or pointer name or :: (scope resolution operator) applied to a class name.

For example, you can use the scope resolution operator instead of this pointer:

class Country
{
private:
    char*  name = nullptr;
    double area = 1;
    int    population = 0;
public:
    Country(double area) { Country::area = area; }
    // ...
    void setArea(double area) { Country::area = area; }
    void setPopulation(int population) { Country::population = population; }
};

Using a class name instead of a this pointer makes the text less obvious. Usually, this practice is used to work with static elements (will be discussed below).

Class methods require the creation of an object, so they cannot be called outside the class through a scope resolution operator:

Country::setArea(1000); // syntax error if done outside the class

You can place typedef definition within a class body.

class Country
{
public:
    typedef double areaType;
private:
    areaType area = 1;
    // ... 
};

You can, for example, create some class that only contains definitions of type aliases:

class Typedefs
{
public:
    typedef long int Integer;
    typedef Integer Long;    // synonym
    typedef long long int VeryLong;
    typedef unsigned long int Cardinal;
};

Such a class definition is analogous to a namespace definition. Inside the class, you can freely use the created synonyms, outside the class you need to use scope resolution operator.

Typedefs::Integer i;
Typedefs::Long l;
Typedefs::VeryLong v;
Typedefs::Cardinal c;

Unlike namespaces, the use of prefixes is mandatory. There are no analogs using-directives or using-declarations for classes.

2.4.2 Nested Types

Class itself can be defined in a global scope, in a class scope, or in a local scope. Inner (nested) classes are defined in a scope of other classes. In our example, we can offer a Capital class to represent the capital of the country.The instance of inner class is not created automatically as a part of outer object. It can be created separately:

class Country
{
public:
    class Capital
    {
    private:
        char name[20] = {};
        int population = 0;
    public:
        Capital() { }
        Capital(const char* name, int population) : population(population) 
            { strcpy(this->name, name); };
        const char* getName() const { return name; }
        void  setName(const char* name) { strcpy(this->name, name); };
        int getPopulation() const { return population; }
        void setPopulation(int population) { this->population = population; }
    };
private:
    // ...
    Capital capital;
public:
    // ...
    Capital getCapital() const { return capital; }
    void setCapital(Capital capital) { this->capital = capital; }
};

An object of Capital class can also be created outside the class:

Country someCountry("France", 551500, 67970000);
Country::Capital capitalOfFrance("Paris", 2175600);
someCountry.setCapital(capitalOfFrance);

A nested class can be defined in the private part. Such a class can only be used in the body of an outer class, so encapsulation has no reason. We can also remove unnecessary constructors. Objects of this class cannot be created outside the outer class. In addition, instead of getCapital() and setCapital(), access functions to individual data about the capital should be created:

class Country
{
private:
    class Capital
    {
    public:
        char name[20] = {};
        int population = 0;
    };
    // ...
    Capital capital;
public:
    // ...
    const char* getCapitalName() const { return capital.name; }
    void  setCapitalName(const char* name) { strcpy(capital.name, name); };
    int   getCapitalPopulation() const { return capital.population; }
    void setCapitalPopulation(int population) { capital.population = population; }
};

Information about the presence of the Capital inner class is now hidden from the programmer that uses the Country class. Work with relevant data is carried out through access functions.

2.4.3 Local Classes

A local type is a type defined inside a block (function). The syntax of local classes is no different from the syntax of ordinary classes. A local class can be large enough.

A local class can only be used within the block in which it was defined. Since it is not a good practice to create functions with too much code, you should avoid creating too large local classes.

Before the introduction of lambda expressions, local classes were the only way to create local functions. In Laboratory training No. 4 of the previous semester, the function of calculating the distance between two points was proposed. Using lambda expressions, the code for this function was:

double distance(double x1, double y1, double x2, double y2)
{
    auto sq = [](double a) { return a * a; };
    return std::sqrt(sq(x2 - x1) + sq(y2 - y1));
}

Instead of a lambda expression, we can create a local class with an appropriate method. The code of the program for calculating the distance between points can be as follows:

#include <iostream>
#include <cmath>

double distance(double x1, double y1, double x2, double y2)
{
    class SecondPower
    {
    public:
        double sq(double a) { return a * a; };
    };
    SecondPower sp;
    return std::sqrt(sp.sq(x2 - x1) + sp.sq(y2 - y1));
}

int main()
{
    std::cout << distance(1, 1, 4, 5) << std::endl;
    return 0;
}

The presence of lambda expressions reduces the sense of using such solutions.

2.5 Static Class Members

Sometimes you need to create some variables which logically belong to some class. All instances of class must share a single copy of a variable. Such data can be stored using conventional global variables. Static data members provide a way of declaration such variables in a class scope.

Examples of potential static data members are the name of the class in different languages, the number of objects of this type, or some other general data about the class. For the Country class, you can, for example, define a static data member for the name of the presented entity in English, Ukrainian, French, or some other language. In the simplest case, it can be a public field:

class Country
{
public:
    static char className[20];
    // ...
};

To access such members, you can use either object name or class name:

. . .

strcpy(Country::className, "Country"); // Using class name
Country c;
strcpy(c.className, "Contrée"); // Using object name (in French)
std::cout << c.className;

As can be seen from the given example, static members do not require the creation of an object. If we interact with a static member through an object, all value changes apply to the entire class, not to a single object.

Static data members declared in a class body must be defined in a global scope. In order for the above code to work, the field definition should be added in the global scope (the static modifier is not repeated):

char Country::className[20] = {};

Static data members cannot be described with mutable modifier.

There are also static member functions. They do not get this pointer and therefore they have no access to non-static members of the object for which they are called. For example, it is better to define a className data member as private and provide it with access functions:

class Country
{
private:
    static char className[20];
public:
    static const char* getClassName() { return className; }
    static void setClassName(const char* className)
        { strcpy(Country::className, className); };
    // ..
};

Working with a static field is now done through methods:

Country::setClassName("Country");
Country c;
c.setClassName("Contrée");
std::cout << c.getClassName();

The className data member, although private, still requires a definition in the global scope:

char Country::className[20] = {};

In addition to implementing work with static data elements, a class with static methods can represent a library of functions. In this context, it plays the role of a namespace or module. Example:

class Math
{
public:
    static double sqr(double x) { return x * x; }
    static double cube(double x) { return x * x * x; }
    static double reciprocal(double x) { return 1 / x; }
    static int factorial(int n);
};

int Math::factorial(int n)
{
    int result = 1;
    for (int k = 2; k <= n; k++)
    {
        result *= k;
    }
    return result;
}

It can be seen from the given code that static member functions can also be defined outside the class. In this case, the static keyword does not used before the implementation of the function.

All these functions can now be used in the program without creating an object. As in the cases of defining typedefs or nested classes, unlike namespaces, you cannot use using-directives or using-declarations:

std::cout << Math::sqr(2) << std::endl;        // 4
std::cout << Math::cube(3) << std::endl;       // 27
std::cout << Math::reciprocal(4) << std::endl; // 0.25
std::cout << Math::factorial(5) << std::endl;  // 120

To reduce the probability of errors due to improper use, it is possible to prohibit the creation of objects of such classes. In C++, this can be done by placing a single constructor in the private part:

class Math
{
private:
    Math() { }
public:
    // ..
};

Now trying to create an object of the Math class will result in a syntax error.

Math m; // syntax error

Starting with C++17, static data can be declared with the inline modifier. Such data members are not declared, but defined in the class body and can be initialized with initial values. Static data members with the inline modifier do not need to be defined outside the class body.

class InlineStatic 
{ 
public:
    static inline int n = 10; 
};

Static data members with the const modifier are actually constants whose values can be specified directly in the class body, or outside the class body:

class StaticConstants
{
public:
    const static int m = 1;
    const static int k;
};

const int StaticConstants::k = 3;

2.6 Class Friends

We can define functions or classes to be friends of a class to allow them direct access to its private and protected members. Friends can be declared in any part of a class body (public, private, or protected). It brings no influence on visibility of class friends. Friends are not a part of a class scope. They do not get this pointer.

In order to easily identify friends, it is often advisable to declare classes in advance. In our case, if we don't want to define a class Capital as a nested class of class Country, we can create the class Capital separately and specify that it has a friend – class Country. Thanks to this, the private data of the Capital class can be used in the Capital class methods, but the object (data member) must be created separately:

class Capital
{
    friend class Country; // class declared
private:
    char name[20] = {};
    int population = 0;
};

class Country
{
private:
    // ...
    Capital capital;
public:
    // ...
    const char* getCapitalName() const { return capital.name; }
    void  setCapitalName(const char* name) { strcpy(capital.name, name); };
    int   getCapitalPopulation() const { return capital.population; }
    void setCapitalPopulation(int population) { capital.population = population; }
};

If a class Country is specified as a friend in the Capital class definition, the methods of the Country class will have access to the private members, but not vice versa. Sometimes you have to realize mutual friendship. Then one of the classes must be declared first, and then specified as a friend.

For greater security, individual function elements, rather than the entire class, can be defined as class friends. Such functions must be implemented outside the class in which they are declared. Also, the Country class definition must now precede the definition of Capital:

class Capital;

class Country
{
private:
    char    name[40] = {};
    double  area = 1;
    int     population = 0;
    Capital *pCapital;
public:
    Country();
    ~Country();
    // ...
    const char* getCapitalName() const;
    void  setCapitalName(const char* name);
    int   getCapitalPopulation() const;
    void setCapitalPopulation(int population);
};

class Capital
{
    friend const char* Country::getCapitalName() const;
    friend void  Country::setCapitalName(const char* name);
    friend int   Country::getCapitalPopulation() const;
    friend void Country::setCapitalPopulation(int population);

private:
    char name[20] = {};
    int population = 0;
};

Country::Country()
{
    pCapital = new Capital();
}

Country::~Country()
{
    delete pCapital;
}

const char* Country::getCapitalName() const 
{ 
    return pCapital->name;
}

void Country::setCapitalName(const char* name) 
{ 
    strcpy(pCapital->name, name);
};

int Country::getCapitalPopulation() const
{ 
    return pCapital->population;
}

void Country::setCapitalPopulation(int population)
{
    pCapital->population = population;
}

You can also define as friend an ordinary function that is not a member function of another class. For example, you can create a printCapital() function whose argument is a reference to an object of Capital class:

class Capital
{
    friend void printCapital(const Capital& capital);
    friend class Country;
private:
    char name[20] = {};
    int population = 0;
};

void printCapital(const Capital& capital)
{
    std::cout << capital.name << " " << capital.population << std::endl;
}

Friend functions can be defined in a class body. They have implicit inline modifier:

class Capital
{
    friend void printCapital(const Capital& capital)
    {
        std::cout << capital.name << " " << capital.population << std::endl;
    }
    friend class Country;
private:
    char name[20] = {};
    int population = 0;
};

Also, the entire friend class can be implemented in the body of the class.

If X is a friend of Y, Y is a friend of Z, X is not a friend of Z by default. The friendship cannot be inherited.

2.7 Operator Overloading

When designing a class, you can define a set of operations that can be performed on objects. The following rules constrain how overloaded operators are implemented.

  • You cannot define new operators, such as ** or <>.
  • You cannot redefine the meaning of operators when applied to built-in data types.
  • Operator priorities cannot be changed.
  • A certain number of operands cannot be changed.
  • If an operator can be used as either a unary or a binary operator (&, *, +, and -), you can overload each use separately.

All overloaded operators except assignment are inherited by derived classes.

Operators

  • :: (scope resolution),
  • . (member selection),
  • .* (member selection through pointer to function),
  • ? : (conditional operator)

cannot be overloaded.

Defining an operation for a class, structure, or enumeration object is done using a so-called operator function. The name of the function begins with the keyword operator and ends with the operator itself.

Operator function must either be a non-static class member function or a global function. A global function must take at least one argument that is of user defined type or that is a reference to such type.

Suppose an Integer class is created:

class Integer
{
public:
    int n;
    Integer(int n) { this->n = n; }
};

An operator function is also defined in the global scope:

Integer operator+(Integer i1, Integer i2)
{
    return Integer(i1.n + i2.n);
}

You can now apply the + operator to objects of type Integer:

void main()
{
    Integer k = 10;
    Integer m = 20;
    Integer sum = k + m;
    cout << sum.n;
}

The previous example violated the principle of encapsulation. If we change the class description and place n in the private section, the operator function can be declared as a class friend:

class Integer
{
    friend Integer operator+(Integer i1, Integer i2);
private:
    int n;
public:
    Integer(int n) { this->n = n; }
    int getN() const  { return n; }
};

Integer operator+(Integer i1, Integer i2)
{
    return Integer(i1.n + i2.n);
}

void main()
{
    Integer k = 10;
    Integer m = 20;
    Integer sum = k + m;
    cout << sum.getN();
}

The function can also be fully implemented in the class body:

class Integer
{
    friend Integer operator+(Integer i1, Integer i2)
    {
        return Integer(i1.n + i2.n);
    }
private:
   ...
};

Keep in mind that the function remains global and does not a part of the Integer class scope.

Operator functions can be overloaded if they can be distinguished on the list of arguments. Different combinations of operand types must be implemented separately, for example:

class Integer
{
    friend Integer operator+(Integer i1, Integer i2);
    friend Integer operator+(int i1, Integer i2);
    friend Integer operator+(Integer i1, int i2);
    ...
}

Overloaded operators cannot have default arguments.

If an operator function is a member function, the number of its parameters must be 1 less than the number of operands of the operation being overloaded, because in this case the first operand is the object itself to which this points.

Some operators

  • = (assignment),
  • [] (subscripting),
  • () (function call),
  • -> (getting a member by pointer)

can be overloaded using member functions only.

You can invoke operator functions explicitly:

Integer x = 1, x1 = 2;
x = operator+(x, x1);

Input and output operators (>> and <<), cannot be overloaded using member functions. The first arguments of operator functions are references to stream objects. The second argument is the reference to an object of user defined type. Operator functions must return reference to stream objects.

ostream& operator<<(ostream& out, const Integer& x);
istream& operator>>(istream& in, Integer& x);

The following functions overload prefix ++ and -- operators

Integer& operator++();
Integer& operator--(); 

The following auctions overload postfix ++ and -- operators. Argument of functions is not used anyway. It allows distinguishing prefix and postfix operators:

Integer operator++(int);
Integer operator--(int); 

Overloaded assignment operator is required in cases when the elementwise copying objects results in an error. If there is a need in a copy constructor or destructor, we should also overload the assignment operator.

You can overload a subscript operator "[]". There is no restriction on the type of parameter or the return value of the function. But there can be the only parameter.

Overloading of function call operators must be implemented using a member function. The arbitrary number of parameters of arbitrary types is allowed. For example,

class Integer
{
    ...
public:
    Integer(int n) { this->n = n; }

    int operator()() { return n; }
    int operator()(int a, int b, int c) { return a + b + c; }
};

void main()
{
    Integer k = 10;
    Integer m = 20;
    Integer sum = k + m;
    cout << sum() << endl; // 30
    cout << sum(1, 2, 3);  // 6
}

There is a special kind of operator functions, so-called type cast operator function, which allows implicit type conversion. This operation should be implemented as a member function and in general case it is the following:

X::operator T(); // T is type name

You cannot set resulting type for such operator function. You cannot specify a list of formal parameters. Here is an example of a type cast operation:

class Integer
{
    int i;
public:
    operator int() { return i; }
    . . .
};

. . .

Integer m = 1;
int k = m; // we use the value of i
int n = m + k;

2.8 Exception Handling

Very often a function of a program in which a certain error occurs is not able to correct this error, because the context of calling this function is unknown. The error must be passed to the part of the program where it can be processed.

The following requirements for the notification mechanism about possible errors can be defined:

  • prevent the possibility of ignoring the error;
  • give the programmer the ability to respond flexibly to error.

Traditional programming tools offer possible solutions to this problem:

  • Termination of the program. Such approach is not always appropriate, for example, when it is enough to repeat user input, or the error can be corrected automatically.
  • Return the error code from function. Such a code can be accidentally ignored, which can lead to fatal consequences. Also, it sometimes makes functions awkward to use by returning an error code instead of the main result.
  • Change the value of a global variable. Such a change is even easier to accidentally ignore. However, the code should contain numerically redundant checks.

The exception throwing and handling mechanism provides a solution to this problem.

An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions. The mechanism of throwing and handling exceptions allows transferring information about the error from place of origin to the place where this error can be handled .

Exception mechanism is present in all modern languages of object-oriented programming.

The exception throwing and handling mechanism provides a solution to this problem.

A program throws an exception by executing a throw statement. For example,

double someFunc(double value)
{
    if (value == 0)               // Not allowed
        throw "Division by Zero"; // Exception of type char*
    return 1 / value;
}

A throw expression is similar to a return statement. A throw expression consists of the throw keyword and an expression. The expression type determines type of exception. There is no relation with a function's recurring type.

Previous versions of C++ supported the ability to add a list of potential exceptions to the header function. This list consisted of the throw keyword and a list of possible types of exceptions in parentheses. For example:

double f(double x) throw (int, char*) // worked in previous versions
{
    . . .
}

If the function could not throw exceptions at all, it was determined by an empty list:

bool g(int x) throw() // worked in previous versions
{
    . . .
}

Newer versions of C++ do not support exception lists, and since C++20 this construct has been removed from the language syntax. To indicate that a function cannot throw exceptions, use the noexcept keyword (starting with version C++11):

void g(int x) noexcept
{
}

or

void g(int x) noexcept(true)
{
}

When the exception was thrown, the flow of control leaves current block and destroys the local variables, then it leaves the outer block (function) until it meets appropriate exception handler. That looks like execution of a sequence of return statements, each of which returns the same object. This process is called stack unwinding.

The try block contains code that might throw an exception. For example

try
{
    double x = 0;
    cout << someFunc(x); // may throw an exceptions
}

A try block is always followed by a sequence of one or more catch statements, or handlers, each of which handles a different type of exception. A catch block is a series of statements, each of which begins with the word catch, followed by an exception type in parentheses, followed by an opening brace, and ending with a closing brace. If you want your handler to catch all exceptions, you use the special form catch (...). This tells the exception handling system that the handler should be invoked for any exception. For example

try 
{
    double x = 0;
    cout << someFunc(x); // may throw exceptions
}   
catch (char*) 
{
    // handles exceptions of type char* 
}
catch (int)
{
    // handles exceptions of type int
}
catch (...)
{
    // handle other exceptions
}

To use the data returned from a point of throwing you may declare a variable of an exception type:

try
{
    double x = 0;
    cout << someFunc(x); // may throw exception
}   
catch (char* c) 
{
    cout << c; // output "Division by Zero" 
}

In some cases, an exception handler may process an exception, then either rethrow the same exception or throw another exception. If the handler wants to rethrow the current exception, it can just use the throw statement with no parameters. This instructs the compiler to take the current exception object and throw it again. For example:

catch (char* c) 
{
    // local handling for the exception
    throw;  // rethrow the exception 
}

You can use fundamental types for exception objects in throw expression. You can also create new classes for exception objects. This approach is more efficient because of exception recognition mechanism. Such classes can be empty:

class Not_Found {};
class Bad_Data {}; 

Classes representing exceptions can be defined as inner classes of classes those throw exceptions:

class MyClass 
{
public: 
    class Not_Found {};
    class Bad_Data {};  
    void f(int i)
    {
        if (i < 0) // Creating a temporary object
            throw Bad_Data();   	
    }
};

void main() 
{
    try 
    {
        MyClass m;
        m.f(-2);
    }
    catch (MyClass::Bad_Data&) 
    {
        //...
    }
    catch (MyClass::Not_Found&) 
    {
        //...
    }
}

To improve efficiency, object-type exceptions are described in catch blocks as references.

Fully fledged exception class with data members and functions can be used for transferring data to exception handle:

class MyClass
{
public: 
    class Bad_Data 
    {
        int bad_value;
    public:
        Bad_Data(int value) : bad_value(value) {}
        int getBadValue() const { return bad_value; }
    };  
    void f(int i) 
    {
        if (i < 0) 
            throw Bad_Data(i);   	
    }
};

void main()
{
    try 
    {
        MyClass m;
        m.f(-2);
    }
    catch (MyClass::Bad_Data& b) 
    {
        cout << "Bad value: " << b.getBadValue();
    }
}

2.9 Class Composition

You can place objects of class types into other classes. This is called class composition.

Class constructors whose objects are placed in an outer class are called invoked before the host class constructor. Constructors without parameters are called automatically. Constructors are always called in the order in which objects are declared in the class. Destructors are called in reverse order.

Sometimes we need to invoke non-default constructors of class members. Arguments for such constructors can be passed using an initializer list. For example:

class X
{
public:
    X(int j) { ... }
    . . .
};

class Y
{
    X x;
public:
    Y(int k) : x(k) { ... }
    . . .
};

3 Sample Programs

3.1 Class for Representing a Point on the Screen

Our goal is to create a class to describe a point on the screen. The class must meet the following requirements:

  • have a constructor without parameters;
  • have a constructor with two parameters;
  • contain data elements of the type int to represent the coordinates of a point on the screen;
  • implement public data access functions (setters and getters).

To calculate the distance, we'll create a separate function that we will define as the second class. We can create a static class function to calculate the second power.

The source code will be as follows:

#include <iostream>
#include <cmath>

// Class for representing a point on the screen
class Point2D
{
    friend double distance(Point2D, Point2D);
private:
    int x, y; // coordinates of the point
public:
    // A constructor without parameters calls another constructor:
    Point2D() : Point2D(0, 0) { }
    // The constructor initializes the data members:
    Point2D(int x, int y) : x(x), y(y) { }
    int getX() { return x; }
    void setX(int x) { this->x = x; }
    int getY() { return y; }
    void setY(int y) { this->y = y; }
    // Auxiliary static function for calculating the second power:
    static double sqr(double x) {  return x * x; }
};

// Calculate the distance between two points
// The friend function has access to the data members of the class
double distance(Point2D p1, Point2D p2)
{
    return std::sqrt(Point2D::sqr(p1.x - p2.x) + Point2D::sqr(p1.y - p2.y));
}

int main()
{
    // We create the first point using the constructor with parameters:
    Point2D p1(1, 2);
    std::cout << p1.getX() << " " << p1.getY() << "\n";
    // For the second point, we indicate the coordinates with setters:
    Point2D p2;
    p2.setX(4);
    p2.setY(6);
    std::cout << p2.getX() << " " << p2.getY() << "\n";
    // Calculate the distance:
    std::cout << distance(p1, p2);
}
      

3.2 Class for Representation of Mathematical Vector

Suppose we need to create a class for representation of mathematical vector in two-dimensional space. We can describe the vector using two coordinates: x and y. Typical operations on vector are finding sum, multiplication by scalars, getting scalar product. For reasons of convenience of working with the objects of our class we'll overload appropriate operators. Operator functions can be implemented as friend functions of our class. The C++ syntax allows us to place the implementation of these functions in the class body. We also need to overload input and output operations.

The checking class capabilities is performed the main() function. The program will be is follows:

#include <iostream>
using std::cin;
using std::cout;
using std::endl;
using std::istream;
using std::ostream;

class Vector {
    friend istream& operator>>(istream& in, Vector& v)  { return in >> v.x >> v.y; }
    friend ostream& operator<<(ostream& out, const Vector& v) { return out << "x=" << v.x << " y=" << v.y; }
    friend Vector operator+(Vector v1, Vector v2) { return Vector(v1.x + v2.x, v1.y + v2.y); }
    friend Vector operator*(double k, Vector v) { return Vector(v.x * k, v.y * k); }
    friend Vector operator*(Vector v, double k) { return operator*(k, v); }
    friend double operator*(Vector v1, Vector v2) { return v1.x * v2.x + v1.y * v2.y; }
private:
    double x, y;
public:
    Vector() { x = y = 0; }
    Vector(double x, double y);
    double getX()       { return x; }
    void setX(double x) { this->x = x; }
    double getY()       { return y; }
    void setY(double y) { this->y = y; }
};

Vector::Vector(double x, double y) {
    this->x = x;
    this->y = y;
}

void main()
{
    Vector v1, v2;
    cout << "Input first vector: ";
    cin >> v1; // For example, 1 2
    cout << "Input second vector: ";
    cin >> v2; // For example, 3 4
    cout << v1 + v2 << endl; // x=4 y=6
    cout << v1 * v2 << endl; // 11
    cout << v1 * 2 << endl;  // x=2 y=4
    cout << 3 * v2 << endl;  // x=9 y=12
}

This example shows that the scalar multiplication operator should be individually implemented for different locations of operands. Constructor with parameters implemented outside of class body, because its implementation requires more than one line.

3.3 Classes for Representing the City and Country

In the previous laboratory training, an example of creating a structure to represent the city, as well as working with an array of structures, was given. Now, using the previous solutions, we can create classes to represent the city and country, and the "Country" object should store an array of cities located in this country. For simplicity, let's assume that we will store no more than 100 cities in our array.

Note: this assumption will be removed in later stages of development of this solution.

The City class will have the following data members:

  • name of the city;
  • pointer to the country in which the city is located;
  • name of the region;
  • population of the city.

It is advisable to place the names of the city and region in the free store. A class must provide a constructor with no parameters and a constructor with four parameters. Memory for data will be freed from the free store in the destructor. In addition to setters and getters, an overloaded assignment operator should be implemented to avoid errors with the free store. We implement the operator function of writing data about the city to the stream as an external friend function. To implement output, it is advisable to use the sprintf() function, which formats a string (similarly to printf(), which outputs a string to the console window). In order to use the sprintf() function you should add the definition #define _CRT_SECURE_NO_WARNINGS.

To ensure that cities are sorted by population, comparison operator for two cities should be overloaded. Such a function can be implemented as an external one. For our algorithm, it is sufficient to redefine the operator>() function. Other comparison operations may be useful for other algorithms.

The following data members will be created in the Country class:

  • name of country;
  • an array of city directories;
  • the actual number of pointers in the array.

We will create an array of pointers to cities of the maximum length, and we will use the logical (actual) length, which determines how many pointers were actually written into the array. This approach is not universal, but it will ensure the best efficiency of the program.

We need to create a getter and setter for the country name. Instead of a getter for an array of pointers, we can overload the operator of getting an element by index (subscript operator). The setCities() function will set data about all cities. In addition, a city sorting function and an external output operator function are required.

In the main() function, we will create a country object and an array of cities (via a helper function), and then demonstrate sorting and data output. The source code will be as follows:

#define _CRT_SECURE_NO_WARNINGS
#include <cstring>
#include <iostream>

using std::strlen;
using std::strcpy;
using std::cout;
using std::endl;

const int MAX_COUNT = 100; // Maximum number of cities

// The class must be declared so that the pointer can be created:
class Country;

// Class to represent the city
class City
{
    // Overloaded operator to output to a stream
    friend std::ostream& operator<<(std::ostream& out, const City& city);
private:
    char *name = nullptr;      // city name
    Country *country = nullptr;// pointer to the location country
    char *region = nullptr;    // the name of the region
    int population = 0;        // population
public:
    // Constructors:
    City() { }
    City(const char* name, Country* country, const char* region, int population);
    City(const City& city);

    ~City(); // destructor

    // Getters:
    const char* getName() const { return name; }
    Country* getCountry() const { return country; }
    const char* getRegion() const { return region; }
    int getPopulation() const { return population; }

    // Setters:
    void setName(const char* name);
    void setRegion(const char* region);
    void setCountry(Country* country) { this->country = country; }
    void setPopulation(int population) { this->population = population; }

    // Overloaded assignment operator
    const City& operator=(const City& city);
};

// Constructor with parameters is implemented by calling setters
City::City(const char* name, Country* country, const char* region, int population)
{
    setName(name);
    setCountry(country);
    setRegion(region);
    setPopulation(population);
}

// Copy constructor
City::City(const City& city)
{
    name = new char[strlen(city.name) + 1];
    strcpy(name, city.name);
    region = new char[strlen(city.region) + 1];
    strcpy(region, city.region);
    country = city.country;
    population = city.population;
}

// Remove city and region names from memory (if arrays were created)
City::~City() 
{
    if (name != nullptr)
    {
        delete[] name;
    }
    if (region != nullptr)
    {
        delete[] region;
    }
}

// Remove the previous name of the city, create a new array and put the new name
void City::setName(const char* name)
{
    if (this->name != nullptr)
    {
        delete[] this->name;
    }
    this->name = new char[strlen(name) + 1];
    strcpy(this->name, name);
}

// Remove the previous name of the region, create a new array and put the new name
void City::setRegion(const char* region)
{
    if (this->region != nullptr)
    {
        delete[] this->region;
    }
    this->region = new char[strlen(region) + 1];
    strcpy(this->region, region);
}

// We implement an overloaded assignment operator by calling setters
const City& City::operator=(const City& city)
{
    if (&city != this)
    {
        setName(city.name);
        setCountry(city.country);
        setRegion(city.region);
        setPopulation(city.population);
    }
    return *this;
}

// Overloaded operation of comparing two cities
bool operator>(const City& c1, const City& c2)
{
    return c1.getPopulation() > c2.getPopulation();
}

// Class to represent the country
class Country
{
    // Overloaded operator to output to a stream
    friend std::ostream& operator<<(std::ostream& out, const Country& country)
    {
        out << country.name << endl;
        for (int i = 0; i < country.count; i++)
        {
            out << *(country.cities[i]) << endl;
        }
        out << endl;
        return out;
    }
private:
    char name[40];                 // name of country
    City *cities[MAX_COUNT] = { }; // array of pointers to cities
    int count = 0;                 // number of pointers in the array
public:
    // Constructors:
    Country() { }
    Country(const char* name) { setName(name); }

    const char* getName() const { return name; } // getter

    // Overloaded operator for getting array items
    City* operator[](int index) const { return cities[index]; }

    // Setters:
    void setName(const char* name) { strcpy(this->name, name); }
    void setCities(City* cities[], int  count);

    void sortByPopulation(); // Sort by population
};

// Fill the array of cities
void Country::setCities(City* cities[], int  count)
{
    this->count = count;
    for (int i = 0; i < count; i++)
    {
        this->cities[i] = cities[i];
        this->cities[i]->setCountry(this);
    }
}

// Sort by population
void Country::sortByPopulation()
{
    bool mustSort = true; // repeat sorting 
                          // if mustSort is true
    do
    {
        mustSort = false;
        for (int i = 0; i < count - 1; i++)
        {
            // Dereferencing 
            // because we can compare objects, not pointers:
            if (*(cities[i]) > *(cities[i + 1]))
                // Exchange items
            {
                City* temp = cities[i];
                cities[i] = cities[i + 1];
                cities[i + 1] = temp;
                mustSort = true;
            }
        }
    } while (mustSort);
}

// Overloaded operator for output to a stream
std::ostream& operator<<(std::ostream& out, const City& city)
{
    char buffer[300];
    sprintf(buffer, "City: %s.\tCountry: %s.\tRegion: %s.\tPopulation: %d",
        city.name, city.country->getName(), city.region, city.population);
    out << buffer;
    return out;
}

// Helper function for filling an array of pointers to cities
void createCities(City *cities[])
{
    cities[0] = new City("Kharkiv", nullptr, "Kharkiv region", 1421125);
    cities[1] = new City("Poltava", nullptr, "Poltava region", 284942);
    cities[2] = new City("Lozova", nullptr, "Kharkiv region", 54618);
    cities[3] = new City("Sumy", nullptr, "Sumy region", 264753);
}

int main()
{
    const int realCount = 4;     // we work with four cities
    City *cities[realCount];     // create an array of pointers to cities   
    createCities(cities);        // fill the array
    Country country = "Ukraine"; // create the Country object,
                                 // call the constructor with one parameter
    country.setCities(cities, realCount); // copy the cities to the Country object
    cout << country << endl;     // output all data
    cout << *country[0] << endl; // display information about the city by index
    country.sortByPopulation();  // sort
    cout << country << endl;     // output all data
    // Remove cities stored in array of pointers to cities
    for (int i = 0; i < realCount; i++)
    {
        delete cities[i];
    }
    return 0;
}
      

The disadvantage of the program is the lack of testing of some functions. You can correct this.

3.4 Class for Presentation of One-Dimensional Array

The following example shows the creation and usage of a class that represents one-dimensional array with overloading necessary operators. Array items are allocated in a free store.

#include <iostream>

using std::cin;
using std::cout;
using std::endl;
using std::istream;
using std::ostream;

// A class for representing a one-dimensional array
class IntArray
{
    // Friend operator functions for output and input:
    friend ostream& operator <<(ostream& out, const IntArray& a);
    friend istream& operator >>(istream& in, IntArray& a);
private:
    int* pa = nullptr; // pointer to future array
    int  size = 0;     // current array size
public:
    // Nested class for creating an exception object
    class OutOfBounds
    {
        int index; // index out of range
    public:
        OutOfBounds(int i) : index(i) { } // constructor
        int getIndex() const { return index; } // getter for index
    };

    // Constructors:
    IntArray() { }
    IntArray(int n) { pa = new int[size = n]; }
    IntArray(IntArray& arr);

    ~IntArray(); // destructor
    void addElem(int elem); // function to add an element
    int& operator [](int index); // read and write access to elements

   // Overloaded operators:
    const IntArray& operator =(const IntArray& a);
    bool operator ==(const IntArray& a) const;

    int getSize() const { return size; } // returns the number of array elements
};

// Overloaded operator of writing to stream
ostream& operator <<(ostream& out, const IntArray& a)
{
    for (int i = 0; i < a.size; i++)
    {
        out << a.pa[i] << ' ';
    }
    return out;
}

// Overloaded operator of reading from stream
istream& operator >>(istream& in, IntArray& a)
{
    for (int i = 0; i < a.size; i++)
    {
        in >> a.pa[i];
    }
    return in;
}

// Copy constructor
IntArray::IntArray(IntArray& arr)
{
    size = arr.size;
    pa = new int[size];
    for (int i = 0; i < size; i++)
    {
        pa[i] = arr.pa[i];
    }
}

// Destructor
IntArray::~IntArray()
{
    if (pa != nullptr)
    {
        delete[] pa;
    }
}

// Adds an item. Allocates an array in a new place
void IntArray::addElem(int elem)
{
    int* temp = new int[size + 1];
    if (pa != nullptr)
    {
        for (int i = 0; i < size; i++)
        {
            temp[i] = pa[i];
        }
        delete[] pa;
    }
    pa = temp;
    pa[size] = elem;
    size++;
}

// Provides read and write access to elements
// Throws an OutOfBounds exception in case of a wrong index
int& IntArray:: operator [](int index)
{
    if (index < 0 || index >= size)
    {
        throw OutOfBounds(index);
    }
    return pa[index];
}

// Overloaded assignment operator
const IntArray& IntArray:: operator =(const IntArray& a)
{
    if (&a != this)
    {
        if (pa != nullptr)
        {
            delete[] pa;
        }
        size = a.size;
        pa = new int[size];
        for (int i = 0; i < size; i++)
        {
            pa[i] = a.pa[i];
        }
    }
    return *this;
}

// Overloaded array comparison operator
bool IntArray:: operator ==(const IntArray& a) const
{
    if (&a == this)
    {
        return true;
    }
    if (size != a.size)
    {
        return false;
    }
    for (int i = 0; i < size; i++)
    {
        if (pa[i] != a.pa[i])
        {

            return false;
        }
    }
    return true;
}

// Global function for finding the minimum array item
// The function does not have direct access to the data and uses
// overloaded operators and member functions
int getMin(IntArray a) // call the copy constructor
{
    int min = a[0];
    for (int i = 1; i < a.getSize(); i++)
    {
        if (min > a[i])
        {
            min = a[i];
        }
    }
    return min;
}

void main()
{
    setlocale(LC_ALL, "UKRAINIAN");
    IntArray a(2); // An array of two items
    cout << "Enter two array elements: ";
    cin >> a;
    cout << "Array items: " << a << endl;
    a.addElem(12);
    cout << "Adding element" << endl;
    cout << "Array items: " << a << endl;
    try
    {
        a[1] = 2;   // changed
        a[10] = 35; // wrong index
    }
    catch (IntArray::OutOfBounds& e)
    {
        cout << "Incorrect index: " << e.getIndex() << endl;
    }
    cout << "New items: " << a << endl;
    IntArray b; // created a new array
    b = a; // copying items
    if (a == b)
    {
        cout << "Arrays a and b are equal" << endl;
    }
    else
    {
        cout << "Arrays a and b are different";
    }
    cout << "Minimum element: " << getMin(a) << endl;
}

As you can see from the example, work with an object outside of class is very similar to working with a conventional array.

3.5 Counting Created Objects

Suppose we need to perform counting objects of a certain class. We can create a counter in the form of a static data member that holds the number of objects present in memory. The invocation of a constructor provides increasing the counter, and the invocation of a destructor provides decreasing. The program can be as follows:

#include <iostream>

using std::cout;
using std::endl; 

class ObjectCount
{
private:
    static int count;
public:
    static int getCount()
    {
        return count;
    }
    ObjectCount()
    {
        count++;
    }
    ~ObjectCount()
    {
        count--;
    }
};

// Static data member must be defined and initialized outside of class:
int ObjectCount::count = 0;

void main()
{
    ObjectCount c1;
    cout << c1.getCount() << endl;  // 1
    ObjectCount *p1 = &c1; // copy the address, constructor is not called
    cout << p1->getCount() << endl; // 1
    ObjectCount *p2 = new ObjectCount();
    cout << p2->getCount() << endl; // 2
    delete p2;
    cout << p2->getCount() << endl; // 1
}

As we can see from the results of the main() function, the getCount() function can be called for the object even after it was removed from the heap. This is due to the fact that the only object type or pointer type (but not object itself) is important for compiler if the static function calls. A more correct is function call via class name:

void main()
{
    ObjectCount c1;
    cout << ObjectCount::getCount() << endl; // 1
    ObjectCount *p1 = &c1;
    cout << ObjectCount::getCount() << endl; // 1
    ObjectCount *p2 = new ObjectCount();
    cout << ObjectCount::getCount() << endl; // 2
    delete p2;
    cout << ObjectCount::getCount() << endl; // 1
}

4 Exercises

  1. Create a class "A pair of strings " with the necessary constructors and access functions.
  2. Create a class "Complex number" with the necessary constructors and access functions. Overload operators + , -, *, / and input / output operations.
  3. Create a class "Vector in N-dimensional space" with the necessary constructors and access functions. Overload operators + , -, *, / and input / output operations.
  4. Create a class "Quadratic equation" with data members (coefficients of the equation), necessary constructors and access functions. Implement a function of finding the roots.

5 Quiz

  1. What is the class and what is its content?
  2. What is the concept of encapsulation?
  3. What access levels to the class members supports C++?
  4. What are access functions?
  5. Is it possible to implement member functions outside the class body?
  6. How to create a constant member function?
  7. What is a this pointer and what its usage?
  8. What is constructor and what mechanism its call?
  9. How many constructors without parameters can be created in the same class?
  10. How to create a class with no constructors?
  11. What is the copy constructor and when it should be defined?
  12. What is destructor and what mechanism its call?
  13. How many destructors can be defined in a class?
  14. What determines the class scope?
  15. Is it possible to create classes within other classes?
  16. What is a static class member?
  17. What are the limitations of static functions implementation?
  18. What class friends and what they are?
  19. Are class friends members of a class scope?
  20. What is the use of operator overloading?
  21. What operators cannot be overloaded?
  22. When the operator should only overload using the member function?
  23. When the operator should only overload using the global function?
  24. How to overload type casting operator?
  25. What is the purpose of exception handling mechanism?
  26. How to create an exception object?
  27. What types of exception objects can be used?
  28. Is it possible to use the main function result if an exception was thrown?
  29. How to catch and handle an exception?
  30. How to create a block that handles all exceptions?

 

up