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

Przykład C++

Układ planetarny Prawo powszechnej grawitacji Model komputerowy układu planetarnego Program w C++ Rozwiązanie obiektowe Program w C++ (wersja 2) Poprzedni przykład Następny przykład Program w Visual C# Kontakt

Układ planetarny

Naszym zadaniem jest opracowanie programu w języku C++, który czyta dane dotyczące hipotety­cznego układu planetar­nego z pliku tekstowego o podanej z klawia­tury nazwie, a następnie prezentuje za pomocą animacji graficznej w WinBGI zachowanie się tego układu w czasie. Zakładamy, że w pierwszym wierszu pliku jest podana liczba ciał układu (gwiazd, planet, księżyców, komet), a w każdym następnym po sześć liczb (pięć rzeczy­wistych i jedna całko­wita) oddzie­lonych spacjami i stano­wiących parametry kolej­nego ciała (rys.):

Wprowa­dzanie tylu liczb z klawia­tury w trakcie wykonania programu byłoby uciążliwe, lepiej użyć pliku tekstowego. Dodatkowym atutem takiego rozwią­zania jest możliwość łatwej modyfi­kacji danych, która przy dużej niestabil­ności układu wynika­jącej z nieauten­tyczności danych jest bardzo pożądana. Współ­rzędne pozycji ciał i składowe ich prędkości są dostoso­wane do płaskiego obszaru roboczego okna o rozmiarze 800x600 pikseli. Założenie to nie jest pozbawione sensu, gdyż orbity planet Układu Słone­cznego leżą niemal w jednej płaszczyźnie.

Prawo powszechnej grawitacji

Niech ciało o masie m1 znajduje się w punkcie (x1,y1), a ciało o masie m2 w punkcie (x2,y2). Prawo powsze­chnego ciążenia Newtona, zwane również prawem powsze­chnej grawi­tacji, orzeka, że siła działająca pomiędzy tymi ciałami jest siłą przycią­gającą, skierowaną wzdłuż prostej łączącej te punkty, i ma wartość

gdzie G jest stałą uniwer­salną, a r jest odległo­ścią pomiędzy tymi punktami:

Prawo powsze­chnego ciążenia możemy przedstawić w postaci wekto­rowej, która określa składowe Fx i Fy siły F w układzie współrzę­dnych x i y. Miano­wicie składowe siły przycią­gania, jakiej ciało m1 doznaje ze strony ciała m2, mają wartości:

Nietrudno uogólnić te wzory dla dowolnej liczby n≥2 ciał o masach m1, m2, ..., mn. Zakładamy, że znajdują się one na wspólnej płaszczy­źnie w punktach (x1,y1), (x2,y2), ..., (xn,yn). Składowe FxpFyp siły wypad­kowej Fp, jakiej doznaje ciało mp ze strony wszystkich pozosta­łych ciał, wyrażają się wzorami:

w których

Sile Fp przyciągania ciała mp przez wszystkie pozostałe ciała towarzyszy przyśpie­szenie wyrażające zmianę prędkości tego ciała w czasie. Oznaczmy go przez ap. Związek pomiędzy działa­jącą na ciało siłą a przyśpie­szeniem i masą określa druga zasada dynamiki Newtona:

Stąd i podanych wyżej wzorów określa­jących składowe siły przycią­gania wynika, że składowe axp i ayp przyśpie­szenia ap wynoszą:

Model komputerowy układu planetarnego

Niech ciało mp porusza się z prędko­ścią vp o skła­dowych vxp i vyp. Jeżeli dt jest niewielkim odstępem czasowym, to po jego upływie nową pozycię ciała można wyznaczyć w sposób przybliżony według schematu:

Równie prosto można obliczyć nowe składowe prędkości ciała po upływie odstępu dt, której zmianę w czasie określa przyśpie­szenie:

Wzory te posłużą do zobrazowania ruchu ciał na ekranie monitora kompute­rowego. Przyjmiemy, że uniwer­salna stała grawi­tacji G jest równa 1. Takie założenie nie ogranicza istoty problemu, a jedynie ułatwia opraco­wanie programu, ponieważ nie trzeba wtedy używać dodatko­wego współczyn­nika skalowania dla danych dopaso­wanych do rozmiaru powierzchni ryso­wania. Powinniśmy również podjąć decyzję odnośnie ich reprezen­tacji. Narzuca­jącym się rozwią­zaniem jest implemen­tacja tablicowa, wygodna ze względu na obecność indeksów w powyższych wzorach.

Zaznaczanie kolorowym pikselem kolejnych pozycji ciała przy niewielkim odstępie czasowym dt (w programie określa go stała DT równa 0.1) prowadzi do utwo­rzenia krzywej stano­wiącej jego orbitę. Dodatkowo w celu uzyskania efektu ruchu będziemy obryso­wywawać punkcik małym kwadra­cikiem, a gdy ciało ma zmienić pozycję, usuwać ten obrys (rysowanie i usuwanie kwadra­cika wymaga włączenia odwraca­jącego trybu ryso­wania, w którym powtórne ryso­wanie tej samej linii powoduje jej zniknięcie). Funkcje rysowania i przesu­wania ciała mogą wyglądać nastę­pująco:

void rysujObiekt(int p, bool punkcik)
{
    int a = int(x[p]), b = int(y[p]);
    if (punkcik) putpixel(a, b, kolor[p]);
    rectangle(a - 1, b - 1, a + 1, b + 1);
}

void przesunObiekt(int p)
{
    rysujObiekt(p, false);
    x[p] += DT * vx[p];
    y[p] += DT * vy[p];
    rysujObiekt(p, true);
}

Funkcja rysuj­Obiekt rysuje kolorowy piksel reprezen­tujący ciało i otacza go małym kwadra­cikiem, gdy wartością argu­mentu punkcik jest true, bądź tylko usuwa kwadracik, gdy wartością tego argu­mentu jest false. Natomiast funkcja przesun­Obiekt likwiduje kwadracik wokół piksela, wyznacza współ­rzędne nowej pozycji ciała i na koniec rysuje piksel w nowym miejscu i otaczający go kwadracik.

Nieco bardziej złożona jest funkcja korygo­wania prędkości ciała po jego przesu­nięciu. Obliczenie nowych składowych prędkości wymaga bowiem sekwen­cyjnego przeglą­dania wszystkich pozosta­łych ciał celem wyzna­czenia przyśpie­szenia wypadko­wego wyraża­jącego szybkość zmiany prędkości tego ciała:

void korygujPredkosc(int p)
{
    double ax = 0, ay = 0, dx, dy, r3;
    for (int i = 0; i < n; i++)
        if (i != p)
        {
            dx = x[p] - x[i];
            dy = y[p] - y[i];
            r3 *= sqrt(r3 = dx * dx + dy * dy);
            ax -= m[i] * dx / r3;
            ay -= m[i] * dy / r3;
        }
    vx[p] += DT * ax;
    vy[p] += DT * ay;
}

Korzystając z omówionych trzech funkcji, łatwo jest sformu­łować fragment kodu źródło­wego programu prezentu­jący zachowanie się układu plane­tarnego w czasie:

setwritemode(XOR_PUT);
for (int p = 0; p < n; p++)
    rysujObiekt(p, true);
while (!kbhit())
{
    Sleep(15);
    for (int p = 0; p < n; p++)
        przesunObiekt(p);
    for (int p = 0; p < n; p++)
        korygujPredkosc(p);
}
setwritemode(COPY_PUT);

