PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C++

Od punktu do fontanny Klasa "Punkt" w C++ Moduły pomocnicze i program testowy Klasa "Okrag" w C++ i program testowy Klasa "Kropla" w C++ Implementacja listy i program "Fontanna" Poprzedni przykład Następny przykład Program w Visual C# Kontakt

Od punktu do fontanny

Tradycyjne podejście do programowania, nazywane programo­waniem procedu­ralnym (ang. Procedural Programming), cechuje rozdzie­lenie danych i przetwa­rzających je procedur (funkcji w C++). Ma ono dwie zasadnicze wady. Po pierwsze, odizolo­wanie danych od wykony­wanych na nich operacji może prowadzić do przypad­kowej, niepożą­danej zmiany tych danych z powodu braku ochrony przed niepowo­łanym dostępem. Po drugie, dostoso­wanie opraco­wanego tą metodą oprogramo­wania do nowych warunków wymaga modyfi­kacji kodu źródłowego, co może okazać się bardzo trudne, a niekiedy z powodu jego niedostę­pności niewykonalne.

Na zupełnie innej filozofii oparte jest programo­wanie zoriento­wane obiektowo (ang. Object Oriented Programming, OOP), zwane krócej programo­waniem obiektowym. Charakteryzuje się ono następu­jącymi cechami:

Klasa nie zajmuje żadnego miejsca w pamięci, jest swoistym wzorcem, według którego tworzy się w pamięci obiekty. Każdy obiekt jest egzempla­rzem (instancją) pewnej klasy, ma własny zestaw pól określa­jących jego cechy, ma również sprecy­zowane przez metody klasy zacho­wanie. Hermety­zacja pozwala na kontrolo­wanie dostępu do pól i metod obiektu dla pozosta­łych części programu. Z kolei dziedzi­czenie umożliwia tworzenie nowych klas na podstawie klas istnie­jących. Klasy potomne dziedziczą pola i metody klas bazowych, a ponadto mogą zawierać nowe pola i metody rozszerza­jące możliwości odziedzi­czone po klasach bazowych. Natomiast polimor­fizm stano­wiący uzupeł­nienie dziedzi­czności pozwala na określenie innego kodu metod w klasach potomnych. Oznacza to, że ta sama metoda może w odnie­sieniu do obiektu potomnego wywołać inne działanie niż w przypadku obiektu bazowego.

Przypomnijmy, że rozwiązania obiektowe znalazły zastosowanie w trzech progra­mach już prezento­wanych na niniejszej witrynie: wieże Hanoi, układ planetarny oraz tworzenie epicy­kloidy i hipocy­kloidy. Używane w nich obiekty stanowią odzwiercie­dlenie obiektów świata rzeczywi­stego (wieże składane z krążków, ciała układu planetar­nego) i matematy­cznego (krzywa genero­wana przez koniec ramienia przytwier­dzonego do okręgu toczącego się po nieru­chomym okręgu). Zgodnie z powsze­chnym zalece­niem hermety­zacji pola tych obiektów zostały zadekla­rowane jako prywatne, przez co są one ukryte przed dostępem z zewnątrz, dostęp do nich mają jedynie metody klasy.

Ważną umiejętnością w programowaniu obiektowym jest tworzenie klas na bazie klas uprzednio zdefinio­wanych. Dzięki mechani­zmowi dziedzi­czenia można utworzyć nową klasę bez modyfiko­wania już gotowego kodu klasy bazowej poprzez uzupeł­nienie o nowe składniki. W dalszych rozważa­niach sformu­łujemy dwie klasy reprezen­tujące punkty i okręgi wraz z progra­mami testowymi umożliwia­jącymi ich rysowanie na ekranie i przesu­wanie, a na koniec klasę wyobra­żającą krople wody i program imitujący fontannę. Klasa reprezen­tująca punkt będzie bazową dla klasy przedsta­wiającej okrąg, a ta z kolei bazową dla klasy wyobraża­jącej kroplę wody.

Klasa "Punkt" w C++

Podstawową zasadą, którą należy kierować się w programo­waniu obiektowym, jest trakto­wanie klasy jako odpowie­dnika pewnej grupy obiektów rzeczy­wistych, których dotyczy program. W defi­nicji klasy należy wziąć pod uwagę tylko istotne, wspólne cechy i zacho­wania tych obiektów. Jeśli np. zadaniem programu jest kreślenie na ekranie prostych figur geometry­cznych i ich przesu­wanie, to przy określaniu pól i metod najpro­stszej klasy reprezen­tującej punkty wypada uwzglę­dnić współ­rzędne i kolor punktu oraz jego pokazy­wanie, ukrywanie i przesu­wanie.

Nową klasę, którą nazwiemy Punkt, umieścimy w module złożonym z pliku nagłów­kowego punkty.h zawiera­jącego jej definicję i pliku źródło­wego punkty.cpp jej funkcji składo­wych. Takie rozwią­zanie jest wygodne, gdy klasa ma być używana w różnych programach, a ponadto moduł taki można skompi­lować i w takiej postaci go udostę­pniać, by ukryć implementację jej metod i nie dopuścić do ich modyfi­kacji. Defi­nicję klasy wolno włączyć do programu tylko jeden raz, dlatego w jej pliku nagłów­kowym należy uwzglę­dnić kompi­lację warun­kową. Zabezpie­czony w ten sposób kod zawarty w pliku punkty.h może wyglądać następu­jąco:

