Kurs podstawy programowania UMCS, zdjęcie laptopa.

PP – Laboratorium 13

Wejściówka

Zaprojektuj strukturę Point, która posiada dwa pola typu zmiennoprzecinkowego x, y. Następnie napisz funkcję w języku C++, która przyjmuje trzy argumenty: punkt typu Point oraz dwie liczby rzeczywiste, będące współrzędnymi kierunkową i przecięć funkcji liniowej. Funkcja powinna zwrócić true, jeśli punkt należy do prostej, bądź false w przeciwnym wypadku.

Rozwiązanie:
struct Point {
    float x, y;
};

bool f(const Point &p, float a, float b) {
    return !(a * p.x + b - p.y);
}

Zadanie 1 – metody i konstruktory

Zaprojektuj klasę Animal, która posiada dwa pola: zmienną typu wyliczeniowego AnimalType type oraz tablicę będącą maksymalnie 10 znakową nazwą pupila – name. Dodatkowo klasa powinna zawierać metodę say_something(int n). Metoda say_something w zależności od typu zwierzęcia powinna wyświetlić odpowiedni komunikat np. dla kota "miał" oraz powtórzyć go n razy, gdzie n zostało przekazane w argumencie metody. Napisz program w języku C++, który przetestuje działanie naszej klasy.

Rozwiazanie:
#include <iostream>
#include <cstring>

enum AnimalType {PIES, KOT};

class Animal {
private:
    char name[11];
    AnimalType type;
public:
    void say_something(int n);
    const char *get_name();
    Animal() : name("unknown"), type(KOT) {}
    Animal(const char *name, AnimalType type);
};

void Animal::say_something(int n) {
    for(int i = 0; i < n; ++i)
        std::cout << (type == KOT ? "mial\n" : "hal\n");
}

const char *Animal::get_name() {
    return this->name;
}

Animal::Animal(const char *name, AnimalType type) {
    memcpy(this->name, name, strlen(name));
    this->type = type;
}

int main() {
    Animal *kotek = new Animal("Zbigniew", KOT);
    Animal *piesek = new Animal("Zbyszek", PIES);

    std::cout << kotek->get_name() << std::endl;
    kotek->say_something(10);

    std::cout << piesek->get_name() << std::endl;
    piesek->say_something(5);

    return 0;
}
Omówienie:

Aby rozwiązać to zadanie moglibyśmy wykorzystać wskaźniki na funkcję, jednakże zdecydowanie lepszym rozwiązaniem jest wykorzystanie możliwości posiadania funkcji w klasie: metod, konstruktorów itd.

Cała znajomość programowania, która poznaliście do tej pory sprowadzała się do tzw. programowanie strukturalnego. Wszystkie implementowane funkcje miały zasięg globalny, innymi słowy były widoczne z każdego miejsca w naszym programie. Wymagało to mi. unikatowych nazw funkcji. Ponadto bardzo często implementowaliśmy funkcje pomocnicze, które powinny być widoczne tylko dla innych funkcji lub obiektów a użytkownik nie powinien mieć do nich dostępu, niestety tak nie było.

Metody to funkcje składowe zadeklarowane w klasach i strukturach. Metody tak, jak atrybuty klas i struktur, mogą mieć jedne z trzech praw dostępu: private, protected, public. Metody publiczne mogą być wywołane z poza klasy, zaś metody prywatne mogą być wywołane tylko i wyłącznie wewnątrz klasy. Tworzenie metod prywatnych jest bardzo wygodne, ponieważ umożliwia nam dzielenie skomplikowanej, zazwyczaj publicznej metody na mniejsze elementy, które przy późniejszej analizie są bardziej czytelne i łatwiejsze do modyfikacji.

Definicję metod można przenieść poza definicję klasy, dzięki czemu oddzielamy wizualny interfejs klasy od jej implementacji. Warto zaznaczyć, że metody definiowane wewnątrz klasy automatycznie stają się funkcjami typu inline. Aby temu zaradzić, ten sam kod co poprzednio można zapisać następująco:

class Animal {
private:
    char name[11];
    AnimalType type;
public:
    void say_something(int n);
    const char *get_name();
    Animal() : name("unknown"), type(KOT) {}
    Animal(const char *name, AnimalType type);
};

void Animal::say_something(int n) {
    for(int i = 0; i < n; ++i)
        std::cout << (type == KOT ? "mial\n" : "hal\n");
}

const char *Animal::get_name() {
    return this->name;
}

Animal::Animal(const char *name, AnimalType type) {
    memcpy(this->name, name, strlen(name));
    this->type = type;
}

W kontekście metod klas (i struktur), należy wspomnieć o wskaźniku this. Wskaźnik this występuje w metodach niestatycznych klas. Ten wskaźnik wskazuje na obiekt, dla którego została wywołana metoda, co umożliwia jawne odwołanie się zarówno do atrybutów, jak i innych metod klasy (instancji klasy).

