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

Przykład C++

Ruch harmoniczny tłumiony Parametry i funkcje ruchu w C++ Układ współrzędnych ekranowych Funkcja argumentem funkcji w C i C++ Program w C++ Poprzedni przykład Następny przykład Program w Visual C# Kontakt

Ruch harmoniczny tłumiony

Zadaniem programu jest sporządzenie wykresu funkcji opisu­jącej ruch harmoni­czny tłumiony, zwany również drganiem harmoni­cznym tłumionym. Wychy­lenie punktu material­nego z poło­żenia równowagi zmienia się w takim ruchu okresowo (periody­cznie, sinusoi­dalnie) i maleje wraz z upływem czasu za przyczyną siły hamującej (tarcie, lepkość, opór powie­trza itp.). Zależność wychy­lenia y drgają­cego punktu od czasu x można wyrazić za pomocą wzoru

w którym A oznacza amplitudę drgań (maksymalne wychy­lenie punktu), b – czynnik tłumiący, ω – częstotli­wość drgań (pulsację), φ – fazę początkową. Wartości A, bω są liczbami dodatnimi (dla b=0 nie ma tłumienia). Tak więc wychylenie punktu jest funkcją zawiera­jącą się pomiędzy dwiema obwiedniami

i znikającą w czasie. Ze względu na obecność funkcji cosinus w wyrażeniu opisu­jącym ruch harmo­niczny tłumiony wygodnie jest wyrażać czas x i fazę φ w stopniach lub radianach.

Parametry i funkcje ruchu w C++

Wizualizacja funkcji opisującej ruch harmo­niczny tłumiony i jej dwóch obwiedni wymaga określenia kilku parame­trów. Ze względu na skalowanie osi y przy przejściu od rzeczywi­stego do ekrano­wego układu współrzę­dnych amplituda A nie wpływa na wygląd rysunku, toteż możemy przypisać jest stałą wartość, np. równą 2. Ograni­czymy również wykres do z góry określo­nego przedziału czasowego od zera do T=360o. Ustalenia te prowadą do nieco prostszej wersji programu, jednak nie mają żadnego wpływu na sposób genero­wania wykresu funkcji na ekranie komputera. Pozostałe para­metry ruchu wczytujemy z klawia­tury:

double A = 2;                 // Amplituda drgań
int T = 360;                  // Przedział czasowy (stopnie)
double b;                     // Czynnik tłumiący
double omega;                 // Częstotliwość drgań (Hz)
double fi;                    // Faza początkowa (radiany)

void czytaj_dane()            // Wersja uproszczona
{
    cin >> b;
    cin >> omega;
    int faza;                 // Faza początkowa drgań
    cin >> faza;              // wczytywana w stopniach
    fi = M_PI * faza / 180;   // i zamieniana na radiany
}

Implementacja zapowiedzianych trzech funkcji wykorzystu­jących parametry ruchu harmoni­cznego tłumio­nego, w której unika się powta­rzania części wyrażeń arytmety­cznych, może wyglądać następu­jąco:

double Amax(double x)
{
    return A * exp(-b * x);
}

double Amin(double x)
{
    return -Amax(x);
}

double Ruch(double x)
{
    return Amax(x) * cos(omega * x + fi);
}

Układ współrzędnych ekranowych

Tworzenie wykresu funkcji na ekranie monitora kompute­rowego należy rozpocząć od określenia parametrów (zakresu współrzę­dnych) okna w układzie rzeczywistym, w którym zdefinio­wana jest funkcja, jak i widoku w układzie ekranowym, w którym ma być ona zobrazowana. Oczywistym elementem wzbogaca­jącym rysunek będzie powszechnie stosowany prostokątny układ dwóch odcinków zakończo­nych strzałkami symbolizu­jący osie x i y. Naturalnie, ze względów estety­cznych zakresy tych odcinków powinny być nieco większe od rozmiarów widoku i nieco mniejsze od rozmiarów obszaru roboczego okna grafi­cznego. Długość i szerokość strzałek może być równa, odpowie­dnio, 8 i 6 pikseli. Na koniec tych wstępnych przygo­towań do genero­wania obrazu trzeba obliczyć czynniki skalujące używane przez odwzoro­wania xEyE opisujące przejście od współrzę­dnych rzeczywi­stych okna do współrzę­dnych ekrano­wych widoku. Rozważania te prowadzą do następu­jących deklaracji zmiennych i definicji funkcji:

double xRmin, xRmax, yRmin, yRmax;  // Parametry układu rzeczywistego
int xEmin, xEmax, yEmin, yEmax;     // Parametry układu ekranowego
int xOmin, xOmax, yOmin, yOmax;     // Zakres osi układu ekranowego
double sx, sy;                      // Czynniki skalujące

void ustaw_parametry()
{
    xRmin = 0;
    xRmax = M_PI * T / 180;
    yRmin = -A;
    yRmax = A;
    xOmin = getmaxx() / 25;
    xOmax = getmaxx() - xOmin;
    xEmin = 2 * xOmin;
    xEmax = xOmax - xOmin - 8;
    yOmax = getmaxy() / 25;
    yOmin = getmaxy() - yOmax;
    yEmax = 2 * yOmax + 8;
    yEmin = yOmin - yOmax;
    sx = (xEmax - xEmin) / (xRmax - xRmin);
    sy = (yEmax - yEmin) / (yRmax - yRmin);
}

Kreślenie osi x, y układu kartezjań­skiego widoku stanie się proste, gdy najpierw wyznaczymy współrzędne ekranowe jego środka będącego obrazem początku (0,0) układu rzeczywi­stego w przekształ­ceniach xEyE. Takie rozwią­zanie zostało przyjęte w poniższej funkcji, która rysuje odcinki obu osi układu współrzę­dnych widoku, kończąc je strzałkami i opisując tradycyj­nymi jednoli­terowymi symbolami, a na koniec opisuje jego początek. Współ­rzędne opisów zostały dobrane eksperymen­talnie.

void rysuj_uklad()
{
    int xZero = xE(0), yZero = yE(0);
    line(xOmin, yZero, xOmax, yZero);
    line(xOmax - 8, yZero - 3, xOmax, yZero);
    line(xOmax - 8, yZero + 3, xOmax, yZero);
    outtextxy(xOmax - 10, yZero - 20, "x");
    line(xZero, yOmin, xZero, yOmax);
    line(xZero - 3, yOmax + 8, xZero, yOmax);
    line(xZero + 3, yOmax + 8, xZero, yOmax);
    outtextxy(xZero - 15, yOmax - 4, "y");
    outtextxy(xZero - 14, yZero - 16, "O");
}

Funkcja argumentem funkcji w C i C++

Ostatnim etapem precyzowania programu jest zaprogra­mowanie operacji kreślenia wykresu funkcji opisującej ruch harmoni­czny tłumiony, a w istocie trzech wykresów obejmu­jących także jej górną i dolną obwiednię. Te trzy funkcje są zdefinio­wane w tym samym, wspólnym oknie w układzie rzeczy­wistym. Naturalnym oczeki­waniem jest więc zbudowanie podpro­gramu (funkcji C++), który rysuje wykres dowolnej funkcji określonej w argu­mencie. Potrójny wykres byłby wówczas efektem trzykro­tnego wywołania tego podprogramu: dla funkcji Amax, AminRuch.

Postawione zadanie dotyczy problemu przekazy­wania parame­trów, które reprezen­tują funkcje. W celu jego rozwiązania przypo­mnijmy pokrótce, że obiekty wyszczegól­nione w nagłówku funkcji i występu­jące w jej treści określa się jako parametry, zaś występu­jące w każdym jej wywołaniu i podsta­wiane zamiast parametrów jako argumenty. Gdy argu­ment wywołania reprezen­tuje funkcję, jest jej nazwą. Tak więc w rozpatry­wanym przypadku prawidło­wymi argumen­tami w wywo­łaniach niesformu­łowanej jeszcze funkcji, którą nazwiemy rysuj_wykres, będą nazwy Amax, AminRuch:

int main()
{
    ...
    rysuj_wykres(Amax);
    rysuj_wykres(Amin);
    rysuj_wykres(Ruch);
    ...
    return 0;
}

De facto nazwa funkcji jest w językach C i C++ wskaźnikiem na tę funkcję. Innymi słowy, jest adresem miejsca pamięci, w którym przechowy­wany jest kod funkcji. Zauważmy, że skoro nazwa funkcji jest wskaźnikiem, operator & nie jest przed nią potrzebny, podobnie jak przed nazwą tablicy.