#ifndef H_PUNKTY
#define H_PUNKTY

#include <graphics.h>

class Punkt
{
protected:
    int x, y;       // Współrzędne punktu
    int kolor;      // Kolor punktu
    bool wid;       // true - widoczny, false - nie
public:
    Punkt(int xPoc, int yPoc, int kol);
    virtual ~Punkt();
    void pokaz();
    void ukryj();
    virtual void rysuj();
    void przesun_do(int xNowe, int yNowe);
    void przesun_o(int xDelta, int yDelta);
    int get_x()     { return x; }
    int get_y()     { return y; }
    int get_kolor() { return kolor; }
    bool widoczny() { return wid; }
};

#endif // H_PUNKTY

Plik zawiera dyrektywę włączającą bibliotekę grafi­czną używaną w drugiej części modułu i programie oraz definicję klasy. Wszystkie pola klasy są chronione (specyfi­kator protected), co oznacza, że są one dostępne nie tylko w klasie, w której zostały zdefinio­wane, lecz również w klasach pochodnych. Natomiast wszystkie metody klasy są publiczne (specyfi­kator public), można się do nich odwoływać również spoza metod klasy. Bardzo krótki kod metod get_x, get_y, get_kolor (ang. get – dostań, pobierz) i widoczny jest umieszczony bezpośre­dnio w defi­nicji klasy, co stanowi wskazówkę dla kompila­tora, aby wywo­łania tych metod zastę­pował ich kodem (kompi­lator może ją zigno­rować). Zalecenie to ma na celu skrócenie czasu wykonania programu. Można je również stosować w odnie­sieniu do zwykłych funkcji, poprze­dzając ich typy w nagłów­kach słowem kluczowym inline (funkcje inline). Dzięki wymie­nionym metodom wartości pól obiektu są dostępne z dowol­nego miejsca programu. Implemen­tację pozosta­łych metod klasy zawiera drugi plik modułu. Każda jest w nim zdefinio­wana podobnie jak zwykła funkcja z tą różnicą, że jej nazwę poprzedza nazwa klasy i operator zasięgu (::) określa­jący przyna­leżność do danej klasy:

#include "punkty.h"

Punkt::Punkt(int xPoc, int yPoc, int kol) : x(xPoc), y(yPoc), kolor(kol), wid(false)
{}

Punkt::~Punkt()
{
    if (wid) ukryj();
}

void Punkt::pokaz()
{
    int kol = getcolor();
    setcolor(kolor);
    wid = true;
    rysuj();
    setcolor(kol);
}

void Punkt::ukryj()
{
    int kol = getcolor();
    setcolor(getbkcolor());
    wid = false;
    rysuj();
    setcolor(kol);
}

void Punkt::rysuj()
{
    putpixel(x, y, getcolor());
}

void Punkt::przesun_do(int xNowe, int yNowe)
{
    bool jest_widoczny = wid;
    if (jest_widoczny) ukryj();
    x = xNowe;
    y = yNowe;
    if (jest_widoczny) pokaz();
}

void Punkt::przesun_o(int xDelta, int yDelta)
{
    przesun_do(x + xDelta, y + yDelta);
}

Dyrektywa #include włączająca plik nagłów­kowy modułu jest niezbędna ze względu na wyma­gany dostęp metod klasy Punkt do jej definicji i biblio­teki graficznej. Metoda Punkt jest konstru­ktorem, specjalną metodą wykony­waną podczas tworzenia obiektu, a metoda ~Punkt destru­ktorem wywoły­wanym automaty­cznie w trakcie niszczenia obiektu. Ich nazwy są takie same jak nazwa klasy, z tym że nazwa destru­ktora jest poprze­dzona tyldą (~). Obie nie zwracają żadnej wartości, nawet typu void. Zadaniem konstru­ktora jest zainicjali­zowanie obiektu polega­jące na nadaniu jego polom odpowie­dnich wartości i wyko­naniu pewnych czynności związanych z obsługą metod wirtu­alnych. W jego definicji zamiast czterech przypisań użyto listy inicjali­zującej, której elemen­tami są nazwy pól z zamknię­tymi w nawiasach wartościami początkowymi. Z kolei zadaniem destru­ktora jest ukrycie punktu, gdy jest widoczny.

Metoda pokaz ukazuje punkt na ekranie, wyświe­tlając go we właściwym mu kolorze, zaś metoda ukryj ukrywa go, wyświe­tlając go w kolorze tła. Obie przecho­wują w zmiennej pomocni­czej aktualny kolor rysowania, by przywrócić go po naryso­waniu punktu, przypisują również polu wid wartość wskazu­jącą, czy jest widoczny. Z kolei metoda przesun_do przesuwa punkt do nowego miejsca. Gdy przed przesu­nięciem punkt jest widoczny, metoda ukrywa go, a po przesu­nięciu pokazuje. Metoda przesun_o przesuwa­jąca punkt o dany wektor jest odmianą poprzedniej.