Aby skorzystać z metod, które są umieszczone wewnątrz klasy, musimy utworzyć sobie najpierw zmienną, a następnie za pomocą utworzonej zmiennej wywołujemy metodę klasy.

Bardzo często, a właściwie prawie zawsze, tworząc obiekt klasy należy zainicjować go początkowymi wartościami. W języku C++, w tym celu używamy konstruktorów.


Konstruktor jest specyficzną metodą, która jest wywoływana zawsze gdy tworzony jest obiekt. Jeśli programista nie utworzy konstruktora dla klasy, kompilator automatycznie utworzy konstruktor, który nic nie będzie robił. Konstruktor nie pojawi się nigdzie w kodzie, jednak będzie on istniał w skompilowanej wersji programu i będzie wywoływany za każdym razem, gdy będzie tworzony obiekt klasy. Jeśli chcemy zmienić domyślne własności konstruktora jaki jest tworzony przez kompilator C++ wystarczy, że utworzymy własny konstruktor dla klasy.

W przeciwieństwie do innych metod konstruktor nie zwraca żadnego typu danych. Ponadto drugą istotną własnością konstruktora jest jego nazwa, która jest taka sama jak nazwa klasy. Do konstruktora można przekazywać parametry, tak samo jak do funkcji. Dodatkowo język C++ umożliwia również tworzenie kilku konstruktorów dla jednej klasy (muszą się one jednak różnić parametrami wejściowymi tak jak w przypadku funkcji).

Gdy tworzymy klasę, wszystkie zmienne jakie są zadeklarowane wewnątrz niej są zainicjowane przypadkowymi wartościami, które w konstruktorze następnie ustawiamy na takie, jakie uważamy za stosowne np.:

Animal::Animal() {
    memcpy(this->name, "unknown", 7);
    this->type=KOT;
}

Takie rozwiązanie jest oczywiście poprawne, niemniej jednak czasami zachodzi potrzeba zainicjowania zmiennej w trakcie tworzenia klasy, a nie po jej utworzeniu. Precyzując, inicjalizowanie składowych nowego obiektu odbywa się zanim obiekt zacznie istnieć. Takie rozwiązanie nazywamy listą inicjalizacyjną konstruktora. Pozornie ciężko jest znaleźć różnice pomiędzy konstruktorem a listą inicjalizacyjną. Wynika to z faktu, że mechanizmy te są ze sobą bardzo ściśle związane, a lista inicjalizacyjna jest rozszerzeniem możliwości konstruktora. Tym samym nie można zdefiniować listy inicjalizacyjnej nie definiując konstruktora w danej klasie. Uzupełniając, lista inicjalizuje składowe podczas ich tworzenia za pomocą odpowiednich konstruktorów, natomiast konstruktor najpierw tworzy składowe za pomocą domyślnych konstruktorów, a następnie przypisuje im odpowiednie wartości za pomocą operatora przypisania. Aby użyć listy inicjalizacyjnej, należy użyć następującego zapisu:

Animal::Animal(): name("unknown"), type(KOT) {}

Powyższy zapis ma kilka bardzo istotnych zalet:
– jest szybszy – brzmi to trochę absurdalnie, ale różnice są znaczne gdy przyjdzie do wykonywania pomiarów czasowych (dla powyższego przykładu nie odczujemy żadnej różnicy, jednakże w przypadku bardziej rozbudowanych, złożonych składowych już tak),
– jest czytelniejszy – programista nie musi analizować zawartości konstruktora, by wiedzieć jaką domyślną wartością zostanie zainicjowana klasa,
– umożliwia inicjowanie zmiennych zdefiniowanych jako stałe,
– umożliwia inicjowanie zmiennych zdefiniowanych jako referencje (i tylko ta metoda umożliwia zainicjować zmienną zadeklarowaną np. tak: int& zmienna;),
– jest metodą stosowaną przy dziedziczeniu klas.
Warto więc od początku wpajać sobie nawyk, który został tu zaprezentowany, ponieważ w przyszłości może to oszczędzić dużo problemów.

Zadanie 2 – destruktory

Uzupełnij poprzedni program o odpowiedni destruktor oraz konstruktor kopiujący.

Rozwiązanie:
#include <iostream>
#include <cstring>

enum AnimalType {PIES, KOT};

class Animal {
private:
    char name[11];
    AnimalType type;
public:
    void say_something(int n);
    const char *get_name();
    Animal() : Animal("unknown", KOT) {}
    Animal(const char *name, AnimalType type);
    Animal(const Animal &animal);
    ~Animal();
};

void Animal::say_something(int n) {
    for(int i = 0; i < n; ++i)
        std::cout << (type == KOT ? "mial\n" : "hal\n");
}

