Image
9.6.2016 0 Comments

C++ / Intermezzo / 9. časť

Späť na úvod >> Späť na programovanie >> Späť na seriál

V dnešnom pokračovaní seriálu si ukážeme trochu rozsiahlejší program, demonštrujúci väčšinu toho, čo sme dosiaľ prebrali. Je jasné, že v ňom nebudú použité všetky konštrukcie, taký ad hoc program by veľmi užitočný nebol. Dlho som rozmýšľal nad tým, čo by mal program vlastne robiť, nakoniec som sa rozhodol pre jednoduchú ukážku z oblasti matematiky (keď už sa tá škatuľa volá počítač, nech nám niečo spočíta) – pôjde o riešenie sústavy lineárnych rovníc a o výpočet koreňov reálneho polynómu. Program nie je ani najefektívnejší, ani najdokonalejší, slúži len ako príklad, preto má niektoré obmedzenia – dokáže riešiť sústavy lineárnych rovníc maximálne s tromi neznámymi a neošetruje všetky možné chyby, ktoré by pri zadávaní údajov alebo výpočte mohli nastať.

Trochu teórie

Prv než si niečo o programe povieme, dovolím si trošku teórie. Na riešenie sústav lineárnych rovníc existuje mnoho metód – analytických či numerických (iteračných). Väčšinu z nich nie je problém naprogramovať, ale ich algoritmus sa môže zdať príliš zložitý tomu, kto danú metódu nepozná. Na účely ukážkového príkladu som preto vybral jednu z najznámejších analytických metód, metódu determinantov. Ako je známe, sústavu lineárnych rovníc môžeme prehľadne zapísať maticovým zápisom

 

A . x = b

 

kde A je (štvorcová) matica sústavy (rozmeru n × n), x je hľadaný vektor riešení (n × 1) a b je vektor pravých strán (n × 1), kde n je počet neznámych (t. j. počet rovníc). Pri tomto značení i-ta rovnica sústavy má tvar ai1x1 + … + ainxn = bi, kde aij je prvok v i-tom riadku a j-tom stĺpci matice A a xi, bii-te prvky vektorov x a b. Jednotlivé neznáme xi môžeme vypočítať na základe jednoduchého vzťahu

 

xi = Di / DS

 

kde DS je determinant sústavy – normálny determinant matice A, Di sú determinanty upravenej matice Ai, ktorá vznikne náhradou i-teho stĺpca (kde i je index počítanej neznámej) vektorom pravých strán b.

Naprogramovať túto metódu je na pohľad veľmi jednoduché, spočítame najprv determinant DS a potom v cykle budeme počítať jednotlivé determinanty Di, deliť ich DS a vypisovať výsledky. Háčik je však v samotnom výpočte determinantu. Pre matice do rozmerov 3 × 3 existuje v podstate jednoduchý vzorec na priamy výpočet na základe prvkov matice, ale pre vyššie rozmery treba použiť rozvoj podľa niektorého riadka či stĺpca a počítať subdeterminanty, čo je už priveľmi komplikované pre náš príklad. Preto program dokáže počítať determinanty len pre matice rozmerov nanajvýš 3 × 3, teda dokáže riešiť sústavy maximálne s tromi neznámymi.

Druhá časť programu slúži na výpočet koreňov reálneho polynómu. Reálny polynóm je výraz v tvare

 

f(x) = anxn + an–1xn–1 + … + a1x + a0

 

kde ai sú koeficienty polynómu a sú to reálne čísla. Číslo n sa nazýva stupeň polynómu. Korene tohto polynómu sú také čísla x, pre ktoré platí

 

f(x) = 0

 

Polynóm n-tého stupňa má práve n koreňov (ktoré však nemusia byť všetky reálne!). Malý príklad: polynóm x2x – 2 je polynómom 2. stupňa a jeho koreňmi sú čísla –1 a 2, pretože (–1)2 – (–1) – 2 = 0 aj 22 – 2 – 2 = 0. Polynóm x2 + 1 je takisto polynómom 2. stupňa, no nemá reálne korene (má dva komplexné: i a –i).

Na výpočet koreňov polynómov vo všeobecnosti neexistuje analytický vzorec, preto treba používať numerické (aj iteračné) metódy. Tieto metódy sú založené na postupnom približovaní sa k správnemu riešeniu pomocou nejakých výpočtov realizovaných v cykloch (iteráciách). Najvhodnejšou pre náš príklad je Newtonova interpolačná metóda. Jej princíp je veľmi jednoduchý: na začiatku si zvolíme nejaký odhad riešenia, x0. V každom ďalšom kroku iterácie vypočítavame novú hodnotu xi na základe vzťahu

 

xi = xi–1f(xi–1) / f’(xi–1)

 