Klasa Punkt zawiera dwie metody wirtualne, destru­ktor ~Punkt i metodę rysuj, wyróżnione w defi­nicji słowem kluczo­wym virtual. Cecha ta umożliwia inne działanie metody w klasie pocho­dnej niż w klasie bazowej. W przy­padku klasy reprezen­tującej okrąg obecny destruktor nie wymaga redefi­niowania, ale metoda rysuj tak, gdyż do ryso­wania okręgu należy użyć funkcji circle, nie putpixel. Ale np. w przy­padku klasy reprezen­tującej łamaną, w której odziedzi­czone pola xy zawierają współ­rzędne punktu początko­wego łamanej, a nowe pole jest tablicą dynami­czną struktur o polach dxdy określa­jących przesu­nięcie kolej­nego wierz­chołka łamanej względem wierz­chołka poprze­dniego, redefi­niowane muszą być obie metody. Destru­ktor klasy pochodnej powinien bowiem nie tylko ukryć łamaną, gdy jest widoczna, lecz także zwolnić pamięć tablicy struktur. Jeżeli nie ma się pewności, że metoda nie będzie redefi­niowana w klasie pochodnej, lepiej zdefi­niować ją jako wirtualną. Uniknie się w ten sposób kłopo­tliwej modyfi­kacji kodu, gdyby później okazało się, że w nowej klasie pochodnej działanie tej metody ma być odmienne i trzeba utworzyć jej nową wersję.

Wykorzystanie zdefiniowanej powyżej klasy Punkt wydaje się proste. Oto przykład funkcji main, która tworzy obiekt lokalny reprezen­tujący czerwony punkt na środku ekranu i pokazuje go, a następnie (po naciśnięciu klawisza przez użytko­wnika) przesuwa ten punkt w nowe miejsce i (po ponownym naciśnięciu klawisza) ukrywa ten punkt:

int void main()
{
    initwindow(640, 480);
    Punkt pkt(320, 240, RED);     // Utworzenie obiektu lokalnego
    pkt.pokaz();                  // Pokazanie punktu
    getch();
    pkt.przesun_do(400, 160);     // Przesunięcie punktu
    getch();
    pkt.ukryj();                  // Ukrycie punktu
    closegraph();
    return 0;                     // Niszczenie obiektu lokalnego
}

Obiekt reprezentujący punkt jest tworzony po włączeniu trybu (otwarciu okna) grafi­cznego. Niestety, jest niszczony w chwili opuszczania funkcji main, czyli po uprzednim wyłączeniu trybu (zamknięciu okna) grafi­cznego. Na szczęście przed zamknięciem grafiki punkt jest ukrywany, dzięki czemu nie pojawia się błąd wywoły­wania funkcji grafi­cznych (ryso­wania punktu w kolorze tła) w trybie tekstowym. Temu niezbyt eleganc­kiemu rozwią­zaniu można zaradzić, używając obiektu dynami­cznego, który można zniszczyć zanim nastąpi wyjście z trybu graficznego:

int void main()
{
    initwindow(640, 480);
    Punkt p = new Punkt(320, 240, RED); // Utworzenie obiektu dynamicznego
    p->pokaz();                         // Pokazanie punktu
    getch();
    p->przesun_do(400, 160);            // Przesunięcie punktu
    getch();
    delete p;                           // Niszczenie obiektu dynamicznego
    closegraph();
    return 0;
}

Opisana sytuacja dotyczy mechanizmu wywoły­wania konstru­ktorów i destru­ktorów w języku C++. Konstru­ktor jest zawsze wywoły­wany w chwili tworzenia obiektu, a destru­ktor automaty­cznie w chwili jego niszczenia. Jeden i drugi moment jest określany następu­jąco:

Moduły pomocnicze i program testowy

Naszym zamierzeniem jest teraz opracowanie programu testo­wego, który wyświetla punkt reprezen­towany przez obiekt klasy Punkt i umożliwia jego przesu­wanie za pomocą klawiatury. Nie możemy jednak skorzystać z wygodnej funkcji czytajZnak pobie­rania znaku zdefinio­wanej w module klaw opartym na funkcji _getch z biblio­teki conio, gdyż powstałby konflikt z biblio­teką graficzną WinBGI (zawie­szenie programu). Przez analogię formu­łujemy zatem podobny moduł używa­jący funkcji getch z biblio­teki grafi­cznej, który nazwiemy gklaw. Jego plik nagłów­kowy zawiera­jący prototyp nowej funkcji pobie­rania znaku i nazwy symboli­czne jej wybranych wartości ma postać:

// gklaw.h - Moduł czytania znaku z klawiatury w trybie WinBGI
// -----------------------------------------------------------
// czytaj_Znak - pobiera znak z klawiatury i zwraca jego kod
// -----------------------------------------------------------

#ifndef H_GKLAW
#define H_GKLAW

#include <graphics.h>

#define G_UP    (256 + 72)  // Strzałka w górę
#define G_DN    (256 + 80)  // Strzałka w dół
#define G_PGUP  (256 + 73)  // PgUp
#define G_PGDN  (256 + 81)  // PgDn
#define G_LEFT  (256 + 75)  // Strzałka w lewo
#define G_RIGHT (256 + 77)  // Strzałka w prawo
#define G_HOME  (256 + 71)  // Home
#define G_END   (256 + 79)  // End
#define G_TAB   9           // Tab
#define G_BACKS 8           // Backspace
#define G_ENTER 13          // Enter
#define G_ESC   27          // Esc

int czytaj_Znak();          // Czytanie znaku

#endif // H_GKLAW