Najpierw ustawia się odwracający tryb rysowania (XOR_PUT) i rysuje wszystkie ciała w pozycjach początkowych. Następnie w każdym kroku pętli while wstrzy­muje się na chwilę wykonanie programu (przyjęto 15 milisekund), po czym przesuwa się ciała do nowych pozycji skorelo­wanych z upływem ustalo­nego odstępu czasowego i koryguje ich prędkości. Dzięki ozna­czaniu bieżącej pozycji ciał kwadra­cikami i wstrzy­mywaniu wykonania programu uzyskuje się efekt animacji. Na koniec, gdy pętla zostanie przerwana, przywraca się normalny tryb ryso­wania (COPY_PUT).

Program w C++

Pełny program w C++, który wczytuje ze strumienia skojarzo­nego z plikiem tekstowym do sześciu tablic dynami­cznych dane opisujące hipote­tyczny układ plane­tarny i prezentuje w oknie grafi­cznym ruch ciał tego układu, ma postać:

#include <iostream>
#include <fstream>
#include <string>
#include <cmath>
#include <graphics.h>

using namespace std;

const double DT = 0.1;          // Odstęp czasowy
const int PRZERWA = 15;         // Szybkość animacji

int n = 0;                      // Liczba obiektów
double *m = NULL;               // Masy
double *x = NULL, *y = NULL;    // Współrzędne pozycji
double *vx = NULL, *vy = NULL;  // Składowe prędkości
int *kolor = NULL;              // Kolory

void error(string err)
{
    cout << err << endl;
    getchar();
}

void zwolnijPamiec()
{
    delete[] kolor;
    delete[] vy;
    delete[] vx;
    delete[] y;
    delete[] x;
    delete[] m;
}

bool czytajDane()
{
    cout << "Nazwa pliku: ";
    string s;
    cin >> s;
    ifstream plik(s.c_str());
    if (!plik)
    {
        error("Nieudane otwarcie pliku");
        return false;
    }
    if (!(plik >> n && n > 1))
    {
        error("Problem z liczbą obiektów.");
        return false;
    }
    m = new double[n];
    x = new double[n];
    y = new double[n];
    vx = new double[n];
    vy = new double[n];
    kolor = new int[n];
    for (int k = 0; k < n; k++)
    {
        if (!(plik >> m[k] >> x[k] >> y[k] >> vx[k] >> vy[k] >> kolor[k]))
        {
            zwolnijPamiec();
            error("Nieudany odczyt liczby.");
            return false;
        }
    }
    return true;
}

void rysujObiekt(int p, bool punkcik)
{
    int a = int(x[p]), b = int(y[p]);
    if (punkcik) putpixel(a, b, kolor[p]);
    rectangle(a - 1, b - 1, a + 1, b + 1);
}

void przesunObiekt(int p)
{
    rysujObiekt(p, false);
    x[p] += DT * vx[p];
    y[p] += DT * vy[p];
    rysujObiekt(p, true);
}

void korygujPredkosc(int p)
{
    double ax = 0, ay = 0, dx, dy, r3;
    for (int i = 0; i < n; i++)
        if (i != p)
        {
            dx = x[p] - x[i];
            dy = y[p] - y[i];
            r3 *= sqrt(r3 = dx * dx + dy * dy);
            ax -= m[i] * dx / r3;
            ay -= m[i] * dy / r3;
        }
    vx[p] += DT * ax;
    vy[p] += DT * ay;
}

int main()
{
    if (!czytajDane())
        return 1;
    initwindow(800, 600, "Układ planetarny");
    setwritemode(XOR_PUT);
    for (int p = 0; p < n; p++)
        rysujObiekt(p, true);
    while (!kbhit())
    {
        Sleep(PRZERWA);
        for (int p = 0; p < n; p++)
            przesunObiekt(p);
        for (int p = 0; p < n; p++)
            korygujPredkosc(p);
    }
    setwritemode(COPY_PUT);
    closegraph();
    zwolnijPamiec();
    return 0;
}