kde f(x) je polynóm, ktorého korene hľadáme, a f’(x) je jeho derivácia. Takto dostaneme postupnosť hodnôt x0, x1, x2, …, ktorá za určitých podmienok konverguje k niektorému z koreňov daného polynómu. Otázkou zostáva, kedy výpočet ukončíme. Tento problém je trošku zložitejší, ako by sa na prvý pohľad zdalo, preto si ho nebudeme komplikovať a dohodneme sa, že výpočet sa skončí, keď sa dva po sebe idúce vypočítané členy postupnosti budú líšiť o číslo menšie ako nejaký vopred dohodnutý limit (značí sa gréckym písmenom epsilon). Nesmieme však zabudnúť na fakt, že polynóm nemusí mať žiadne reálne korene a v takom prípade by sme iterovali donekonečna (prečo, to si tu nebudeme vysvetľovať). Obmedzíme preto počet iterácií nejakou maximálnou hodnotou.

Program

Toľko teoretická príprava. V prípade, že ste niečomu neporozumeli, vráťte sa k nej ešte raz po preštudovaní programu, a ak ani potom nebudete mať vo veci jasno, tak je mi to ľúto, skúste sa poobzerať po nejakej literatúre. Myslím, že na pochopenie výkladu stačia zhruba vedomosti na úrovni 3. až 4. ročníka strednej školy.

Nasleduje výpis programu. Program je rozdelený do dvoch súborov, matemat.h a matemat.cpp.

 

Výpis súboru matemat.h:

 

// matemat.h

 

// max. počet neznámych

#define NMAX   3

// max. stupeň polynómu

#define STMAX  10

// presnosť výpočtu koreňov

#define EPS    0.0000001

// max. počet iterácií

#define ITMAX  1000

 

int menu();

void confirm();

 

void linsys();

double det(int, double[NMAX][NMAX],

           double[NMAX], int = -1);

 

void newton();

double abs(double);

double poly(int, double[STMAX], double);

double dpoly(int, double[STMAX], double);

 

Výpis súboru matemat.cpp:

 

// matemat.cpp

 

#include <stdio.h>

#include <stdlib.h>

#include <conio.h>

#include "matemat.h"

 

//-----------------------------------------

//  main()

//      Hlavná funkcia programu.

//-----------------------------------------

int main()

{

    while (1)

    {

        int choice;

        if ((choice = menu()) == 3)

            break;

 

        switch (choice)

        {

        case 1:

            linsys();

            break;

 

        case 2:

            newton();

            break;

        }

    }

 

    return 0;

}

 

//-----------------------------------------

//  menu()

//      Výpis ponuky činnosti programu.

//

//      Vstup:  žiaden

//      Výstup: 1 - sústava rovnic

//              2 - korene polynómu

//              3 - koniec

//-----------------------------------------

int menu()

{

    clrscr();

    printf(

        "Matematika 1.0\n"

        "--------------\n"

        "\n"

        "1. riešenie sústavy lin. rovníc\n"

        "2. hľadanie koreňov polynómu\n"

        "3. koniec\n"

        "\n"

        "Vaša voľba: ");

 

    int choice;

    do {

        choice = getch();

        if (!choice)

            getch();

    }

    while (choice < '1' || choice > '3');

 

    printf("%c\n\n", choice);

 

    return choice - '0';

}

 

//-----------------------------------------

//  confirm()

//      Požiada o stlačenie ľub. klávesu.

//

//      Vstup:  žiaden

//      Výstup: žiaden

//-----------------------------------------

void confirm()

{

    printf("\nStlačte kláves...\n");

    if (!getch())

        getch();

}

 

//-----------------------------------------

//  linsys()

//      Riešenie sústavy lin. rovníc.

//

//      Vstup:  žiaden

//      Výstup: žiaden

//-----------------------------------------

void linsys()

{

    char buf[80];

    int n = 0;

 

    // vstup počtu neznámych

    while (n <= 0 || n > NMAX)

    {

        printf("Počet neznámych: ");

        gets(buf);

        n = atoi(buf);

    }

 

    double A[NMAX][NMAX];

    double b[NMAX];

    double x[NMAX];

 

    // vstup prvkov matice A

    for (int i = 0; i < n; i++)

        for (int j = 0; j < n; j++)

        {

            printf(" A[%i][%i] = ", i, j);

            gets(buf);

            A[i][j] = atof(buf);

        }

 

    // vstup prvkov vektora b

    for (i = 0; i < n; i++)

    {

        printf(" b[%i]    = ", i);

        gets(buf);

        b[i] = atof(buf);

    }

 

    // výpočet vektora x

    double dets = det(n, A, b);

    if (dets == 0.0)

    {

        printf("\n"

               "Matica A je singulárna, "

               "sústava nemá riešenie!\a\n");

        confirm();

        return;

    }

 

    for (i = 0; i < n; i++)

        x[i] = det(n, A, b, i) / dets;

 

    // výpis výsledkov

    printf("-----------------------\n");

    for (i = 0; i < n; i++)

        printf(" x[%i]    = %lg\n", i, x[i]);

 

    confirm();

}

 

//-----------------------------------------

//  det()

//      Výpočet determinantu matice.

//

//      Vstup:  n - skutočný rozmer matice