Definicja zapowie­dzianej funkcji czytaj_Znak jest zawarta w pliku gklaw.cpp modułu przedsta­wionym na poniższym listingu. W przypadku naciśnięcia zwykłego klawisza funkcja zwraca jego kod, a przypadku klawisza funkcyj­nego lub kombi­nacji generu­jącej podwójny znak – kod drugiego znaku powię­kszony o liczbę 256. Kod pierwszego z podwójnych znaków wczyty­waych przez funkcję getch biblio­teki graficznej jest zawsze równy zero, dlatego funkcja pobierania znaku w trybie graficznym jest nieco prostsza od jej odpowie­dnika w trybie tekstowym:

#include "gklaw.h"

int czytaj_Znak()
{
    int c = getch();
    return (c != 0) ? c : 256 + getch();
}

Drugi moduł pomocniczy o nazwie ruch zawiera tylko funkcję, której pierwszym argumentem jest przeka­zywany przez przez referencję obiekt klasy Punkt, zaś drugi i trzeci są liczbami całko­witymi określa­jącymi wielkość przesu­nięcia jednostko­wego wzdłuż osi xy. Plik nagłów­kowy tego modułu ma postać:

#ifndef H_RUCH
#define H_RUCH

#include "gklaw.h"
#include "punkty.h"

void ruch_Punktu(Punkt &pkt, int dx, int dy);

#endif // H_RUCH

Zadaniem funkcji ruch_Punktu jest pobieranie znaków z klawia­tury i reago­wanie na klawisze strzałek przesu­nięciem punktu zgodnie z ich oznacze­niami, a na klawisz Home umieszczeniem punktu na środku okna grafi­cznego. W przy­padku napotkania znaku Esc funkcja ma zakończyć działanie. Inne znaki ma ignorować. Kod źródłowy funkcji zawarty w pliku ruch.cpp modułu wygląda następująco:

#include "ruch.h"

void ruch_Punktu(Punkt &pkt, int dx, int dy)
{
    do
        switch (czytaj_Znak())
        {
            case G_UP:
                pkt.przesun_o(0, -dy);
                break;
            case G_DN:
                pkt.przesun_o(0, dy);
                break;
            case G_LEFT:
                pkt.przesun_o(-dx, 0);
                break;
            case G_RIGHT:
                pkt.przesun_o(dx, 0);
                break;
            case G_HOME:
                pkt.przesun_do(getmaxx()/2, getmaxy() / 2);
                break;
            case G_ESC:
                return;
        }
    while (true);
}

Jak widać, w formalnie nieskończonej pętli do–while pobierane za pomocą funkcji czytaj_Znak znaki są interpre­towane przez instru­kcję switch. Gdy funkcja zwróci wartość G_ESC oznacza­jącą, że użytko­wnik nacisnął klawisz Esc, pętla zostaje przerwana przez instru­kcję return. Instru­kcje break nie przery­wają pętli, lecz instru­kcję switch, gdyż w jej zakresie się znajdują.

Można wreszcie przystąpić do prezentacji programu wyświetla­jącego punkt i umożliwia­jącego przesu­wanie go za pomocą klawia­tury. Do projektu programu należy oczywiście dodać opraco­wane powyżej moduły (pliki punkty.h, punkty.cpp, gklaw.h, gklaw.cpp, ruch.hruch.cpp). Kod programu jest bardzo prosty:

#include <iostream>
#include "ruch.h"

using namespace std;

int main()
{
    cout << "M E N U :\n---------\n";
    cout << "Strzałki - lewo, prawo, góra, dół\n";
    cout << "Centrum - Home\nKoniec - Esc";
    initwindow(480, 300, "Przesuwanie punktu");
    Punkt *p = new Punkt(getmaxx() / 2, getmaxy() / 2, LIGHTCYAN);
    p->pokaz();
    ruch_Punktu(*p, 2, 2);
    delete p;
    closegraph();
    return 0;
}

Program wypisuje w oknie tekstowym krótkie menu informu­jące o dostępnych klawiszach, a po przejściu do trybu grafi­cznego konstruuje obiekt reprezen­tujący jasnotur­kusowy punkt i wyświetla go pośrodku okna grafi­cznego. Użytkownik może ten punkt przesuwać, naciskając klawisze strzałek i klawisz Home, a gdy wybierze Esc, program niszczy obiekt, zamyka okno graficzne i kończy działanie. Widok okien utworzo­nych przez program tuż po jego urucho­mieniu jest pokazany na poniższym rysunku.

Klasa "Okrag" w C++ i program testowy

Definicję klasy pochodnej umożliwiającej tworzenie obiektów reprezen­tujących okręgi wygodnie jest również umieścić w odrębnym module. Jego plik nagłów­kowy okregi.h jest przedsta­wiony na poniż­szym listingu. Na początku definicji, po dwukropku podany jest zapis stwier­dzający, że klasa Okrag wywodzi się od klasy Punkt. Specyfi­kator public wskazuje, że dostęp do odziedzi­czonych składników jest w klasie pochodnej taki sam jak w klasie bazowej (niższe, rzadziej stosowane poziomy dziedzi­czenia umożli­wiają specyfi­katory protectedprivate).

#ifndef H_OKREGI
#define H_OKREGI

#include "punkty.h"