const char *Animal::get_name() {
    return this->name;
}

Animal::Animal(const char *name, AnimalType type) {
    memcpy(this->name, name, strlen(name));
    this->type = type;
}

Animal::Animal(const Animal &animal) {
    memcpy(this->name, animal.name, sizeof(animal.name));
    this->type = animal.type;
}

Animal::~Animal() {
    std::cout << "Destruktor " << name << std::endl;
}

int main() {
    Animal *kotek = new Animal("Zbigniew", KOT);
    Animal *piesek = new Animal("Zbyszek", PIES);
    Animal kotek_cpy = Animal(*kotek);

    std::cout << kotek->get_name() << std::endl;
    kotek->say_something(10);

    std::cout << piesek->get_name() << std::endl;
    piesek->say_something(5);

    delete kotek;
    delete piesek;

    std::cout << kotek_cpy.get_name() << std::endl;

    return 0;
}
Omówienie:

Często jest tak, że podczas „życia” obiektu klasy rezerwujemy pamięć, którą chcielibyśmy zwalniać zawsze przed usunięciem obiektu. Pierwszym wariantem jest pamiętanie o wywołaniu funkcji, która będzie za to odpowiedzialna. Takie podejście jest jednak ryzykowne, ponieważ bardzo łatwo zapomnieć o wywoływaniu funkcji, która będzie zwalniała ewentualną zarezerwowaną dynamicznie pamięć.

Lepszym rozwiązaniem tego problemu jest wykorzystanie destruktorów. Destruktor jest specjalną metodą, która jest wywoływana zawsze tuż przed zniszczeniem (usunięciem) instancji klasy z pamięci. Destruktor, tak samo jak konstruktor nie posiada zwracanego typu. Dodatkowo destruktor zawsze musi być bezparametrowy oraz jest możliwość zdefiniowania tylko i wyłącznie jednego destruktora dla danej klasy. Kolejną charakterystyczną własnością destruktora jest jego nazwa, tak samo jak w przypadku konstruktora, nazwa destruktora jest taka sama jak nazwa klasy, z tym że poprzedza ją znak ~.


Konstruktor kopiujący to konstruktor spełniający specyficzne zadanie. Mianowicie jest on wywołany (jawnie lub niejawnie), gdy występuje inicjalizacja obiektu za pomocą innej instancji tej klasy. Jeżeli nie zaimplementujemy konstruktora kopiującego, kompilator zrobi to automatycznie. Konstruktor taki będzie po prostu tworzył drugą instancję wszystkich pól obiektu.

Kompilator wywołuje konstruktor niejawnie, jeżeli zachodzi potrzeba stworzenia drugiej instancji obiektu np. podczas przekazywania obiektu do funkcji przez wartość. Z drugiej strony możemy go wywołać jawnie w następujący sposób:

Animal kotek("Zbigniew", KOT); //To nie jest konstruktor kopiujący
Animal kotek2(kotek); //To jest konstruktor kopiujący
Animal kotek3 = kotek2; //To również wywołanie konstruktora kopiującego a nie przypisanie.
kotek3 = kotek; //To nie jest wywołanie konstruktora kopiującego a przypisanie.

Jeżeli dokonujemy w instrukcjach inicjujących alokacji pamięci, to nie możemy zdać się na konstruktor kopiujący tworzony niejawnie. Gdy skorzystamy z domyślnego konstruktora kopiującego, to w tak stworzonym obiekcie pole, będące wskaźnikiem, będzie wskazywać na ten sam fragment pamięci, co w obiekcie wzorcowym. Jeżeli nie jest to zamierzony efekt (a zwykle nie jest) trzeba samodzielnie zaimplementować konstruktor kopiujący.

Należy zwrócić uwagę na dwie ostatnie linie powyższego listingu. Konstruktor kopiujący zostanie użyty tylko podczas inicjalizacji nowego obiektu, zaś operator przypisania zostanie użyty wtedy, gdy wartość jednego obiektu ma zostać przypisana innemu.

Aby samemu zaimplementować konstruktor kopiujący należy zadeklarować konstruktor o jednym parametrze, będącym referencją, najlepiej stałą const na obiekt tej samej klasy:

Animal::Animal(const Animal &animal) {
    memcpy(this->name, animal.name, sizeof(animal.name));
    this->type = animal.type;
}

Ostatnim omawianym pojęciem jest delegacja konstruktów (od C++ 11). W przypadku wielu wariantów konstruktorów często zdarza się, że muszą one powielać różne testy poprawności argumentów lub jakieś szczególne operacje konieczne do inicjalizacji obiektu. Niekiedy taki wspólny kod wyciąga się do osobnych prywatnych lub chronionych metod.

Jednakże w C++11 dodano możliwość użycia na liście inicjalizacyjnej innych konstruktorów klasy:

Animal::Animal(AnimalType type) : Animal::Animal("unknown", type) {}