//              A - matica koeficientov

//              b - vektor pravých strán

//              s - nahradzovaný stĺpec

//      Výstup: determinant

//-----------------------------------------

double det(int n, double A[NMAX][NMAX],

           double b[NMAX], int s)

{

    // vytvorenie (upravenej) kópie matice

    double A2[NMAX][NMAX];

 

    for (int i = 0; i < n; i++)

        for (int j = 0; j < n; j++)

            if (s != -1 && s == j)

                A2[i][j] = b[i];

            else

                A2[i][j] = A[i][j];

 

    // výpočet determinantu

    double res;

 

    switch (n)

    {

    case 1:

        res = A2[0][0];

        break;

 

    case 2:

        res = A2[0][0] * A2[1][1]

            - A2[0][1] * A2[1][0];

        break;

 

    case 3:

        res = A2[0][0] * A2[1][1] * A2[2][2]

            + A2[1][0] * A2[2][1] * A2[0][2]

            + A2[2][0] * A2[0][1] * A2[1][2]

            - A2[0][2] * A2[1][1] * A2[2][0]

            - A2[1][2] * A2[2][1] * A2[0][0]

            - A2[2][2] * A2[0][1] * A2[1][0];

        break;

    }

 

    return res;

}

 

//-----------------------------------------

//  newton()

//      Výpočet koreňov polynómu.

//

//      Vstup:  žiaden

//      Výstup: žiaden

//-----------------------------------------

void newton()

{

    char buf[80];

    int st = 0;

 

    // vstup stupňa polynómu

    while (st <= 0 || st > STMAX)

    {

        printf("Stupeň polynómu: ");

        gets(buf);

        st = atoi(buf);

    }

 

    // vstup koeficientov polynómu

    double a[STMAX + 1];

 

    for (int i = st; i >= 0; i--)

    {

        printf(" a[%i] = ", i);

        gets(buf);

        a[i] = atof(buf);

    }

 

    // vstup počiatočného bodu

    printf("Počiatočný odhad: ");

    gets(buf);

    double x0 = atof(buf);

 

    // výpočet koreňa

    double x1, dif;

    int it = 0;

 

    do {

        double fx = poly(st, a, x0);

        double dfx = dpoly(st, a, x0);

 

        if (dfx == 0.0)

        {

            it = ITMAX;

            break;

        }

 

        x1 = x0 - fx / dfx;

        dif = abs(x1 - x0);

        x0 = x1;

    }

    while (++it < ITMAX && dif > EPS);

 

    // výpis výsledku

    printf("-----------------------\n");

    if (it == ITMAX)

        printf(" riešenie sa nenašlo\n");

    else

        printf(" x    = %lg\n", x1);

 

    confirm();

}

 

//-----------------------------------------

//  abs()

//      Výpočet absolútnej hodnoty.

//

//      Vstup:  x - číslo

//      Výstup: jeho absolútna hodnota

//-----------------------------------------

double abs(double x)

{

    return x >= 0 ? x : -x;

}

 

//-----------------------------------------

//  poly()

//      Výpočet hodnoty polynómu v bode.

//

//      Vstup:  st - stupeň polynómu

//              a  - koeficienty polynómu

//              x  - bod výpočtu

//      Výstup: hodnota polynómu v bode x

//-----------------------------------------

double poly(int st, double a[STMAX],

            double x)

{

    double res = a[st];

 

    for (int i = st - 1; i >= 0; i--)

        (res *= x) += a[i];

 

    return res;

}

 

//-----------------------------------------

//  dpoly()

//      Výpočet hodnoty derivácie polynómu

//      v bode.

//

//      Vstup:  st - stupeň polynómu

//              a  - koeficienty polynómu

//              x  - bod výpočtu

//      Výstup: hodnota derivácie polynómu

//              v bode x

//-----------------------------------------

double dpoly(int st, double a[STMAX],

             double x)

{

    double res = 0.0;

 

    for (int i = st; i > 0; i--)

        (res *= x) += (i * a[i]);

 

    return res;

}

 

Prv než si preberieme program trochu podrobnejšie, povieme si niečo o tom, ako je v C++ realizovaný vstup údajov od používateľa. Nebudeme zachádzať do úplných podrobností, opíšeme si len niektoré možnosti, ktoré sú použité aj v našom programe. Bližšie sa vstupom (i výstupom) údajov budeme zaoberať v časti venovanej štandardnej knižnici.

Vstup údajov z klávesnice

Údaje do programu možno dostať mnohými spôsobmi – z klávesnice, čítaním zo súboru, cez sieť. V tomto odseku sa obmedzíme na možnosti vstupu priamo z klávesnice. Všetky uvedené funkcie sú súčasťou štandardnej knižnice jazyka C, a preto by mali byť k dispozícii v každom prekladači.