class Okrag : public Punkt
{
protected:
    int r;          // Promień okręgu
public:
    Okrag(int xSr, int ySr, int kol, int rOkr);
    virtual void rysuj();
    void zmien_r(int rNowe);
    void zmien_ro(int rDelta);
    int get_r()     { return r; }
};

#endif // H_OKREGI

Odziedziczone pola x, ykolor określają współ­rzędne środka i kolor okręgu, a nowe pole r jego promień. Oprócz pól klasa Okrag dziedziczy metody klasy Punkt, które tym razem udostę­pniają współ­rzędne środka i kolor okręgu oraz służą do jego pokazywania, ukrywania i przesu­wania. Naturalnie redefi­nicji wymaga metoda rysowania, gdyż okrąg wygląda inaczej niż punkt. Natomiast nowe metody klasy pochodnej dotyczą zmiany i udostę­pniania promienia okręgu. Kod źródłowy zawarty w pliku okregi.cpp drugiej części modułu ma postać:

#include "okregi.h"

Okrag::Okrag(int xSr, int ySr, int kol, int rOkr) : Punkt(xSr, ySr, kol)
{
    r = (rOkr > 0) ? rOkr : 1;
}

void Okrag::rysuj()
{
    circle(x, y, r);
}

void Okrag::zmien_r(int rNowe)
{
    if (rNowe > 0)
    {
        bool jest_widoczny = wid;
        if (jest_widoczny) ukryj();
        r = rNowe;
        if (jest_widoczny) pokaz();
    }
}

void Okrag::zmien_ro(int rDelta)
{
    zmien_r(r + rDelta);
}

Zauważmy, że lista inicjalizująca konstru­ktora klasy Okrag zawiera wywołanie konstru­ktora Punkt. Jest to konieczne, ponieważ podczas tworzenia obiektu klasy pochodnej wszystkie pola, które ten obiekt dziedziczy, muszą być najpierw zainicjali­zowane przez konstru­ktor klasy bazowej, co wymaga przeka­zania mu stoso­wnych argumentów. Dopiero po tej operacji mogą być inicjali­zowane pozostałe pola klasy pochodnej.

Program wyświetlania okręgu i przesu­wania go za pomocą klawia­tury jest bardzo podobny do przedsta­wionego powyżej programu wyświe­tlania i przesu­wania punktu. Różnica polega właściwie tylko na użyciu obiektu klasy pochodnej Okrag zamiast obiektu klasy bazowej Punkt, co oczywiście wymaga uwzglę­dnienia w projekcie programu dodatko­wych plików okregi.hokregi.cpp:

#include <iostream>
#include "ruch.h"
#include "okregi.h"

using namespace std;

int main()
{
    cout << "M E N U :\n---------\n";
    cout << "Strzałki - lewo, prawo, góra, dół\n";
    cout << "Centrum - Home\nKoniec - Esc";
    initwindow(480, 300, "Przesuwanie okręgu");
    Okrag *p = new Okrag(getmaxx() / 2, getmaxy() / 2, LIGHTRED, 50);
    p->pokaz();
    ruch_Punktu(*p, 2, 2);
    delete p;
    closegraph();
    return 0;
}

Jak widać, mechanizm dziedziczenia pozwolił na wykorzy­stanie w nowych warun­kach kodu modułów punktyruch. Formalnie pierwszym argu­mentem funkcji ruch_Punktu jest referencja wskazu­jąca na obiekt klasy Punkt. Oznacza to, że w wywołaniu tej funkcji może nią być zarówno obiekt klasy Punkt, jak i obiekt klasy pochodnej wywo­dzącej się bezpo­średnio lub pośrednio od klasy Punkt. Tutaj jest nim obiekt klasy Okrag (rys.). Utworzenie klasy potomnej nie tylko nie zmienia zachowań przewidzia­nych w klasie bazowej, ale i nie wymaga żadnych modyfi­kacji jej kodu. Dzięki temu raz napisany i spraw­dzony kod nadaje się do dalszego wykorzy­stania i rozsze­rzania, co jest wielką zaletą progra­mowania obiekto­wego.

Klasa "Kropla" w C++

Zajmiemy się obecnie zdefiniowaniem klasy Kropla, która posłuży do tworzenia obiektów imitu­jących krople wody wyrzucane w górę z dyszy fontanny w nocnej wielo­barwnej ilumi­nacji. Narzuca­jącym się rozwią­zaniem jest wywie­dzenie nowej klasy z klasy Okrag. Odziedzi­czone pola x i y będą określać pozycję kropli na ekranie, r jej wielkość, a nowe pola vxvy składowe wektora prędkości. Potrzebna jest też nowa metoda przesu­wania kropli i zmiany jej prędkości. Aby wywołać wrażenie tryska­jącego z fontanny strumienia wody, wprowa­dzimy element chaosu przy ustalaniu prędkości początkowej kropel, posługując się funkcją rand generu­jącej liczby losowe.

Naturalnym opisem ruchu wyrzuconej kropli wody jest rzut ukośny w polu grawita­cyjnym z prędkością początkową o kierunku pionowym lub niemal pionowym w górę (rys. powyżej, α ≈ 90o). Rzut ten można po pominięciu oporu powietrza rozłożyć na dwa ruchy – ruch jedno­stajny prostoli­niowy poziomy ze stałą prędko­ścią vx w prawo lub w lewo i ruch jedno­stajny opóźniony prostoli­niowy ku górze z prędko­ścią począ­tkową vy. Gdy rzut nastąpi z punktu x = y = 0, to po czasie t kropla znajdzie się w punkcie o współ­rzędnych