Zadanie 3

Zaliczenie z pewnego przedmiotu wygląda tak, że studenci piszą dwa kolokwia, oba za 50 punktów. Ocena dostateczna przyznawana jest w przedziale (50-60] punktów, i rośnie o połowę oceny co 10 punktów aż do 100. Student, który nie otrzyma zaliczenia, może poprawić jedno z kolokwiów, ale wtedy może otrzymać co najwyżej ocenę dostateczną. Napisz klasę, w której znajduje się: imię i nazwisko studenta będące napisem, wynik pierwszego kolokwium, wynik drugiego kolokwium i wynik poprawy. Zakładamy, że student, który nie otrzyma zaliczenia, zawsze będzie poprawiał kolokwium, które gorzej napisał. Napisz funkcję w języku C++, która przyjmie tablicę takich struktur oraz jej rozmiar, a zwróci średnią ocen (nie punktów) zdobytych z tego przedmiotu.

Rozwiązanie:
#include <iostream>
#include <cstring>

class Student {
public:
    char id[100];
    int result_1, result_2, result_3;
    Student() {}
    Student(const char *name, int result_1, int result_2, int result_3) {
        strcpy(id, name);
        this->result_1 = result_1;
        this->result_2 = result_2;
        this->result_3 = result_3;
    }
    Student(const char *name, int result_1, int result_2) : Student(name, result_1, result_2, 0) {}
};

float function(Student arr[], int n) {
    float avg = 0;
    for(int i = 0; i < n; ++i) {
        int sum = arr[i].result_1 + arr[i].result_2;
        if(sum <= 50) {
            if(arr[i].result_1 < arr[i].result_2)
                sum = arr[i].result_2 + arr[i].result_3;
            else
                sum = arr[i].result_1 + arr[i].result_3;
            avg += sum <= 50 ? 2 : 3;
        } else switch ((sum - 1) / 10) {
            case 9: avg += 5; break;
            case 8: avg += 4.5; break;
            case 7: avg += 4; break;
            case 6: avg += 3.5; break;
            case 5: avg += 3; break;
            default: avg += 2; break;
        }
    }
    return avg / n;
}

int main() {
    Student arr[3];
    arr[0] = Student("Jan Nowak", 40, 4, 30);
    arr[1] = Student("Adam Kowalski", 26, 30);
    arr[2] = Student("Adam Kowalski", 50, 50);
    std::cout << function(arr, 3) << std::endl;
    return 0;
}

Zadanie 4

Napisz klasę prostokąt posiadającą dwie zmienne prywatne i jedną metodę, która zwróci pole prostokąta.

Rozwiązanie:
#include <iostream>

class  Rectangle {
private:
    int x, y;
public:
    int area() {
        return x * y;
    }
    void set_x(int x) { this->x = x; }
    void set_y(int y) { this->y = y; }
    int get_x() { return x; }
    int get_y() { return y; }

    Rectangle(): Rectangle(0, 0) {}
    Rectangle(int x, int y) {
        this->x = x;
        this->y = y;
    }
};


int main() {
    int x, y;
    std::cin >> x >> y;
    //Rectangle rect = Rectangle(x, y);
    Rectangle rect;
    rect.set_x(x);
    rect.set_y(y);
    std::cout << rect.area() << std::endl;
    std::cout << rect.get_x() << " " << rect.get_y() << std::endl;
    return 0;
}
Omówienie:

Enkapsulacja inaczej zwana hermetyzacją (kapsułkowaniem) jest to jedno z głównych i podstawowych założeń programowania obiektowego. Polega na ukrywaniu metod i atrybutów dla klas i funkcji zewnętrznych. Dostęp do nich możliwy jest tylko z wewnątrz klasy, do której należą, lub z klas dziedziczących, czy też klas i funkcji zaprzyjaźnionych. Warto dodać, że gdy wszystkie pola w klasie znajdują się w sekcji prywatnej lub chronionej, to taką hermetyzację nazywa się hermetyzacją pełną.

W związku z powyższym, aby dostać się do prywatnych atrybutów klasy musimy zdefiniować specjalne metody. Te metody powinny umożliwiać odczytywanie lub ustawianie wartości pól klasy, a ogólnie nazywamy je akcesorami. Akcesory dzielimy na te, które ustawiają wartości pól tzw. settery (fraza set w nazwie metody) i te, które umożliwiają pobranie wartości pól tzw. gettery (fraza get w nazwie metody).

Zadanie 5