Poniższy rysunek przedstawia orbity ciał wygene­rowane przez program po jednej minucie i 30 sekun­dach trwania animacji. Plik wejściowy zawierał dane dla układu złożonego z sześciu ciał, z których jedno o najwię­kszej masie symboli­zuje gwiazdę (kolor żółty), trzy są planetami obiega­jącymi gwiazdę po niemal kołowych orbitach (kolor turku­sowy, zielony i niebieski), a dwa najmniejsze to komety porusza­jące się po wydłu­żonej elipsie (kolor fioletowy) i hiperboli (kolor brązowy):

   m       x     y     vx    vy   kolor
  8000    400   300     0     0    14
  0.8     475   300     0   -10     3
  0.6     250   300     0     7     2
  1.25    400    60    -6     0     9
  0.0001   50     0     1     3.25  5
  0.0001  800   600    -4    -6     6

Niepozorny, nieco rozmazany kwadracik w centrum układu wyobraża gwiazdę. Bardziej odpowie­dnim jej reprezen­tantem byłby np. obrazek gwiazdki, ryso­wanie go wymaga­łoby jednak modyfi­kacji programu. Inny sposób wyekspono­wania gwiazdy polega na zastą­pieniu jej dwoma krążącymi blisko siebie ciałami symboli­zującymi gwiazdę podwójną. Poniższy rysunek utwo­rzony przez program w podobnym jak poprze­dnio czasie ukazuje orbity siedmiu ciał dla tożsamych danych z gwiazdą podwójną (kolor żółty i czerwony) zamiast poje­dynczej:

   m       x     y     vx    vy   kolor
  4000    400   295   -11     0    14
  4000    400   305    11     0    12
  0.8     475   300     0   -10     3
  0.6     250   300     0     7     2
  1.25    400    60    -6     0     9
  0.0001   50     0     1     3.25  5
  0.0001  800   600    -4    -6     6

Porównując obydwa rysunki nietrudno zauważyć, że chociaż łączna masa gwiazdy podwójnej jest równa masie gwiazdy pierwo­tnej, orbity pozosta­łych ciał nieco się różnią. Zaburzona orbita elipty­czna komety na drugim rysunku nasuwa skoja­rzenie z kometą Halleya, która wraca w pobliże Słońca średnio co 75 lat. Jej ruch po wydłu­żonej elipsie jest zakłócany przez planety Układu Słone­cznego, głównie Jowisza i Saturna. Przejście komety blisko Ziemi w 1910 roku (22,4 mln km) było szcze­gólnie widowi­skowe, zaś ostatnie w 1986 roku (150 mln km) bardzo rozczaro­wujące.

Rozwiązanie obiektowe

Wadą zastosowanej w powyższym programie implemen­tacji tabli­cowej danych opisu­jących hipote­tyczny układ plane­tarny jest rozpro­szenie informacji o każdym ciele w różnych miejscach (sześciu tablicach). Można tego uniknąć, używając tylko jednej tablicy dynami­cznej, której elemen­tami są struktury:

struct Obiekt
{
    double m;                   // Masa
    double x, y;                // Współrzędne pozycji
    double vx, vy;              // Składowe prędkości
    int kolor;                  // Kolor obiektu
} *ob = NULL;                   // Tablica struktur

Każda struktura zawiera pola (dane) dotyczące jednego ciała. Przydział pamięci takiej tablicy, gdy znana jest liczba ciał układu, a na końcu jej zwolnienie odbywa­łoby się za pomocą prostych instrukcji:

ob = new Obiekt[n];
...
if (ob != NULL) delete[] ob;

Dostęp do danych ciała umieszczonych w polach struktury będącej elementem tablicy umożliwia konstru­kcja złożona ze zmiennej indekso­wanej, operatora kropki i nazwy pola. Na przykład funkcje rysowania i przesu­wania ciała miałyby wtedy bardziej czytelną postać:

void rysujObiekt(int p, bool punkcik)
{
    int a = int(ob[p].x), b = int(ob[p].y);
    if (punkcik) putpixel(a, b, ob[p].kolor);
    rectangle(a - 1, b - 1, a + 1, b + 1);
}