gdzie g jest przyśpie­szeniem ziemskim równym 9,81 m·s-2. Równania te przedsta­wiają parabolę skiero­waną ku dołowi (w skrajnym przypadku odcinek). Nietrudno sprawdzić, że maksy­malna wysokość, na jaką wzniesie się kropla zanim zacznie spadać, wynosi

Z równości tej wynika, że jeśli kropla będzie wyrzucana w górę z dolnej krawędzi obszaru roboczego okna z prędkością początkową wzdłuż osi y spełnia­jącą warunek

w którym ymax oznacza wysokość tego obszaru, to nie przekroczy jego górnej krawędzi. Wydaje się oczywiste, aby pozycją początkową kropli był środek dolnej krawędzi. Zważywszy na pożądaną losowość, pozycja ta będzie nieco zaburzana w poziomie (do 10 pikseli w lewo lub w prawo).

W celu zobrazo­wania ruchu kropli posłużymy się prostym schematem oblicze­niowym wynika­jącym z przedsta­wionych na początku wzorów określa­jących zmienia­jące się w czasie współ­rzędne. Jeśli miano­wicie dt jest niewielkim odstępem czasowym, to po jego upływie przy dowolnie ustalonej pozycji początkowej nową pozycję kropli i składową pionową jej prędkości można wyznaczyć w sposób przybli­żony nastę­pująco:

Po tych wstępnych przygotowaniach związanych z ustale­niem wygodnej reprezen­tacji ruchu kropli wody możemy przejść do sformuło­wania zapowie­dzianej klasy Kropla. Podobnie jak poprze­dnio, nową klasę umieszczamy w module złożonym z plików krople.hkrople.cpp. Pierwszy zawiera definicję klasy i prototyp (nagłówek) użyte­cznej funkcji pomocni­czej los generu­jącej losowe liczby całkowite z określo­nego w argu­mentach zakresu:

#ifndef H_KROPLE
#define H_KROPLE

#include <cstdlib>
#include <ctime>>
#include <cmath>>
#include "okregi.h"

int los(int a, int b);          // Losowa liczba całkowita od a do b-1

class Kropla : public Okrag
{
    double vx, vy;              // Składowe prędkości
    void predkosc_Pocz();       // Prędkość początkowa
public:
    Kropla(int x, int y, int kolor, int r);
    void przesun(double dt);
};

#endif // H_KROPLE

Klasa Kropla dziedziczy pola i metody klasy Okrag, ma także nowe pola reprezen­tujące składowe prędkości kropli oraz nowe metody umożli­wiające ustawienie ich wartości początko­wych i przesu­wanie kropli o odcinek przebyty przez nią w ostatnim interwale czasowym. Definicje metod klasy, stałej określa­jącej przyśpie­szenie ziemskie i zapowie­dzianej funkcji los oraz podobnej do niej funkcji o takiej samej nazwie, lecz generu­jącej losowe liczby rzeczy­wiste (zob. przecią­żanie funkcji), znajdują się w drugim pliku modułu:

#include "krople.h"

const double g = 9.81;          // Przyśpieszenie ziemskie

int los(int a, int b)           // Losowa liczba całkowita od a do b-1
{
    return rand() % (b - a) + a;
}

double los(double a, double b)  // Losowa liczba rzeczywista [a,b]
{
    return (double(rand()) / RAND_MAX) * (b - a) + a;
}

Kropla::Kropla(int x, int y, int kolor, int r) : Okrag(x, y, kolor, r)
{
    predkosc_Pocz();
    pokaz();
}

void Kropla::przesun(double dt)
{
    if (getmaxy() == 0) return;
    if (y <= getmaxy())
    {
        przesun_o(int(vx * dt), -int(vy * dt));
        vy -= g * dt;
    }
    else
    {
        przesun_do(getmaxx() / 2 + los(-10, 11), getmaxy());
        predkosc_Pocz();
    }
}

void Kropla::predkosc_Pocz()
{
    double vyMax = sqrt(2 * getmaxy() * g);
    vx = los(0.03, 0.12) * vyMax;
    if (rand() % 2) vx = -vx;
    vy = los(0.75, 0.99) * vyMax;
}

Konstruktor Kropla definiowanej klasy pochodnej po wywołaniu konstru­ktora klasy bazowej inicjali­zuje pola vxvy tworzonego obiektu i pokazuje na ekranie reprezen­towaną przez niego kroplę (okrąg), korzy­stając z prywatnej metody predkosc_Pocz klasy pochodnej i odziedzi­czonej metody pokaz. Metoda predkosc_Pocz wyznacza najpierw wartość maksy­malną prędkości pionowej, przy której kropla nie przekroczy górnej krawędzi obszaru robo­czego okna, a potem obie składowe wektora prędkości początkowej jako liczby losowe z zakresu 3÷12%75÷99% obliczonej uprzednio wartości maksy­malnej (dla składowej poziomej losowany jest również jej znak).