Parametr reprezentu­jący funkcję jest również typu wskaźni­kowego, ale jego specyfi­kacja jest nieco zawiła, wymaga bowiem określenia typu argu­mentów tej funkcji i typu zwracanej przez nią wartości. W przy­padku zapowie­dzianej funkcji rysuj_wykres prawidłowym parame­trem określa­jącym funkcję, której wykres ma być utworzony, jest

double (*f)(double)

Zapis ten orzeka, że f jest wskaźnikiem na funkcję (lokalną nazwą funkcji) jednej zmiennej typu double zwracającej wynik typu double. Pierwsza para nawiasów jest bardzo istotna, ponieważ bez niej zapis określałby funkcję zwraca­jącą wskaźnik na wartość typu double.

Po tych rozważaniach możemy wreszcie przejść do sformu­łowania podpro­gramu ryso­wania wykresu funkcji określonej przez parametr. Oczywiście zamiast krzywej przedsta­wiającej graficznie funkcję rysujemy łamaną o odpo­wiednio dużej liczbie wierz­chołków leżących na tej krzywej. Sama technika ryso­wania polega na przenie­sieniu pisaka do wierz­chołka początko­wego łamanej i rysowaniu wszystkich jej kolejnych odcinków łączących sąsiadu­jące dwa wierzchołki:

void rysuj_wykres(double (*f)(double))
{
    int n = xEmax - xEmin;
    moveto(xEmin, yE(f(xRmin)));
    for (int k = 1; k <= n; k++)
        lineto(xEmin + k, yE(f(k / sx + xRmin)));
}

Być może wyjaśnienia wymaga sposób wyznaczania wartości współrzę­dnych wierz­chołków łamanej. Przy przyjętym ich zagęszczeniu wierz­chołek o indeksie k ∈ {0,1,...,n} ma w układzie ekra­nowym współ­rzędną x równą wartości wyra­żenia xEmin+k. W układzie rzeczy­wistym odpo­wiada mu dokła­dnie jeden punkt o współ­rzednej x równej wartości k/sx+xRmin i współ­rzędnej y równej wartości f(k/sx+xRmin). Oznacza to, że współ­rzędną y tego wierz­chołka jest w układzie ekra­nowym wartość wyra­żenia yE(f(k/sx+xRmin)).

Inny sposób rysowania wykresu funkcji przedstawia załączony poniżej podprogram. Wszystkie wierz­chołki łamanej zostają najpierw zapamię­tane w tablicy elementów całko­witych, z których każda para zawiera współ­rzędne x i y kolej­nego wierz­chołka. Dopiero po tej operacji łamana jest rysowana na raz za pomocą funkcji drawpoly, która wymaga podania liczby wierz­chołków i tablicy ich współ­rzędnych:

void rysuj_wykres(double (*f)(double))
{

    int n = xEmax - xEmin;
    int *P = new int[2 * n + 2];
    for (int k = 0; k <= n; k++)
    {
        P[2 * k] = xEmin + k;
        P[2 * k + 1] = yE(f(k / sx + xRmin));
    }
    drawpoly(n + 1, P);
    delete[] P;
}

Program w C++

Na poniższym listingu zaprezentowany jest pełny program, który wczytuje parametry ruchu harmoni­cznego tłumionego (oprócz amplitudy i zakresu czasowego) oraz wyświetla na ekranie wykres tego ruchu w zależności od czasu. Program zawiera pierwszą wersję funkcji rysowania wykresu.

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

using namespace std;

double A = 2;                       // Amplituda drgań
int T = 360;                        // Przedział czasowy (stopnie)
double b;                           // Czynnik tłumiący
double omega;                       // Częstotliwość drgań (Hz)
double fi;                          // Faza początkowa (radiany)

double xRmin, xRmax, yRmin, yRmax;  // Parametry układu rzeczywistego
int xEmin, xEmax, yEmin, yEmax;     // Parametry układu ekranowego
int xOmin, xOmax, yOmin, yOmax;     // Zakres osi układu ekranowego
double sx, sy;                      // Czynniki skalujące

double Amax(double x)
{
    return A * exp(-b * x);
}

double Amin(double x)
{
    return -Amax(x);
}

double Ruch(double x)
{
    return Amax(x) * cos(omega * x + fi);
}