Utwórz klasę o nazwie Box, która posiada trzy zmienne prywatne typu zmiennoprzecinkowego podwójnej precyzji: width, height, depth. Wewnątrz klasy zdefiniuj konstruktor oraz klasę Ball wewnątrz tej klasy. Klasa Ball zawiera jedną prywatną zmienną typu zmiennoprzecinkowego podwójnej precyzji – r, która jest promieniem piłki. Dodatkowo klasa powinna posiadać konstruktor oraz metodę, która sprawdza czy dana piłka mieści się w pudełku. Zaprojektuj w języku C++ odpowiednie klasy i ich metody oraz funkcje umożliwiające wykorzystanie tych klas.

Rozwiązanie:
#include <iostream>

class Box {
private:
    double width, height, depth;
    class Ball {
    private:
        double r;
    public:
        Ball(double r) : r(r) {}
        bool fit(Box *box) {
            return r <= box->height / 2.0 &&
                    r <= box->width / 2.0 &&
                    r <= box->depth / 2.0;
        }
    };
    Ball *ball;
public:
    Box(double width, double height, double depth, double r) : width(width), height(height), depth(depth) { 
        ball = new Ball(r);
    }
    bool ball_fiting(Ball *b);
    Ball *get_ball() { return ball; }
    ~Box();
};

Box::~Box() {
    delete ball;
}

bool Box::ball_fiting(Ball *b) {
    return b->fit(this);
}


int main() {
    Box *box = new Box(5.0, 6.0, 7.2, 2.5);
    std::cout << box->ball_fiting(box->get_ball()) << std::endl;
    delete box;
    return 0;
}

Zadanie 6

Zaprojektuj klasę w języku C++, która umożliwi wykonanie operacji dodawania liczb zespolonych. Napisz program, który przetestuje działanie tak zaprojektowanej klasy. Program powinien poprosić o rzeczywistą i urojoną część dwóch liczb zespolonych oraz wyświetlić rzeczywistą i urojoną część ich sumy.

Rozwiązanie:
#include <iostream>

class Complex{
public:
    double real, imag;
    Complex(double real, double imag) {
        this->real = real;
        this->imag = imag;
    }
};

Complex add(const Complex &n1, const Complex &n2) {
    return Complex(n1.real + n2.real, n1.imag + n2.imag);
}

int main() {
    double real, imag;

    std::cin >> real >> imag;
    Complex number_1(real, imag);
    std::cin >> real >> imag;
    Complex number_2(real, imag);

    Complex result = add(number_1, number_2);
    std::cout << result.real << " " << result.imag << std::endl;

    return 0;
}

Zadanie 7

Utwórz klasę w języku C++ o nazwie Rectangle. Klasa powinna posiadać dwa pola prywatne typu zmiennoprzecinkowego: width i height oraz konstruktor. W przypadku, gdy użytkownik nie poda długości boków podczas tworzenia obiektu tej klasy, należy przypisać im domyślne wartości (odpowiednio: 5.f oraz 3.f). Dodatkowo klasa powinna posiadać metodę duplicate, która zwraca nowy obiekt tej klasy będący dwa razy większy oraz metodę area, która zwraca pole tego prostokąta. Napisz program w języku C++, który przetestuje działanie tak zaprojektowanej klasy.

Rozwiązanie:
#include <iostream>

class Rectangle {
    float width, height;
public:
    Rectangle() : width(5.f), height(3.f) {}
    Rectangle(float x, float y) : width(x), height(y) {}
    Rectangle duplicate();
    float area();
};

Rectangle Rectangle::duplicate() {
    return Rectangle(width * 2.f, height * 2.f);
}

float Rectangle::area() {
    return width * height;
}

int main() {
    Rectangle * test = new Rectangle();
    Rectangle d_test = test->duplicate();
    std::cout << test->area() << std::endl << d_test.area() << std::endl;
    return 0;
}

Zadanie 8

Zdefiniuj strukturę Point oraz klasę Rectangle. Struktura Point powinna zawierać dwa pola typu zmiennoprzecinkowego reprezentujące punkt w dwuwymiarowym układzie współrzędnych. Klasa Rectangle powinna posiadać pole reprezentujące dynamicznie zaalokowaną tablicę wierzchołków (wskaźnik na wskaźniki typu Point) oraz funkcje, która zwróci pole tego prostokąta. Projektując program w języku C++ zapewnij, że wszystkie stworzone obiekty zostaną usunięte. Konstruktor klasy Rectangle, powinien przyjmować cztery liczby zmiennoprzecinkowe x, y, w, h, które są kolejno pozycją lewego górnego rogu prostokąta oraz jego szerokością i wysokością.

Rozwiązanie:
#include <iostream>
#include <cmath>

struct Point {
    float x, y;
    Point (float x, float y) : x(x), y(y) {}
};

class Rectangle {
    Point **vertices;

public:
    Rectangle(float x, float y, float width, float height);
    ~Rectangle();
    float area();
};

Rectangle::Rectangle(float x, float y, float width, float height) {
    vertices = new Point*[4];
    vertices[0] = new Point(x, y);
    vertices[1] = new Point(x + width, y);
    vertices[2] = new Point(x, y + height);
    vertices[3] = new Point(x + width, y + height);
}