Zadaniem metody przesun jest przesu­nięcie kropli do nowej pozycji i uaktual­nienie jej prędkości pionowej zgodnie z przedsta­wionym powyżej schematem oblicze­niowym wyraża­jącym zmianę parametrów rzutu ukośnego w odstępie czasu, jaki upłynął od osta­tniego do bieżącego momentu czasowego (zmiana znaku drugiego argu­mentu w wywo­łaniu metody przesun_o jest konse­kwencją odwro­tnego kierunku osi y na ekranie). Jednak obli­czenia te mają sens tylko wtedy, gdy kropla jest widoczna na ekranie. Gdy opadła poniżej dolnej krawędzi obszaru roboczego okna, jest przesu­wana do środka tej krawędzi (z niewielkim zabu­rzeniem w lewo lub w prawo), nadawana jest jej też prędkość ku górze za pomocą metody predkosc_Pocz. W ten sposób krople, które wydostają się poza obszar roboczy okna, są z powrotem kierowane do stru­mienia tryska­jącego z fontanny.

Implementacja listy i program "Fontanna"

Najprostszy program przedstawiający strumień wody fontanny złożony z wielu obiektów klasy Kropla funkcjonu­jących nieza­leżnie od siebie można zbudować, korzystając z tablicy staty­cznej o z góry ustalonej liczbie elementów, którymi są wskaźniki na te obiekty. Oto przykład takiego programu dla tablicy 500-elemen­towej:

#include "krople.h"

int main()
{
    srand((unsigned)time(NULL));
    initwindow(640, 480);
    Kropla *kropla[500];
    for (int k = 0; k < 500; k++)
        kropla[k] = new Kropla(getmaxx() / 2, getmaxy(), los(1, 16), los(2, 6));
    while (!kbhit())
        for (int k = 0; k < 500; k++)
            kropla[k]->przesun(0.25);
    for (int k = 0; k < 500; k++)
        delete kropla[k];
    closegraph();
    return 0;
}

Każdy z 500 obiektów jest tworzony za pomocą operatora new i konstru­ktora klasy Kropla, a wygene­rowany wskaźnik na ten obiekt zostaje przypi­sany kolejnemu elemen­towi tablicy. Gdy użytkownik przerwie animację, naciskając dowolny klawisz, wszystkie obiekty są jeden po drugim niszczone za pomocą operatora delete. Rzecz jasna projekt programu wymaga włączenia modułów klas Punkt, OkragKropla.

Wadą programu jest stała, ściśle określona liczba elementów tablicy. Wprawdzie można użyć tablicy dynami­cznej, jednak tylko w przypadku inicjali­zowania jej elementów za pomocą tzw. konstru­ktora domyślnego, który cechuje się brakiem argu­mentów lub ma tylko argu­menty domyślne. Klasa Kropla takiego konstru­ktora nie ma, a nawet gdyby miała, jego przydatność byłaby wątpliwa, gdyż wszystkie obiekty inicjalizo­wałby takimi samymi wartościami.

Problem nieznanej w trakcie tworzenia programu liczby obiektów można rozwiązać, posłu­gując się listą jednokie­runkową składa­jącą się z elementów zawiera­jących pewne dane i wskaźnik na następny element (rys.). Dostęp do pierwszego elementu listy umożliwia dodatkowy wskaźnik. Ostatni element zawiera wskaźnik pusty (NULL w C/C++), który oznacza, że element nie ma nastę­pnika.

Użycie listy jednokie­runkowej wymaga zdefinio­wania struktury złożonej z pól reprezen­tujących dane i pole wskazujące na następny element listy. Poniższy zapis jest definicją struktury (nowego typu) o nazwie Element, której składni­kami są pola danych (oznaczone symboli­cznie trzema kropkami) i wskaźnik nast na zmienną typu Element. W definicji od razu zadekla­rowano zmienną poc o wartości NULL, co oznacza, że wstępnie lista jest pusta. Przypo­mnijmy, że dostęp do składników struktury umożliwia operator ->. Jeśli np. p jest wskaźni­kiem na strukturę typu Element (na element listy), to p->nast jest polem tej struktury (wskaźni­kiem na następny element listy).

struct Element
{
    ...             // Pola (dane)
    Element *nast;
} *poc = NULL;

Rozmiar listy może się zmieniać: elementy mogą być tworzone i wsta­wiane do listy, mogą też być z niej usuwane. Operacje te są bardzo proste, gdy dotyczą początku listy. Wstawienie nowego elementu na początek listy sprowadza się do uaktualnień dwóch wskaźników. Jeśli zmienna poc zawiera wskaźnik na pierwszy element listy, a zmienna p wskaźnik na wsta­wiany element, to operację tę można sformu­łować następu­jąco:

p->nast = poc;
poc = p;

Pierwsze przypisanie sprawia, że następnikiem elementu wskazy­wanego przez wskaźnik p jest dotych­czasowy pierwszy element listy (rys. poniżej z lewej), a drugie, że nowym pierwszym elementem listy jest element wskazy­wany przez wskaźnik p (rys. poniżej z prawej). Kod ten jest poprawny również w przypadku, gdy lista jest pusta.

Operacja wielokrotnego wstawiania nowego elementu na początek wstępnie pustej listy jest prostym sposobem genero­wania takiej listy. W przy­padku pominięcia manipu­lacji na danych wstawia­nych elementów proces ten jest realizo­wany w następu­jącym algorytmie tworzenia listy n-elemen­towej:

while (n-- > 0)
{
    Element *p = new Element;
    p->nast = poc;
    poc = p;
}