void przesunObiekt(int p)
{
    rysujObiekt(p, false);
    ob[p].x += DT * ob[p].vx;
    ob[p].y += DT * ob[p].vy;
    rysujObiekt(p, true);
}

Nie jest to jednak rozwiązanie w pełni satysfakcjo­nujące, albowiem tak zdefinio­wany obiekt hipote­tyczny nie oddaje sposobu postrze­gania obiektu rzeczywi­stego przez człowieka. Obiekt rzeczy­wisty nie tylko ma swoje cechy, jak np. masa, pozycja w prze­strzeni i nawet kolor (Słońce jest żółte, Ziemia niebieska, Mars rdzawo­czerwony, Księżyc srebrny), ale charakte­ryzuje się też pewnym zacho­waniem, np. zmienia swoją pozycję i prędkość, przyciąga inne ciała. Dzieje się to oczywiście pod wpływem wszecho­becnej grawitacji, ale obserwator zawsze kojarzy zacho­wanie obiektu z jego cechami. Jednakże opisujące to zacho­wanie funkcje w powyższym programie i przykła­dzie nie stanowią jednej całości wraz z danymi, na których działają, nie są formalnie powiązane ani ze sobą, ani z danymi.

W programowaniu zorientowanym obiektowo (ang. Object Oriented ProgrammingOOP) definiuje się klasy i tworzy według nich obiekty. Klasa jest typem stano­wiącym pewnego rodzaju wzorzec, na podstawie którego buduje się obiekty. Elementami składowymi klasy są pola (dane) określa­jące cechy obiektu i metody (funkcje) opisujące jego zacho­wanie. Z kolei każdy obiekt jest instancją (egzempla­rzem) pewnej klasy, ma swoje własne cechy (indywi­dualny zestaw pól) i zdefi­niowane w klasie zacho­wanie. Definicja klasy przypo­mina definicję struktury. Różnica polega na użyciu słowa kluczo­wego class zamiast struct i zadekla­rowaniu, oprócz pól, metod. Ponadto składniki klasy są podzielone na sekcje według specyfi­katorów określa­jących poziom dostępu do nich. Na przykład specyfi­kator public oznacza, że wymie­nione po nim składniki są dostępne w całym programie (publiczne), a private, że są dostępne tylko w klasie, w której zostały zadekla­rowane (prywatne). Domyślnie dostęp do składników klasy jest prywatny. Zaleca się, aby pola były prywatne, a ważne metody publiczne. Cechy (stan) obiektu są wtedy niedo­stępne bezpo­średnio z zewnątrz, można je zmieniać jedynie za pomocą odpowie­dnich metod.

Przejdźmy wreszcie do definicji klasy służącej do two­rzenia obiektów hipotety­cznego układu planetar­nego i opisującej ich zachowanie w czasie. Rozpoczy­namy od zdefinio­wania ogólnej składni klasy:

class Obiekt
{
    double m;                   // Masa
    double x, y;                // Współrzędne pozycji
    double vx, vy;              // Składowe prędkości
    int kolor;                  // Kolor obiektu
public:
    Obiekt(double masa, double xPoc, double yPoc, double vxPoc, double vyPoc, int kol);
    void rysuj(bool punkcik);
    void przesun();
    void korygujPredkosc(int n, Obiekt *ob[]);
};

Jak widać, klasa Obiekt składa się z sześciu pól prywa­tnych i czterech metod publi­cznych. Pola są definio­wane tak samo jak pola struktury, zaś metody są deklaro­wane jak nagłówki (prototypy) funkcji. Metoda Obiekt jest tzw. konstru­ktorem – specjalną metodą wywoły­waną, gdy tworzony jest obiekt. Jego zadaniem jest inicjali­zacja obiektu polegająca m.in. na nadaniu jego polom wartości początko­wych. Konstruktor ma nazwę taką samą jak klasa, nie zwraca żadnej wartości (nawet typu void) i jest deklaro­wany w sekcji public, by był dostępny z dowolnego miejsca spoza klasy. Pozostałe metody określają funkcjo­nalność obiektu, pełniąc taką samą rolę jak funkcje o podobnych nazwach w zaprezen­towanym powyżej programie. Nie mają jednak argumentu identyfi­kującego obiekt (indeks p), jako składniki obiektu mają bowiem bezpo­średni dostęp do jego pól.