Rectangle::~Rectangle() {
    for(int i = 0; i < 4; ++i)
        delete vertices[i];
    delete[] vertices;
}

float Rectangle::area() {
    float a = sqrt(pow(vertices[0]->x - vertices[1]->x, 2) +
            pow(vertices[0]->y - vertices[1]->y,2));
    float b = sqrt(pow(vertices[0]->x - vertices[2]->x, 2) +
            pow(vertices[0]->y - vertices[2]->y,2));
    return a * b;
}

int main() {
    Rectangle *rect = new Rectangle(1, 2, 5, 4);
    std::cout << rect->area() << std::endl;
    delete rect;
    return 0;
}

Zadanie 9

Rozbuduj poprzedni program o metody, które umożliwią tworzenie głębokiej kopii obiektu klasy Rectangle. Dodatkowo należy przeciążyć operator przypisania dla klasy Rectangle tak, aby wykonał głęboką kopię wszystkich pól składowych.

Rozwiązanie:
#include <iostream>
#include <cmath>

struct Point {
    float x, y;
    Point (float x, float y) : x(x), y(y) {}
};

class Rectangle {
    Point **vertices;

public:
    Rectangle();
    Rectangle(float x, float y, float width, float height);
    Rectangle(const Rectangle &rect);
    ~Rectangle();
    float area();

    Rectangle& operator =(const Rectangle &rect);
};

Rectangle::Rectangle() {
    vertices = nullptr;
}

Rectangle::Rectangle(float x, float y, float width, float height) {
    vertices = new Point*[4];
    vertices[0] = new Point(x, y);
    vertices[1] = new Point(x + width, y);
    vertices[2] = new Point(x, y + height);
    vertices[3] = new Point(x + width, y + height);
}

Rectangle::Rectangle(const Rectangle &rect) {
    this->vertices = new Point*[4];
    for(int i = 0; i < 4; ++i)
        this->vertices[i] = new Point(rect.vertices[i]->x, rect.vertices[i]->y);
}

Rectangle::~Rectangle() {
    for(int i = 0; i < 4; ++i)
        delete vertices[i];
    delete[] vertices;
}

float Rectangle::area() {
    float a = sqrt(pow(vertices[0]->x - vertices[1]->x, 2) +
            pow(vertices[0]->y - vertices[1]->y,2));
    float b = sqrt(pow(vertices[0]->x - vertices[2]->x, 2) +
            pow(vertices[0]->y - vertices[2]->y,2));
    return a * b;
}

Rectangle& Rectangle::operator =(const Rectangle &rect) {
    if(this->vertices) {
        for(int i = 0; i < 4; ++i)
                delete vertices[i];
        delete[] vertices;
    }
    
    this->vertices = new Point*[4];
    for(int i = 0; i < 4; ++i)
        this->vertices[i] = new Point(rect.vertices[i]->x, rect.vertices[i]->y);
    return *this;
}

int main() {
    Rectangle *rect = new Rectangle(1, 2, 5, 4);
    Rectangle *rect_cpy = new Rectangle(*rect);
    Rectangle rect_cpy_2;
    rect_cpy_2 = *rect;
    std::cout << rect->area() << " " << rect_cpy->area() << " " << rect_cpy_2.area() << std::endl;
    delete rect;
    std::cout << rect_cpy->area() << std::endl;
    std::cout << rect_cpy_2.area() << std::endl;
    delete rect_cpy;
    return 0;
}
Omówienie:

Przeciążanie (przeładowanie) operatorów ma za zadanie przypisać operatorom nowe funkcje, określone przez nas zachowanie. Należy dodać, że generalnie większość operatorów nie jest generowana automatycznie dla tworzonych przez nas klas i struktur. Sytuacja wygląda nieco inaczej w przypadku operatora przypisania =, który generowany jest automatycznie i wykonuje płytką kopię wszystkich pól składowych. Jeżeli chcemy zmienić domyślne zachowanie operatora lub określić jak operator powinien się zachowywać dla naszej klasy należy go przeciążyć. Przeładowania operatorów w większości przypadków można dokonać jako metodę składową lub jako funkcję globalną (wyjątkiem są operatory =, [], -> te muszą być zdefiniowane jako metody).

Wspomniany wyżej operator przypisania może być przeciążony w następujący sposób:
Typ& Typ::operator=(const Typ& t);
Bardziej rozbudowany przykład mamy poniżej:

Rectangle& Rectangle::operator =(const Rectangle &rect) {
    if(this->vertices) {
        for(int i = 0; i < 4; ++i)
                delete vertices[i];
        delete[] vertices;
    }
    
    this->vertices = new Point*[4];
    for(int i = 0; i < 4; ++i)
        this->vertices[i] = new Point(rect.vertices[i]->x, rect.vertices[i]->y);
    return *this;
}