Prvou z funkcií, ktorú však v našom programe nemáme, je funkcia scanf(). Jej názov čiastočne napovedá, že je komplementárnou k funkcii printf() a má na starosti formátovaný vstup. Jej syntax je podobná syntaxi funkcie printf() – prvým argumentom je formátovací reťazec, po ňom nasleduje zoznam premenných, ktorých hodnoty chceme pomocou vstupu z klávesnice zmeniť. Ale pozor! Tu sa často robí chyba, pred každou premennou musí byť operátor &. V skutočnosti totiž funkcia scanf() ako svoje argumenty očakáva ukazovatele na príslušné premenné. Samozrejme, ak máme k dispozícii priamo takýto ukazovateľ, môžeme ho použiť. Príklad:

 

int a, *pa = &a;

scanf("%i", &a);

scanf("%i", pa);

 

Obe volania scanf() sú ekvivalentné.

Funkcia scanf()číta dáta zo štandardného vstupu, ktorým je implicitne klávesnica. Jej použitie je trochu ťažkopádne, hlavne z toho dôvodu, že operačný systém pri vstupe z klávesnice používa riadkovú vyrovnávaciu pamäť (line buffer), takže vstup sa deje po dávkach, ukončených klávesom Enter. Naproti tomu scanf() prijíma v podstate prúd znakov, ktorý spracúva podľa formátovacieho reťazca. Preto je často výhodnejšie použiť inú funkciu, gets(). Jej argumentom je pole znakov, do ktorého sa skopíruje znak po znaku celý riadok, zadaný používateľom. Takto načítaný riadok môžeme potom spracovať vo vlastnej réžii.

V našom programe zadávané údaje najprv načítame do poľa buf[]. Potom, keďže potrebujeme čísla, a nie reťazce znakov, pomocou knižničných funkcií atoi() a atof() prevedieme získané reťazce na čísla typu int, resp. double. Argumentom týchto funkcií je reťazec znakov, ktorý chceme previesť. Spôsob použitia je zrejmý z programu.

Tretí spôsob vstupu, o ktorom si dnes povieme, sa používa v prípade, že chceme okamžite reagovať na stlačenie nejakého klávesu (a nečakať, kým používateľ stlačí Enter). Na tento účel máme k dispozícii funkciu getch(). Táto funkcia čaká na stlačenie ľubovoľného klávesu, ktorého ASCII kód potom vráti. V prípade, že používateľ stlačil nejaký kláves, ktorý nemá ASCII kód (typicky funkčné klávesy, šípky a pod.), getch() vracia hodnotu 0 a pri ďalšom volaní tzv. scan-kód stlačeného klávesu. Funkcia getch() nevypisuje stlačený znak na obrazovku.

V programe je funkcia getch() použitá jednak pri výbere položky z menu, jednak po ukončení výpočtu a výzve na stlačenie nejakého klávesu. Vo funkcii confirm(), ktorá výzvu realizuje, je ukážka správneho spôsobu realizácie „čakania na stlačenie klávesu“. V podmienke príkazu if je volanie funkcie getch(). Podmienka je splnená, keď getch() vráti nulu (operátor !) – v takom prípade sa getch() zavolá ešte raz. Keby sme netestovali návratovú hodnotu funkcie getch(), pri nasledujúcom volaní (a to nielen z nášho programu, ale hocikde v rámci operačného systému) by sme dostali scan-kód predchádzajúceho klávesu, ktorý by sme mylne interpretovali ako ASCII kód nového klávesu.

Funkcia getch(), ako sme už povedali, čaká na stlačenie klávesu. BIOS však stlačenia klávesov ukladá do vyrovnávacej pamäte pre prípad, že stihneme stlačiť viacero klávesov, ako program stačí spracovať. V takom prípade bude funkcia getch() (logicky) postupne vracať všetky stlačené klávesy uložené v tejto vyrovnávacej pamäti. Na zistenie, či používateľ stlačil nejaký kláves, slúži funkcia kbhit(), ktorá vráti nenulovú hodnotu, ak vo vyrovnávacej pamäti klávesnice je aspoň jeden znak. Ale pozor! Ak chceme túto funkciu použiť na prerušenie nejakej bežiacej slučky, nesmieme zabudnúť po jej skončení ten stlačený kláves prečítať a tak odstrániť z vyrovnávacej pamäte. Na ilustráciu toho, o čom hovorím, skúste si preložiť nasledujúci program:

 

#include <conio.h>

 

void main()

{

    while (!kbhit());

    getch();

}

 

Program čaká na stlačenie ľubovoľného klávesu (v slučke while), potom hodnotu klávesu prečíta (pokiaľ je slučka prázdna ako v tomto prípade, tak je vlastne zbytočná a postačí samotný príkaz getch()). Teraz si skúste zakomentovať riadok, na ktorom je volanie getch(). Keď program spustíte napríklad z Volkov Commandera (alebo iného, ale musí to byť dosovský program) stlačením klávesu Enter a ukončíte ho takisto stlačením Enteru, program sa spustí sám od seba znovu. Čo sa stalo? Keď sme stlačili Enter, program sa skončil, ale vo vyrovnávacej pamäti klávesnice toto stlačenie zostalo. Po skončení programu sa k slovu dostal Volkov Commander, ktorý tiež čaká na stlačenie klávesu. Ten si stlačený Enter „vytiahol“ a interpretoval ho ako požiadavku na spustenie programu, na ktorom mal nastavený kurzor. Ale tým programom bol, samozrejme, opäť náš program. Pozor teda na takéto chyby!