Zadeklarowane metody klasy powinny być w programie zdefi­niowane. Ich definicje można umieszczać poza klasą w tym samym lub innym pliku, jak i wewnątrz niej, gdy kod metod jest krótki (zob. klasa Wieza w programie przeno­szenia wież Hanoi). Nazwy metod definio­wanych na zewnątrz klasy należy poprze­dzać nazwą klasy i opera­torem zakresu :: (dwa dwukropki), który określa przynale­żność tych metod do danej klasy. Definicje zewnętrzne metod klasy Obiekt mogą wyglądać nastę­pująco:

Obiekt::Obiekt(double masa, double xPoc, double yPoc, double vxPoc, double vyPoc, int kol)
{
    m = masa;
    x = xPoc;
    y = yPoc;
    vx = vxPoc;
    vy = vyPoc;
    kolor = kol;
}

void Obiekt::rysuj(bool punkcik)
{
    int a = int(x), b = int(y);
    if (punkcik) putpixel(a, b, kolor);
    rectangle(a - 1, b - 1, a + 1, b + 1);
}

void Obiekt::przesun()
{
    rysuj(false);
    x += DT * vx;
    y += DT * vy;
    rysuj(true);
}

void Obiekt::korygujPredkosc(int n, Obiekt *ob[])
{
    double ax = 0, ay = 0, dx, dy, r3;
    for (int i = 0; i < n; i++)
        if (this != ob[i])
        {
            dx = x - ob[i]->x;
            dy = y - ob[i]->y;
            r3 *= sqrt(r3 = dx * dx + dy * dy);
            ax -= ob[i]->m * dx / r3;
            ay -= ob[i]->m * dy / r3;
        }
    vx += DT * ax;
    vy += DT * ay;
}

Być może wyjaśnienia wymaga kod metody Koryguj­Predkosc, której zadaniem jest wyzna­czenie składowych prędkości obiektu po jego przesu­nięciu do nowego miejsca. Argumen­tami metody są liczba wszystkich obiektów układu i tablica ich wskaźników. Dostęp do obiektów poprzez wskaźnik odbywa się na tej samej zasadzie jak do struktur. Mianowicie jeśli znany jest wskaźnik na obiekt, dostęp do tego obiektu umożliwia operator * (gwiazdka), a do jego pól i metod albo para operatorów gwiazdki i kropki, albo operator -> (strzałka złożona z dwóch znaków). Na przykład obiekt klasy Obiekt utworzony dynami­cznie:

Obiekt *p = new Obiekt(50, 400, 300, 10, -5, LIGHTBLUE);

można narysować na ekranie za pomocą instrukcji

(*p).rysuj();

albo

p->rysuj();

Rzecz jasna druga notacja jest wygodniejsza, toteż jest zazwyczaj stosowana. W metodzie Koryguj­Predkosc wskaźni­kami do przeglą­danych sekwen­cyjnie obiektów układu są elementy ob[i] tablicy ob, natomiast słowo kluczowe this oznacza wskaźnik identyfi­kujący obiekt, którego dotyczy wywołana metoda.

Powróćmy jeszcze na moment do konstru­ktora klasy Obiekt, który inicja­lizuje pola obiektu wartościami argumentów za pomocą sześciu przypisań. Inne rozwią­zanie polega na użyciu tzw. listy inicjali­zującej, którą umieszcza się w definicji konstru­ktora po nawiasie zamyka­jącym listę argumentów, poprze­dzając dwukropkiem. Jej elemen­tami są nazwy pól z zamknię­tymi w nawiasach wartościami początkowymi. Konstruktor Obiekt może być więc zdefinio­wany inaczej:

Obiekt::Obiekt(double masa, double xPoc, double yPoc, double vxPoc, double vyPoc, int kol)
    : m(masa), x(xPoc), y(yPoc), vx(vxPoc), vy(vyPoc), kolor(kol)
{}

Program w C++ (wersja 2)

Po tych rozważaniach możemy przejść do przekształ­cenia nieobie­ktowej wersji programu animacji układu planetar­nego na obiektową. Zaczynamy od zdefinio­wania zmiennej typu wskaźni­kowego potrzebnej do utworzenia tablicy dynami­cznej wskaźników obiektów reprezen­tujących wszystkie ciała układu:

Obiekt **op = NULL;

Wyjaśnijmy, że zapis ten jest interpre­towany jako pusty wskaźnik *op na pierwszy element jednowy­miarowej tablicy wskaźników typu Obiekt*, czyli jako jednowy­miarowa tablica op elementów wskazu­jących na obiekty klasy Obiekt. Gdy w funkcji czytajDane znana jest liczba ciał układu, można tej tablicy przydzielić pamięć, a nastę­pnie wczytywać z pliku dane każdego ciała i tworzyć reprezen­tujący je obiekt:

ob = new Obiekt*[n];
for (int k = 0; k < n; k++)
{
    double masa, xPoc, yPoc, vxPoc, vyPoc;
    int kolor;
    if (!(plik >> masa >> xPoc >> yPoc >> vxPoc >> vyPoc >> kolor))
    {
        zwolnijPamiec(k);
        error("Nieudany odczyt liczby.");
        return false;
    }
    ob[k] = new Obiekt(masa, xPoc, yPoc, vxPoc, vyPoc, kolor);
}

W przypadku błędu danych pamięć przydzie­lona dynami­cznie części obiektów i całej tablicy wskaźników jest zwalniana (w C++ nie ma mecha­nizmu automa­tycznego odśmie­cania pamięci). Ma się rozumieć, po zakoń­czeniu animacji należy zwolnić pamięć wszystkich obiektów i tablicy. Liczbę niszczonych obiektów wygodnie jest podać w argu­mencie funkcji:

void zwolnijPamiec(int k)
{
    while (k > 0)
        delete ob[--k];
    delete[] ob;
}

A oto wersja obiektowa programu konsolowego w C++ wczytującego z pliku tekstowego dane opisujące hipote­tyczny układ plane­tarny i prezentu­jącego w oknie grafi­cznym ruch ciał tego układu:

#include <iostream>
#include <fstream>
#include <string>
#include <cmath>
#include <graphics.h>

using namespace std;

const double DT = 0.1;          // Odstęp czasowy
const int PRZERWA = 15;         // Szybkość animacji

int n = 0;                      // Liczba obiektów

class Obiekt                    // Definicja obiektu
{                               // -----------------
    double m;                   // Masa
    double x, y;                // Współrzędne pozycji
    double vx, vy;              // Składowe prędkości
    int kolor;                  // Kolor obiektu
public:
    Obiekt(double, double, double, double, double, int);
    void rysuj(bool);
    void przesun();
    void korygujPredkosc(int, Obiekt *[]);
} **ob = NULL;                  // Tablica obiektów

Obiekt::Obiekt(double masa, double xPoc, double yPoc, double vxPoc, double vyPoc, int kol)
    : m(masa), x(xPoc), y(yPoc), vx(vxPoc), vy(vyPoc), kolor(kol)
{}

void Obiekt::rysuj(bool punkcik)
{
    int a = int(x), b = int(y);
    if (punkcik) putpixel(a, b, kolor);
    rectangle(a - 1, b - 1, a + 1, b + 1);
}

void Obiekt::przesun()
{
    rysuj(false);
    x += DT * vx;
    y += DT * vy;
    rysuj(true);
}