Powyższy kod w liniach 2-6 weryfikuje, czy do wskaźników nie jest przypisany adres dynamicznie zaalokowanego obszaru pamięci. W sytuacji, gdy tak jest, to najpierw tą pamięć zwalnia, a następnie w liniach 7-9 dynamicznie allokuje pamięć i inicjalizuje zmienne wartościami obiektu przekazanego w argumencie.


Warto dodać, że nie wszystkie operatory występujące w języku C++ można przeładować. Przykładami takich operatorów są: odniesienie do składowej klasy (operator .), odniesienia do składnika będącego wskaźnikiem (operator .*), operatora zakresu (operator ::), czy operator zwracającego wartość zależnie od spełnienia warunku (operator ?:).

Dodatkowo nie możemy modyfikować priorytetów i argumentowości operatorów. Kontynuując nie możemy przeciążyć operatorów dla typów wbudowanych tj. int, char, float itd. Tematyka przeciążania operatorów jest bardzo rozbudowanym działem, a omówienie przeładowania wszystkich operatorów wykracza za zakres tego kursu. Jednakże osoby zainteresowane zachęcam do zapoznania się z dokumentacją przeciążenia operatorów – dokumentacja.

Zadanie 10*

Zaimplementuj klasę w języku C++, która będzie symulowała zachowanie kolejki liczb całkowitych (FIFO). Kolejka powinna być implementacją tablicową, czyli pojedynczy węzeł reprezentowany jest przez element tablicy. Napisz program w języku C++, który przetestuje działanie tej klasy.

Rozwiązanie:
#include <iostream>
#include <cstdlib>

#define SIZE 10

class MyQueue {
    int *arr, capacity, front, rear, count;

public:
    MyQueue(int size = SIZE);
    ~MyQueue();

    void pop();
    void push(int x);
    int first();
    int size();
    bool is_empty();
    bool is_full();
};

MyQueue::MyQueue(int size) {
    arr = new int[size];
    capacity = size;
    front = 0;
    rear = -1;
    count = 0;
}

MyQueue::~MyQueue() {
    delete[] arr;
}

void MyQueue::pop() {
    if(is_empty()) {
        exit(EXIT_FAILURE); //Zakończenie programu błędem
    }
    front = (front + 1) % capacity;
    count--;
}

void MyQueue::push(int item) {
    if (is_full()) {
        exit(EXIT_FAILURE); //Zakończenie programu błędem
    }
    rear = (rear + 1) % capacity;
    arr[rear] = item;
    count++;
}

int MyQueue::first() {
    if (is_empty()) {
        exit(EXIT_FAILURE); //Zakończenie programu błędem
    }
    return arr[front];
}

int MyQueue::size() {
    return count;
}

bool MyQueue::is_empty() {
    return (size() == 0);
}

bool MyQueue::is_full() {
    return (size() == capacity);
}

int main() {
    MyQueue q(5);

    q.push(1);
    q.push(2);
    q.push(3);

    std::cout << "Pierwszy element: " << q.first() << std::endl;
    
    q.pop();
    q.push(4);
    
    std::cout << "Rozmiar kolejki: " << q.size() << std::endl;
    q.pop();
    q.pop();
    q.pop();
    
    if (q.is_empty()) 
        std::cout << "Kolejka jest pusta.\n";
    else 
        std::cout << "Kolejka nie jest pusta.\n";

    return 0;
}

Zadanie 11*

Znamy takie kalendarze jak: gregoriański, juliański, starogrecki, rzymski, egipski, aztecki, czy bardzo ciekawy kalendarz Majów. Każdy z nich jest charakterystyczny, dzieli czas na pewne cykle, a niektóre kalendarze zakładają np. lata przestępne, bądź inne istotne parametry. Oczywiście nie są to jedyne znane nam kalendarze, są również kalendarze bardzo osobliwe związane z pewnymi światami opisanymi w literaturze, filmie, czy grach. Jednym z takich popularnych kalendarzy jest kalendarz elfów, pojawiający się w książkach Sapkowskiego. (Tak! Tego gościa od Wiedźmina. 🙂 )

Naszym zadaniem jest napisanie programu, który w sposób automatyczny będzie liczył dni tygodnia, dla kalendarza określonego pewnymi zmiennymi parametrami. Te zmienne parametry dotyczą liczby miesięcy, liczby dni w poszczególnych miesiącach, liczbie dni tygodnia, numeru miesiąca, w którym doliczany jest dodatkowy dzień dla roku przestępnego. Zakładamy, że w naszym kalendarzu może pojawić się rok przestępny, a metoda sprawdzenia, czy dany rok takim jest wygląda następująco:

F1: 0
F2: 2
Frok: (a * Frok - 1 + Frok - 2 + b) mod m