Na záver rozprávania o vstupe údajov ešte upozornenie: ak chcete používať uvedené funkcie, musíte do vášho programu vložiť príslušné hlavičkové súbory. Pre scanf() a gets() je to stdio.h, pre getch() a kbhit() zase conio.h.

Analýza programu

Program je rozdelený do dvoch súborov, matemat.h a matemat.cpp. Prvý z nich, matemat.h, je (vďaka svojej prípone) hlavičkovým súborom. Pozrime sa na jeho obsah. Nájdeme v ňom štyri riadky, začínajúce sa reťazcom #define. Ak tušíte, že ide o podobnú syntaktickú konštrukciu ako direktíva #include, tak ste na správnej stope – ide o direktívu preprocesora. Zatiaľ si povieme iba, že táto direktíva nám umožňuje definovať tzv. makrá. Jej syntax je #define meno reťazec, riadok nie je ukončený bodkočiarkou! Od miesta výskytu direktívy #define môžeme v kóde programu používať meno ako ekvivalent reťazca. Túto náhradu vykoná automaticky preprocesor ešte pred preložením programu kompilátorom (nalistujte si druhú časť seriálu). Reťazec môže byť prakticky ľubovoľná postupnosť znakov. Často sa používajú makrá, ktoré sa rozvinú na konštanty, podobne ako v našom príklade, kde napríklad makro STMAX ďalej v programe predstavuje maximálny dovolený stupeň polynómu. Túto hodnotu používame v kóde viackrát a v prípade, že by sme sa rozhodli ju zmeniť, museli by sme ručne nájsť všetky jej výskyty a prepísať ich novou hodnotou. Takto nám stačí zmeniť jeden riadok programu. Bližšie o direktíve #define v niektorej z budúcich častí.