void Obiekt::korygujPredkosc(int n, Obiekt *ob[])
{
    double ax = 0, ay = 0, dx, dy, r3;
    for (int i = 0; i < n; i++)
        if (this != ob[i])
        {
            dx = x - ob[i]->x;
            dy = y - ob[i]->y;
            r3 *= sqrt(r3 = dx * dx + dy * dy);
            ax -= ob[i]->m * dx / r3;
            ay -= ob[i]->m * dy / r3;
        }
    vx += DT * ax;
    vy += DT * ay;
}

void error(string err)
{
    cout << err << endl;
    getchar();
}

void zwolnijPamiec(int k)
{
    while (k > 0)
        delete ob[--k];
    delete[] ob;
}

bool czytajDane()
{
    cout << "Nazwa pliku: ";
    string s;
    cin >> s;
    ifstream plik(s.c_str());
    if (!plik)
    {
        error("Nieudane otwarcie pliku");
        return false;
    }
    if (!(plik >> n && n > 1))
    {
        error("Problem z liczbą obiektów.");
        return false;
    }
    ob = new Obiekt*[n];
    for (int k = 0; k < n; k++)
    {
        double masa, xPoc, yPoc, vxPoc, vyPoc;
        int kolor;
        if (!(plik >> masa >> xPoc >> yPoc >> vxPoc >> vyPoc >> kolor))
        {
            zwolnijPamiec(k);
            error("Nieudany odczyt liczby.");
            return false;
        }
        ob[k] = new Obiekt(masa, xPoc, yPoc, vxPoc, vyPoc, kolor);
    }
    return true;
}

int main()
{
    if (!czytajDane())
        return 1;
    initwindow(800, 600, "Układ planetarny");
    setwritemode(XOR_PUT);
    for (int p = 0; p < n; p++)
        ob[p]->rysuj(true);
    while (!kbhit())
    {
        Sleep(PRZERWA);
        for (int p = 0; p < n; p++)
            ob[p]->przesun();
        for (int p = 0; p < n; p++)
            korygujPredkosc(p);
    }
    setwritemode(COPY_PUT);
    closegraph();
    zwolnijPamiec(n);
    return 0;
}

Na koniec przytoczmy jeszcze jeden przykład hipote­tycznego układu planetar­nego. Przy odrobinie cierpli­wości udało się metodą prób i błędów tak dobrać opisujące go dane, by animacja obrazo­wała obieg księżyca (kolor brązowy) wokół planety (kolor zielony) krążącej po orbicie wokół gwiazdy podwójnej:

   m       x     y     vx     vy    kolor
  10000   395   300    0.01   20     14
  10000   405   300    0.01  -20     12
  8       470   370   10     -11      9
  25      400   120  -11       0     10
  0.01    400   112  -12.5    -0.05   6
  10      150   300    0       9.5   13

Nawet niewielkie zmiany danych wprowa­dzanych do programu ujawniają, jak łatwo może dojść do katastrofy w takim układzie plane­tarnym. Na szczęście Układ Słoneczny wydaje się bardziej stabilny. Czy jednak na pewno? Nieraz słyszy się o upadłych meteory­tach i przelatu­jących niebezpie­cznie blisko asteroidach, a badacze dziejów Ziemi twierdzą, że przyczyną wymarcia dinozaurów 65 mln lat temu były zmiany klimatyczne o chara­kterze globalnym spowodo­wane uderzeniem meteorytu o średnicy 10 km w pobliżu półwyspu Jukatan. Szczególnie emocjo­nującym zjawiskiem, które miało miejsce w lipcu 1994 roku, było uderzenie komety Shoemaker-Levy 9 w Jowisza – największą planetę Układu Słone­cznego. Efekty były widoczne nawet przy użyciu amator­skiego teleskopu. Kometa została najpierw uwię­ziona na orbicie wokół Jowisza, potem rozer­wana na ponad 20 części pod wpływem dużych sił grawita­cyjnych, a na koniec wszystkie jej kawałki spadały na powierz­chnię planety (efektowne zdjęcia można znależć w inter­necie).


Opracowanie przykładu: lipiec 2019