void czytaj_dane()
{
    cout << "Ruch harmoniczny t\x88umiony:\n--------------------------\n";
    cout << "Amplituda            : " << A << endl;
    cout << "Przedzia\x88 czasowy (\xf8): " << T << endl;
    cout << "Czynnik t\x88umi\xa5" "cy     : ";
    cin >> b;
    cout << "Cz\xa9stotliwo\x98\x86 (Hz)   : ";
    cin >> omega;
    cout << "Faza pocz\xa5tkowa (\xf8)  : ";
    int faza;                       // Faza początkowa (stopnie)
    cin >> faza;
    fi = M_PI * faza / 180;
}

void ustaw_parametry()
{
    xRmin = 0;
    xRmax = M_PI * T / 180;
    yRmin = -A;
    yRmax = A;
    xOmin = getmaxx() / 25;
    xOmax = getmaxx() - xOmin;
    xEmin = 2 * xOmin;
    xEmax = xOmax - xOmin - 8;
    yOmax = getmaxy() / 25;
    yOmin = getmaxy() - yOmax;
    yEmax = 2 * yOmax + 8;
    yEmin = yOmin - yOmax;
    sx = (xEmax - xEmin) / (xRmax - xRmin);
    sy = (yEmax - yEmin) / (yRmax - yRmin);
}

int xE(double x)
{
    return int(sx * (x - xRmin)) + xEmin;
}

int yE(double y)
{
    return int(sy * (y - yRmin)) + yEmin;
}

void rysuj_uklad()
{
    int xZero = xE(0), yZero = yE(0);
    line(xOmin, yZero, xOmax, yZero);
    line(xOmax - 8, yZero - 3, xOmax, yZero);
    line(xOmax - 8, yZero + 3, xOmax, yZero);
    outtextxy(xOmax - 10, yZero - 20, "x");
    line(xZero, yOmin, xZero, yOmax);
    line(xZero - 3, yOmax + 8, xZero, yOmax);
    line(xZero + 3, yOmax + 8, xZero, yOmax);
    outtextxy(xZero - 15, yOmax - 4, "y");
    outtextxy(xZero - 14, yZero - 16, "O");
}

void rysuj_wykres(double (*f)(double))
{
    int n = xEmax - xEmin;
    moveto(xEmin, yE(f(xRmin)));
    for (int k = 1; k <= n; k++)
        lineto(xEmin + k, yE(f(k / sx + xRmin)));
}

int main()
{
    czytaj_dane();
    initwindow(720, 480, "Ruch harmoniczny tłumiony");
    ustaw_parametry();
    setcolor(LIGHTGRAY);
    rysuj_uklad();
    setcolor(LIGHTCYAN);
    rysuj_wykres(Amax);
    rysuj_wykres(Amin);
    setcolor(LIGHTRED);
    setlinestyle(SOLID_LINE, 0, 2);
    rysuj_wykres(Ruch);
    getch();
    closegraph();
    return 0;
}

Wypada zwrócić uwagę na drobne różnice kodu źródłowego tego programu dla trzech rozpatry­wanych kompila­torów. I tak, kompilator MinGW C++ ma zastrze­żenia do zapisu łańcuchów wyświe­tlanych przez funkcję outtextxy wywoły­waną trzykrotnie w funkcji rysuj_uklad. Uznaje, że są one stałymi typu string niezgo­dnymi ze specyfi­kacją zawartą w dekla­racji funkcji outtextxy, według której powinny być stałymi starego typu char*. Ostrzeżeń można uniknąć, wymuszając jawną konwersję, np.

outtextxy(xOmax - 10, yZero - 20, (char*)"x");

Z kolei kompilator Visual C++ potraktuje użycie stałej M_PI w funkcjach czytaj_­daneustaw_­parametry jako błąd, gdy nie zdefi­niuje się symbolu _USE_MATH_DEFINES w dyrektywie #define przed włącze­niem biblio­teki matema­tycznej lub nie wpisze się go do listy definicji preprocesora. Rozpoczy­namy zatem program dyrektywą:

#define _USE_MATH_DEFINES

A oto przykładowy wykres ruchu harmoni­cznego tłumio­nego wygenero­wany przez program dla danych wyświe­tlonych w oknie konsoli:


Opracowanie przykładu: luty 2019