Usunięcie pierwszego elementu listy jednokie­runkowej sprowadza się tylko do uaktual­nienia wskaźnika poc. Należy jednak zachować wskaźnik na usuwany element, by go nie zgubić. Element ten może być bowiem nadal potrzebny w programie, a jeśli nie, trzeba zwolnić przydzie­loną mu pamięć, by nie dopuścić do jej wycieku. Korzy­stając zatem z pomocni­czego wskaźnika p, zadanie to można wyrazić następu­jąco (rys.):

poc = (p = poc)->nast;

Zwolnienie pamięci wszystkich elementów listy, gdy nie są wymagane dodatkowe czynności przy ich usuwaniu, jest prostą operacją sekwen­cyjną. Mianowicie dopóki lista nie jest pusta, należy usunąć jej pierwszy element i zwolnić zajmowany przez niego obszar pamięci:

Element *p;
while (poc != NULL)
{
    poc = (p = poc)->nast;
    delete p;
}

Pomiędzy elementami listy jednokie­runkowej można przecho­dzić tylko w jednym kierunku, od jej początku do końca, wykorzy­stując wskaźnik na następny element. Cofnięcie się do poprze­dnika wskazy­wanego elementu nie jest możliwe. Przecho­dzenie po wszystkich elemen­tach listy wymaga użycia wskaźnika pomocni­czego, który na początku wskazuje na pierwszy element listy i po wykonaniu każdego kroku pętli jest przesu­wany tak, by wskazywał na następny element. Przyjmijmy, że określa go zmienna wskaźni­kowa p. Pętla kończy się, gdy p uzyska wartość NULL określa­jącą, że nie ma więcej elementów listy:

for (Element *p = poc; p != NULL; p = p->nast)
{
    ...     // Operacje na elemencie wskazywanym przez p
}

Przejdźmy wreszcie do implementacji fontanny za pomocą listy jednokie­runkowej obiektów reprezentu­jących krople wody. Każdy element tej listy jest strukturą złożoną z dwóch pól wskaźni­kowych (rys.), a miano­wicie wskaźnika kropla na obiekt klasy Kropla i wskaźnika nast na następny element listy:

struct Element
{
    Kropla *kropla;
    Element *nast;
} *poc = NULL;

Omówione powyżej operacje tworzenia listy jednokierun­kowej i zwalniania pamięci wymagają większej uwagi z powodu obsługi pola kropla. Po pierwsze, gdy nowo utworzony element ma być wstawiony na początek listy, jego pole kropla powinno zawierać wskaźnik wygene­rowany za pomocą opera­tora new i konstru­ktora klasy Kropla:

while (n-- > 0)
{
    Element *p = new Element;
    p->kropla = new Kropla(getmaxx() / 2, getmaxy(), los(1, 16), los(2, 6));
    p->nast = poc;
    poc = p;
}

Po drugie, zwolnienie pamięci usuniętego z listy elementu powinno zawsze nastąpić po uprzednim zniszczeniu za pomocą opera­tora delete obiektu wskazy­wanego przez pole kropla tego elementu:

Element *p;
while (poc != NULL)
{
    poc = (p = poc)->nast;
    delete p->kropla;
    delete p;
}

Przesunięcie każdej wyimaginowanej kropli strumienia fontanny do nowej pozycji ekranowej polega na sekwen­cyjnym przecho­dzeniu po wszystkich elemen­tach listy i wyko­naniu metody przesun obiektu przypi­sanego temu elementowi:

for (Element *p = poc; p != NULL; p = p->nast)
    p->kropla->przesun(0.25);

Pełny program imitujący fontannę, w którym trzy omówione wyżej operacje na liście obiektów klasy Kropla zostały wyodrę­bnione jako trzy funkcje o sugesty­wnych nazwach, jest przedsta­wiony na poniższym listingu. Liczba obiektów reprezen­tujących krople wody stru­mienia fontanny jest generowana losowo z zakresu od 750 do 1500.

#include <iostream>
#include "krople.h"

const double DT = 0.25;     // Odstęp czasowy

struct Element
{
    Kropla *kropla;
    Element *nast;
} *poc = NULL;

void fontanna_Utworz(int n)
{
    while (n-- > 0)
    {
        Element *p = new Element;
        p->kropla = new Kropla(getmaxx() / 2, getmaxy(), los(1, 16), los(2, 6));
        p->nast = poc;
        poc = p;
    }
}

void fontanna_Usun()
{
    Element *p;
    while (poc != NULL)
    {
        poc = (p = poc)->nast;
        delete p->kropla;
        delete p;
    }
}

void fontanna_Przesun()
{
    for (Element *p = poc; p != NULL; p = p->nast)
        p->kropla->przesun(DT);
}

int main()
{
    std::cout << "Koniec - dowolny klawisz\n";
    srand((unsigned)time(NULL));
    initwindow(800, 600, "Fontanna");
    fontanna_Utworz(los(750, 1501));
    while (!kbhit())
        fontanna_Przesun();
    fontanna_Usun();
    closegraph();
    return 0;
}

Poniższy rysunek przedstawia obraz fontanny uchwycony po ok. jednej minucie działania programu (procesor i5-430M, grafika proce­sora). Z oczywi­stych powodów nie oddaje on w pełni prezento­wanego na ekranie zjawiska wygląda­jącego całkiem natu­ralnie.


Opracowanie przykładu: październik 2019