Ďalej v hlavičkovom súbore nájdeme akoby definície funkcií, ale bez tela. Takéto definície, lepšie povedané deklarácie, nazývame prototypmi funkcií a pomocou nich hovoríme kompilátoru, že v našom programe existujú také a také funkcie s takými a takými argumentmi a návratovými hodnotami. Samozrejme, nie je nevyhnutné prototypy používať, ale v takom prípade môžeme v nejakom mieste programu zavolať iba funkciu, ktorá už bola niekedy predtým definovaná. To v našom programe neplatí, pozrite sa do matemat.cpp, kde napr. z funkcie main() voláme funkcie linsys() a newton(), ktorých definícia, teda telo sa nachádza o pár riadkov nižšie a teda až za ich volaním. Prototypy slúžia na akési predbežné oboznámenie kompilátora s tým, aké funkcie v programe mám, a súčasne mu umožňujú kontrolovať, či sú tieto funkcie volané so správnym počtom a typmi argumentov. Štandardné hlavičkové súbory, ako stdio.h a pod., sú plné prototypov príslušných funkcií, ako sa môžete i sami presvedčiť, a ich vložením (#include) do nášho programu si zabezpečíme možnosť použitia týchto funkcií. Kompilátor bude vedieť, že niekde tieto funkcie existujú, a keď ich nenájde v našom programe, pripojí ich z knižnicových súborov (.lib) v čase linkovania. Deklarácia prototypu funkcie je podobná definícii „plnej“ funkcie, s tým rozdielom, že namiesto tela funkcie uzavretého v krútených zátvorkách napíšeme len bodkočiarku (povinná!). V deklarácii prototypu takisto nie je potrebné uvádzať názvy argumentov, stačí ich typ. Celú záležitosť okolo prototypov a všeobecne funkcií si opíšeme v ďalších častiach seriálu.

Možno vás napadla otázka, načo je nám vlastne náš hlavičkový súbor dobrý, veď celý jeho obsah by sme mohli napísať priamo do hlavného zdrojového súboru. Pravdu povediac, jeho použitie v našom konkrétnom príklade je zbytočné. No ak by sme v budúcnosti program rozširovali, pridávali ďalšie funkcie v samostatných zdrojových súboroch, potom v prípade nutnosti použiť niektorú z funkcií zo súboru matemat.cpp (napr. confirm(), tá je dosť všeobecná), bude nám stačiť vložiť v týchto ďalších súboroch hlavičkový súbor matemat.h a hneď budeme mať všetky ním „predstavené“ funkcie k dispozícii.

Prejdime k hlavnému súboru matemat.cpp. Hneď na jeho začiatku nachádzame známe direktívy #include, ktoré jednak vkladajú niekoľko štandardných hlavičkových súborov, jednak náš vlastný matemat.h. Za povšimnutie stojí, že zatiaľ čo názvy štandardných súborov sú uzavreté v lomených zátvorkách (<>), názov nášho súboru je v úvodzovkách. Rozdiel medzi oboma spôsobmi zápisu je ten, že súbory uvedené v lomených zátvorkách prekladač bude pri preklade hľadať len v adresároch určených pre hlavičkové súbory (toto určenie sa realizuje napr. pomocou prepínačov príkazového riadka prekladača alebo v integrovaných prostrediach v niektorom z nastavovacích dialógov), kým súbory uvedené v úvodzovkách bude hľadať aj v adresároch určených pre zdrojové súbory. Mnoho integrovaných vývojových prostredí umožňuje zvoliť samostatný adresár pre zdrojové súbory a samostatný pre hlavičkové súbory (a takisto samostatný pre výstupné súbory, ako .obj a .exe). Nie že by ste museli striktne rozdeľovať súbory do adresárov, vždy to závisí od rozsahu a typu vytváraného projektu, ale typicky sa používa zápis s lomenými zátvorkami na vkladanie štandardných hlavičkových súborov a zápis s úvodzovkami na vkladanie súborov vlastných (na ich odlíšenie od tých štandardných), tak ako je to v našom príklade.

Program sa začína uvedením hlavnej funkcie programu main(). Všimnite si, že pred ňou, ako aj pred každou inou funkciou v programe, je krátky komentárový blok, v ktorom je uvedený názov funkcie, stručný opis, čo funkcia robí, ďalej názvy a opis jednotlivých argumentov a opis návratovej hodnoty. Takýto blok vám jednak pomôže aj po dlhšom čase zistiť, čo ktorá funkcia robí, čo očakáva na vstupe a čo dáva na výstupe, a jednak vizuálne oddeľuje definície jednotlivých funkcií, čo v dlhších súboroch výrazne pomáha pri orientácii. Ale späť k funkcii main(). Je veľmi jednoduchá, pomocou volania funkcie menu() zistí, ktorý z oboch výpočtov chce používateľ realizovať, a podľa toho zavolá jednu z funkcií linsys() alebo newton(). Všimnite si podmienku v príkaze if:

 

if ((choice = menu()) == 3)

    break;

 

Takýto zápis je v C++ bežný a jeho význam by vám mal byť už jasný, ale pre istotu: najprv sa do premennej choice vloží návratová hodnota funkcie menu() a tá sa potom testuje na rovnosť s konštantou 3, čo je dohodnutá konštanta pre voľbu „Koniec programu“. Zátvorky sú nevyhnutné, operátor == má väčšiu prioritu ako =. Ďalej v príkaze switch sa premenná choice testuje len na hodnoty 1 a 2, pretože v tomto mieste programu už hodnotu 3 mať nemôže (a ani žiadnu inú, preto v bloku nenájdeme ani návestie default:). Celý výber a vetvenie sú uzavreté do nekonečnej slučky while, z ktorej je možné vyskočiť po výbere príslušnej položky menu – to je ten break za príkazom if.

Nasledujúcou funkciou v zdrojovom kóde programu je funkcia menu(). V nej za pozornosť stojí cyklus do, v tele ktorého sa čaká na stlačenie príslušného klávesu. Pozor, premenná choice, ktorá sa tu vyskytuje, nemá okrem názvu nič spoločné s podobnou premennou vo funkcii main()! Je tu takisto opisovaný test na stlačenie rozšírených klávesov, ktoré musíme čítať na dvakrát. Cyklus sa ukončí, keď  stlačíme niektorý z požadovaných klávesov. Zaujímavé je, že hoci porovnávame premennú choice so znakovými konštantami ('1', '3'), jej typ je int, a nie char. To je však úplne v poriadku, pretože oba typy sú celočíselné a vzhľadom na porovnanie kompatibilné. Uplatňujú sa tu určité štandardné konverzie, povieme si o nich neskôr. Na záver funkcie stlačený kláves (lepšie povedané príslušný znak) vypíšeme, pretože getch() to za nás nespraví. Ešte si všimnite, akým spôsobom prevedieme znak stlačeného klávesu na návratovú hodnotu – číslo 1 až 3. Je to jednoduché, pretože ASCII kódy znakov '0' až '9' nasledujú za sebou, teda ak odčítame trebárs od konštanty '3' (ASCII kód 0x33) konštantu '0' (ASCII kód 0x30), dostaneme práve požadované číslo 3. Dá sa to, pravda, aj inak, napríklad veľmi elegantne a profesionálne výrazom choice & 0x0F, pretože ASCII kódy znakov číslic sú v rozmedzí 0x30 až 0x39, zrejme teda stačí ponechať dolné štyri bity (čo práve robí operátor & s maskou 0x0F).

Ideme ďalej. Funkcia confirm() je natoľko triviálna, že o nej hádam netreba nič hovoriť. Nasleduje funkcia linsys(), ktorá má na starosti riešenie sústav lineárnych rovníc. Táto funkcia nemá žiadne vstupy, to znamená, že načítanie i výstup dát sa dejú až v jej vnútri. Ako prvé si vypýtame od používateľa počet neznámych. Ten musí byť v rozmedzí 1 až NMAX, čo je z uvedených dôvodov 1 až 3. Vstup od používateľa sa bude opakovať dovtedy, kým nebude v požadovanom rozsahu. Podmienka cyklu while môže byť taká ako v našom príklade alebo aj !(n > 0 && n <= NMAX), čo je klasická negácia tej našej použitím de Morganovho zákona.

Po zistení počtu neznámych si definujeme príslušné premenné. A ako dvojrozmerné pole, b a x ako polia jednorozmerné. Keďže zatiaľ nevieme vytvárať polia dynamicky, podľa potreby, definujeme naše premenné ako statické polia s maximálnym možným rozmerom, daným maximálnym počtom neznámych NMAX. Zaberie nám to trochu pamäte, ale to už dnes hádam až tak neprekáža. Pri výpočte použijeme len takú časť týchto polí, akú potrebujeme. Ďalej v cykle načítame hodnoty všetkých prvkov premenných A a b. Spočítame determinant sústavy a postupne v cykle budeme zisťovať determinanty pre jednotlivé neznáme, vypočítavať tieto neznáme a vypisovať ich na obrazovku. Na konci funkcie si vyžiadame stlačenie ľubovoľného klávesu (funkcia confirm()).

Funkcia det() slúži na výpočet determinantu štvorcovej matice. Tak ako je napísaná, funguje iba pre matice do rozmerov 3 × 3. Prvým jej argumentom je rozmer matice, druhým je samotná matica. Jej typ je uvedený ako double A[NMAX][NMAX], i keď z nej možno použijeme menšiu časť. V skutočnosti sa však nekopíruje do funkcie celá matica, ale len ukazovateľ na ňu. O vzťahu polí a ukazovateľov sme si dosiaľ nehovorili, takže to zatiaľ berte ako fakt, že na to, aby program správne pracoval, musíme pri maticiach definovaných ako dvojrozmerné polia deklarovať ich typ v argumentoch funkcie takto (v skutočnosti je nevyhnutný len ten druhý NMAX, prvý by sme mohli vynechať – double A[][NMAX]). Podobne pri vektore b, i keď tu je to jednoduchšie a stačilo by napísať double b[]. Dokonca (môžete si to vyskúšať, ale bez nároku na vysvetľovanie) je možné napísať typ argumentu ako double* b. Posledným argumentom je číslo stĺpca, ktorý sa má nahradiť vektorom b (pri výpočte determinantov pre jednotlivé neznáme). Ak je tento argument rovný –1, chceme počítať normálny determinant matice A. Všimnite si, že pri volaní funkcie det() z funkcie linsys() pri výpočte determinantu sústavy (premenná dets) sme tento posledný argument vôbec neuviedli! Ako je to možné? Jazyk C++ povoľuje takzvané implicitné argumenty (bližšie o nich neskôr) – všimnite si v súbore matemat.h prototyp funkcie det() a deklaráciu posledného argumentu: int = –1. Tá hovorí, že ak tento argument neuvedieme, prekladač ho sám doplní implicitnou hodnotou –1. Výhodne tento fakt používame v našom príklade – je to prehľadnejšie ako písať tam tú mínus jednotku. Vnútri funkcie det() si deklarujeme premennú A2 rovnakých rozmerov ako matica A, do ktorej túto maticu skopírujeme, prípadne nahradíme niektorý zo stĺpcov vektorom b. Potom už len spočítame determinant klasickým spôsobom a výsledok vrátime. Pochopiť spôsob náhrady stĺpcov vám dávam na domácu úlohu.

Výpočet koreňov polynómu má na starosti funkcia newton(). Podobne ako vo funkcii linsys() zistíme najprv stupeň polynómu (v rozsahu 1 až STMAX – polynómy 0. stupňa nemajú korene a nemá zmysel ich počítať), potom definujeme pole koeficientov a[], ktorého dĺžka je o jeden väčšia ako stupeň polynómu, a následne toto pole naplníme vstupnými údajmi od používateľa. Ešte si vypýtame počiatočný odhad x0 a môžeme začať iterovať. V rámci každej iterácie spočítame hodnotu polynómu v bode x0, hodnotu jeho derivácie v tomto bode a na základe oboch hodnôt spočítame nový „odhad“ x1. Zistíme rozdiel medzi hodnotami x0 a x1, aby sme vedeli, či už máme skončiť (premenná dif), a potom vypočítaný odhad x1 prekopírujeme do x0. V každom prechode inkrementujeme premennú it, ktorá nám počíta iterácie. Cyklus môže skončiť tromi rôznymi spôsobmi. Buď premenná it dosiahne hodnotu ITMAX, čo znamená, že sme vyčerpali maximálny počet iterácií a riešenie sme stále nenašli, alebo premenná dif klesne pod hodnotu EPS, čo je nastavená presnosť nájdenia koreňa, alebo pri výpočte zistíme, že hodnota derivácie nám vyšla nulová (čo znamená nesplnenie podmienok na použitie metódy), a vtedy umelo zvýšime hodnotu premennej it na ITMAX a z cyklu vyskočíme. Podmienka cyklu do musí byť zapísaná v takom poradí, ako je to uvedené v programe, pretože potrebujeme zaručiť inkrementáciu it v každom prechode a operátor &&, ako vieme, nevyhodnotí oba svoje operandy, pokiaľ prvý z nich je nulový. Na záver funkcie vypíšeme buď nájdený koreň, alebo oznam, že riešenie sa nenašlo. Rozhodujeme sa na základe obsahu premennej it, ktorá je v prípade nenájdenia riešenia rovná ITMAX (preto to umelé nastavovanie pri nulovej derivácii).

Nasleduje krátka a jednoduchá funkcia na výpočet absolútnej hodnoty, myslím, že ju opäť netreba opisovať podrobnejšie. Posledné dve funkcie - poly() a dpoly() - slúžia na výpočet hodnôt polynómu a jeho derivácie v danom bode. Argumentmi sú vždy stupeň polynómu, jeho koeficienty a bod, v ktorom hodnoty počítame. Na výpočet používame tzv. Hornerovu schému, čo nie je nič iné ako prepísanie polynómu

 

anxn + an–1xn–1 + an–2xn–2 + … + a1x + a0

 

do tvaru

 

(…((anx + an–1)x + an–2)x + … + a1)x + a0

 

(v podstate postupné vynímanie x „pred zátvorku“), čím ušetríme dosť veľa násobenia. Výpočet derivácie sa deje rovnakým spôsobom, s využitím faktu, že deriváciou uvedeného polynómu je polynóm

 

nanxn–1 + (n – 1)an–1xn–2 + … + 2a2x + a1

 

Cyklus, ktorým sa výpočet realizuje, je pri bližšom pohľade zrejmý, začíname najvyšším koeficientom an, ktorý uložíme do premennej res. Táto premenná bude predstavovať kumulovaný, dosiaľ vypočítaný výsledok. V každom kroku vynásobíme tento výsledok hodnotou x (res *= x) a pripočítame ďalší koeficient (res += a[i]). Prípadne si program krokujte, aby ste cyklus lepšie pochopili.

Pri pozornom preskúmaní oboch cyklov (vo funkcii poly() a vo funkcii dpoly()) si isto všimnete jeden rozdiel – vo funkcii poly() začína premenná res s hodnotou najvyššieho koeficientu (a[st]) a v cykle premenná i začína s hodnotou st – 1. Naproti tomu vo funkcii dpoly() inicializujeme res na nulu a premenná i začína od hodnoty st. Napriek tomu oba cykly fungujú rovnako. Skúste sa zamyslieť prečo (je to inak veľmi jednoduché).

Záver

Prebrali sme si celý program dosť podrobne, verím, že ste ho bez problémov pochopili. Majte na pamäti, že jeho možnosti sú obmedzené, pri jeho behu môže vzniknúť nejaká nepredvídaná chyba a podobne. Ak by ste mali chuť akokoľvek ho upraviť, vylepšiť, nech sa páči, fantázii sa medze nekladú. Ak by ste v ňom dokonca našli nejakú neodhalenú chybu, napíšte mi (verím, nijaké závažné chyby tam nie sú, ale to viete, človek mieni…) Takisto dúfam, že som uspokojil i tých, ktorí volali po väčšom rozsahu jednotlivých častí seriálu. Nabudúce sa vrátime opäť ku klasickému výkladu, o čom to bude, nechajte sa prekvapiť.

C++

Nechajte si posielať prehľad najdôležitejších správ emailom

Mohlo by Vás zaujímať

Ako na to

Tipy a triky: Ako na snímku obrazovky na akomkoľvek počítači s Windows?

02.12.2016 00:13

Ak snímky obrazovky robíte často apotrebujete napríklad funkcie na posun stránok alebo snímanie zobrazenia pri vyššom rozlíšení displeja, zrejme používate nejakú špecializovanú aplikáciu. Väčšina použ ...

Ako na to 1

Tipy a triky: Ako aplikácii prednastaviť spúšťanie s administrátorskými právami?

30.11.2016 00:10

Väčšina aspoň trochu skúsenejších používateľov vie, že aj keď máte na operačnom systéme Windows vytvorený administrátorský účet, aplikácie pre bezpečnosť nefungujú vždy splnými administrátorskými práv ...

Ako na to 2

Tipy a triky: Ako vypnúť uzamykaciu obrazovku vo Windows 10?

29.11.2016 00:10

Rozčuľuje vás, že pred každým prihlásením doúčtu vášho počítača musíte prejsť uzamykacou obrazovkou? Windows 10 na tejto obrazovke ukazuje čas,dátum anejakú zaujímavú fotografiu zrôznych kútov sveta. ...

Žiadne komentáre

Vyhľadávanie

Kyocera - prve-zariadenia-formatu-a4-s-vykonom-a3

Najnovšie videá