gdzie a, b i m to parametry wybrane do określenia naszego kalendarza. Jeśli wynikiem funkcji będzie wartość nieparzysta to rok jest przestępny.

Zakładamy, że pierwszego dnia pierwszego miesiąca pierwszego roku mamy pierwszy dzień tygodnia. Dodatkowo obliczenia rozpoczynamy od roku 1.

Napisz program w języku C++, który na standardowym wejściu przyjmie w pierwszym wierszu sześć liczb naturalnych n_months, n_week_days, leap_month, a, b, m, oznaczająco kolejno: liczba miesięcy, liczba dni tygodnia, miesiąc przestępny oraz parametry metody obliczającej, czy dany rok jest przestępnym. W kolejnym wierszu program powinien wczytać od użytkownika ciąg n_months liczb dodatnich, z których każda oznacza liczbę dni w kolejnych miesiącach. Na koniec program, powinien wczytać 3 liczby dodatnie d, m, r, oznaczające datę w postać: dzień miesiąc rok.

W wyniku działania program powinien wyświetlić na standardowym wyjściu liczbę naturalną, oznaczającą numer dnia tygodnia dla odczytanej daty.

Uwagi: n_months, n_week_days, leap_month, a, b, m, d, m, r oraz wartości określające ilość dni w miesiącach to liczby naturalne. W zadaniu nie można używać struktur i klas.

Przykład:
input:
3 5 2 20 15 5
10 12 7
9 2 1
output:
4

Rozwiązanie:
#include<iostream>

void calculate_years_leap_days(int *arr, int n, int a, int b, int m) {
    int tmp, n_leap_days = 0, f_0 = 0, f_1 = 2;
    for(int i = 2; i <= n; ++i) {
        tmp = (a * f_1 + f_0 + b) % m;
        f_0 = f_1;
        f_1 = tmp;
        if(f_1 % 2) 
            ++n_leap_days;
        arr[i] = n_leap_days;
    }
}

unsigned int calculate_day_of_the_week(int *arr, int *days_months, int day, int month, int year, int leap_month, int n_week_days, int n_months) {
    int tmp = year - 1, n_leap_days = 0;
    if(year > 1) 
        n_leap_days = (month > leap_month && arr[year] > arr[tmp]) ? arr[tmp] + 1 : arr[tmp];
    return (days_months[n_months] * tmp + days_months[month - 1] + day + n_leap_days - 1) % n_week_days + 1;
}

int main(){
    int day, month, year, n_months, n_week_days, leap_month, a, b, m;
    std::cin >> n_months >> n_week_days >> leap_month >> a >> b >> m;
    
    int *days_months = new int[n_months + 1]();
    for(int i = 1; i <= n_months; ++i) {
        std::cin >> days_months[i];
        days_months[i] += days_months[i - 1];
    }
    
    std::cin >> day >> month >> year;
    int *years_n_leap_days = new int[year + 1]();
    calculate_years_leap_days(years_n_leap_days, year, a, b, m);
    
    std::cout << calculate_day_of_the_week(years_n_leap_days, days_months, day, month, year, leap_month, n_week_days, n_months) << std::endl;
    
    delete[] days_months; 
    delete[] years_n_leap_days;
    return 0;
}

Przygotuj się na kolejne laboratorium!

W celu przygotowania się na kolejne zajęcia, spróbuj wykonać poniższe zadania samodzielnie.

Zadanie 1

Napisz program w języku C++, który wyświetli zawartość obiektu std::string znak po znaku posługując się operatorem subskryptowym [], pętlą for i funkcją std::string::size.

Zadanie 2

Napisz funkcję w języku C++, która przyjmie tablicę obiektów std::string i jej rozmiar. Funkcja powinna zwrócić napis, w którym znajdą się skonkatenowane, oddzielone pojedynczą spacją napisy z tablicy. Użyj operatora +=. Napisz program w języku C++, który przetestuje działanie tej funckji.

Zadanie 3

Napisz funkcję w języku C++, która przyjmie jako parametry dwa obiekty klasy std::string, które nazwane zostaną przeszukiwany i poszukiwany. Funkcja powinna zwrócić indeks, w którym napis poszukiwany znajduje się w przeszukiwanym lub -1 jeżeli nie zostanie on znaleziony. Użyj metody std::string::find.

Zadanie 4

Napisz program w języku C++, który wyświetli zawartość obiektu std::string posługując się iteratorami std::string::begin, std::string::end oraz pętlą for.

Zadanie 5

Napisz funkcję w języku C++, która przyjmie obiekt std::vector oraz dwie wartości całkowite a i b. Funkcja powinna zwrócić obiekt std::vector zawierający liczby z wektora wejściowego znajdujące się w obustronnie otwartym zakresie (a, b). Napisz program w języku C++, który przetestuje działanie tej funkcji.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *