Programovanie (1) v C/C++
1-INF-127, ZS 2024/25

Úvod · Pravidlá · Prednášky · Softvér · Testovač
· Kontaktujte nás pomocou e-mailovej adresy E-prg.png (bude odpovedať ten z nás, kto má príslušnú otázku na starosti alebo kto má práve čas).
· Prosíme študentov, aby si pravidelne čítali e-maily na @uniba.sk adrese alebo aby si tieto emaily preposielali na adresu, ktorú pravidelne čítajú.


Prednáška 15

Z Programovanie
Skočit na navigaci Skočit na vyhledávání

Oznamy

  • Na piatkových doplnkových cvičeniach bude bonusová rozcvička (za 1 bonusový bod). Zvyšné úlohy z tohto týždňa treba odovzdať do stredy 18. novembra, 22:00.
  • Druhú domácu úlohu treba odovzdať do piatku 13. novembra, 22:00.
  • Budúci týždeň budú kvôli štátnemu sviatku iba piatkové cvičenia. V pondelok 16. novembra bude zverejnených niekoľko úloh na cvičenia č. 9 (menej, než obvykle).
  • V piatok 20. novembra bude na začiatku doplnkových cvičení krátky test, body za ktorý budú riadnou súčasťou hodnotenia z cvičení č. 9. Pokyny ohľadom technickej realizácie testu budú upresnené neskôr.

Smerníková aritmetika

Na smerníkoch možno vykonávať určité operácie, ktoré sa zvyknú nazývať smerníková aritmetika. Uvažujme číslo n typu int a smerníky p a q na nejaký typ T

int n;
T *p, *q;
  • p + n je smerník na n-té políčko za adresou p, pričom veľkosť políčka je daná typom T
    • p + n je teda to isté ako &(p[n]) a *(p+n) je to isté ako p[n].
    • p++ je skratkou pre p = p + 1, posunie nám teda smerník p o políčko doprava.
    • Výraz p[n] je len skratkou pre *(p+n).
    • p[n] aj p + n teda chceme používať iba ak p ukazuje na prvok poľa, za ktorým v poli ide ešte aspoň n ďalších políčok
  • Podobne p - n je smerník na n-té políčko pred adresou p.
    • p - n teda chceme používať iba ak p ukazuje na prvok poľa, pred ktorým v poli ide ešte aspoň n ďalších políčok
  • Ak p a q sú adresami prvkov v tom istom poli, p - q je celé číslo k také, že p == q + k, t.j. o koľko políčok je p ďalej vpravo od q.
  • Ak p a q sú adresami prvkov v tom istom poli, môžeme ich tiež porovnávať pomocou <, >, <=, >=
  • Ľubovoľné dva smerníky toho istého typu vieme porovnávať pomocou ==, !=.

Tu je napr. zvláštny spôsob ako vypísať pole a:

int a[4] = {4, 3, 2, 1};
for (int *smernik = a; smernik < a + 4; smernik++) {
     cout << "Prvok " << smernik - a << " je " << *smernik << endl;
}

Podobný kód sa ale občas používa na prechádzanie reťazcov. Napríklad nasledujúca funkcia spočíta počet medzier v reťazci:

int zratajMedzery(char str[]) { // mohli by sme dat aj char *str
  int pocet = 0;
  while(*str != 0) {   // kym nenajdeme ukoncovaciu nulu v retazci
     if(*str == ' ') { // skontroluj znak, na ktory ukazuje smernik
         pocet++; 
     }
     str++;           // posun smernik na dalsi znak 
  }
  return pocet;
}

Funkcie z knižnice cstring so smerníkovou aritmetikou

  • strstr(text, vzorka) vracia smerník na char
    • NULL ak sa vzorka nenachádza v texte, smerník na začiatok prvého výskytu inak
    • pozíciu výskytu zistíme smerníkovou aritmetikou:
char *text = "Hello world!";
char *vzorka = "or";
char *where = strstr(text, vzorka);
if(where != NULL) {
  int position = where - text;
}
  • Ako by ste spočítali počet výskytov vzorky v texte?
  • Podobne strchr hľadá prvý výskyt znaku v texte

Práca s konzolou na spôsob jazyka C: printf a scanf

  • Doposiaľ sme s konzolou pracovali prostredníctvom knižnice iostream, ktorá patrí medzi štandardné knižnice jazyka C++ a v ktorej sú definované prúdy cin a cout.
  • Dnes si ukážeme alternatívny prístup k práci s konzolou založený na knižnici cstdio, ktorá je štandardnou knižnicou jazyka C.

Výpis formátovaných dát na konzolu: printf

  • S použitím knižnice cstdio možno na konzolu písať pomocou funkcie printf.
  • Tu sú príklady jej použitia:
#include <cstdio>
#include <cmath>
using namespace std;

int main() {
    int x = 10;
    double y = sqrt(x);
    printf("Ahoj svet, este raz!\n");
    printf("Odmocnina cisla %d je %f.\n", x, y);
}

Tento program vypíše:

Ahoj svet, este raz!
Odmocnina cisla 10 je 3.162278.

Vo všeobecnosti vyzerá volanie printf nasledovne:

printf(format, hodnota1, hodnota2, ...)
  • Prvý argument je formátovací reťazec, za ním môže nasledovať niekoľko ďalších argumentov.
  • Bežné znaky z formátovacieho reťazca sa priamo vypíšu na výstup.
  • Koniec riadku sa píše pomocou "\n".
  • Symbol % začína takzvanú špecifikáciu konverzie a má za následok vypísanie ďalšieho argumentu funkcie (prvého, ktorý sa nepoužil).
  • V jednoduchých príkladoch za znakom % nasleduje znak reprezentujúci typ vypísanej hodnoty.
  • Pozor, typy jednotlivých argumentov musia byť v súlade s formátovacím reťazcom.
  • %d: celé číslo (typ int).
  • %ld: dlhé celé číslo (typ long int).
  • %f: reálne číslo (typ double).
  • %e: reálne číslo vo vedeckej notácii, napr. 5.4e7 (typ double).
  • %c: znak (typ char).
  • %s: reťazec (typ char *).
  • %%: vypíše samotný znak %.

Formátovanie výstupu

  • Formát vypísania daného argumentu možno upraviť nepovinnými parametrami medzi symbolom % a znakom konverzie.
  • Napríklad:
    • %.2f: vypíše reálne číslo na 2 desatinné miesta,
    • %4d: ak má celé číslo menej ako 4 cifry, doplní vľavo medzery,
    • %04d: podobne, ale dopĺňa nuly.

Nasledujúci program vo vhodnom formáte vypíše hodnoty faktoriálu prirodzených čísel od 1 po 20 použitím typu long long int, ktorý garantuje aspoň 64 bitové číslo:

#include <cstdio>
using namespace std;

long long int factorial(int n) {                      
    if (n == 0) {
        return 1;
    } else {
        return n * factorial(n-1);
    }
}

int main() {
    for (int i = 1; i <= 20; i++) {                   
        printf("%2d! = %22lld\n", i, factorial(i));
    }
}
 1! =                      1
 2! =                      2
 3! =                      6
 4! =                     24
 5! =                    120
 6! =                    720
 7! =                   5040
 8! =                  40320
 9! =                 362880
10! =                3628800
11! =               39916800
12! =              479001600
13! =             6227020800
14! =            87178291200
15! =          1307674368000
16! =         20922789888000
17! =        355687428096000
18! =       6402373705728000
19! =     121645100408832000
20! =    2432902008176640000


Nasledujúci program vypíše zadaný dátum vo formáte typu 02.01.2019:

#include <cstdio>
using namespace std;

void vypisDatum(int d, int m, int r) {
    printf("%02d.%02d.%04d\n", d, m, r);
}

int main() {
    vypisDatum(2, 1, 2019);
}


Celá špecifikácia konverzie pozostáva z nasledujúcich častí:

  • Z povinného úvodného znaku %.
  • Z nepovinných príznakov, napríklad -, ktorého použitie vyústi v zarovnanie vypisovaného textu vľavo (bez jeho použitia sa text zarovná vpravo). Ďalšími príznakmi sú napríklad 0 (dopĺňanie núl naľavo), + (vypíše znamienko + pri kladných číslach), atď.
  • Z nepovinného celého čísla udávajúceho minimálnu šírku výpisu (minimálny počet „políčok”, do ktorých sa text vypíše).
  • Z nepovinnej bodky nasledovanej celým číslom udávajúcim presnosť výpisu (pri reálnych číslach napríklad počet desatinných miest; presnosť má však svoju interpretáciu aj pri iných typoch dát).
  • Z nepovinného modifikátora l, ll, alebo h pre long, long long, resp. short.
  • Z povinného symbolu konverzie (napr. d, f, s, ...).

Načítanie formátovaných dát z konzoly: scanf

Vstup z konzoly sa dá načítať funkciou scanf s typickým volaním

scanf(format, adresa1, adresa2, ...)
  • Napríklad scanf("%d", &x) načíta celočíselnú hodnotu do premennej x.
  • Zatiaľ čo argumentmi printf sú priamo hodnoty, scanf potrebuje adresy premenných, pretože ich modifikuje.

Pomocou scanf možno načítať aj viacero premenných naraz:

#include <cstdio>
using namespace std;

void vypisDatum(int d, int m, int r) {
    printf("%02d.%02d.%04d\n", d, m, r);
}

int main() {
    int d, m, r;
    printf("Zadaj den, mesiac a rok: ");  
    scanf("%d %d %d", &d, &m, &r);        
    vypisDatum(d, m, r);                    
}

Formátovací reťazec sa v scanf interpretuje nasledovne:

  • Špecifikácia typu načítavaných premenných je podobná ako pri funkcii printf.
    • %d načíta int
    • %lf načíta double (pozor, tu je rozdiel od printf, kde sa double vypisuje pomocou %f)
    • %s načíta reťazec, konkrétne jedno slovo, t.j. postupnosť nebielych znakov. Ako argument sa zadá hodnota typu char*, ktorá má ukazovať na dostatočne veľké pole.
    • %100s načíta slovo, ale najviac 100 znakov. Pole má mať veľkosť aspoň 101.
    • %c načíta jeden znak. Na rozdiel od všetkých predchádzajúcich typov, tu sa nepreskakujú biele znaky pred prvým nebielym
  • Biele znaky (angl. whitespace, t.j. medzery, konce riadkov, tabulátory) vo formátovacom reťazci spôsobia, že funkcia scanf číta a ignoruje všetky biele znaky pred ďalším nebielym znakom. Jeden biely znak vo formátovacom reťazci tak umožní ľubovoľný počet bielych znakov na vstupe.
  • Ostatné znaky formátovacieho reťazca musia presne zodpovedať vstupu.

Nasledujúci príkaz tak napríklad načíta dátum vo formáte deň.mesiac.rok:

scanf("%d.%d.%d", &d, &m, &r);

Kontrola správnosti vstupu

Funkcia scanf vracia počet úspešne načítaných hodnôt zo vstupu.

  • V prípade chyby hneď na začiatku vstupu tak napríklad vráti 0.
  • Ak hneď na začiatku narazí na koniec vstupu, vráti hodnotu EOF (typicky -1).
  • Vstup z konzoly sa dá ukončiť pod Linuxom ako Ctrl+D resp. pod Windowsom ako Ctrl+Z a Enter

Príklad: zadávanie dátumu vo formáte deň.mesiac.rok s kontrolou vstupu:

#include <cstdio>
using namespace std;

void vypisDatum(int d, int m, int r) {
    printf("%02d.%02d.%04d\n", d, m, r);
}

int main() {
    int d, m, r;
    printf("Zadaj datum: ");  
    if (scanf("%d.%d.%d", &d, &m, &r) == 3) {
        printf("Datum je ");
        vypisDatum(d,m,r);
    } else {
        printf("Nebol zadany korektny datum.\n");
    }
}

Ďalší program počíta súčet postupne zadávaných čísel, až kým je zadané nekorektné číslo alebo koniec súboru:

#include <cstdio>
using namespace std;

int main() {
    double sum = 0;
    double x;
    while (scanf("%lf", &x) == 1) {
         sum += x;
    }
    printf("Sucet je %.2f\n", sum);
}

Textové súbory

Na načítavanie a vypisovanie dát sme doposiaľ používali výhradne konzolu. V praxi však často potrebujeme spracovávať dáta uložené v súboroch.

  • Zameriame sa na súbory v textovom formáte, s ktorými sa pracuje podobne ako s konzolou.
  • V C++ existujú ekvivalenty cin >> a cout << aj pre súbory, nájdete ich v knižnici fstream.

Základy: typ FILE * a funkcie fopen, fclose, fprintf, fscanf

So súbormi sa pri použití knižnice cstdio pracuje pomocou typu FILE * (veľkými písmenami).

  • Je to smerník na štruktúru typu FILE, ktorá obsahuje informácie o súbore, s ktorým sa práve pracuje.
  • Premenné pre prácu so súbormi tak možno definovať napríklad takto:
FILE *fr, *fw;

Otvorenie súboru pre čítanie

  • fr = fopen("vstup.txt", "r");
  • Otvorí súbor s názvom vstup.txt (prípadne možno zadať kompletnú cestu k súboru).
  • Ak taký súbor neexistuje alebo sa nedá otvoriť, do fr priradí NULL.
  • Z otvoreného súboru môžeme čítať napríklad pomocou fscanf, ktorá je analógiou k scanf.
  • Napríklad fscanf(fr, "%d", &x);

Otvorenie súboru pre zápis

  • fw = fopen("vystup.txt", "w");
  • Vytvorí súbor s názvom vystup.txt. Ak už existoval, zmaže jeho obsah (keby sme vo volaní fopen namiesto "w" použili "a", pridávalo by sa na koniec existujúceho súboru).
  • Ak sa nepodarí súbor otvoriť, do fw priradí NULL.
  • Do otvoreného súboru môžeme zapisovať napr. pomocou funkcie fprintf, ktorá je analógiou k printf.
  • Napr. fprintf(fw, "%d", x);

Zatvorenie súboru

  • Po ukončení práce so súborom je ho potrebné zavrieť pomocou fclose(f);
  • Počet súčasne otvorených súborov je obmedzený.

Príklad

Nasledujúci program načíta číslo n a následne n celých čísel zo súboru vstup.txt. Do súboru vystup.txt vypíše vstupné čísla v opačnom poradí.

#include <cstdio>
#include <cassert>
using namespace std;

int main() {
    // otvorime subory a skontrolujeme, ze nie su NULL
    FILE *fr = fopen("vstup.txt", "r");   
    FILE *fw = fopen("vystup.txt", "w");
    assert(fr != NULL && fw != NULL);
    
    // nacitame pocet cisel
    int n, r;
    r = fscanf(fr, "%d", &n);
    assert(r == 1 && n >= 0);

    // alokujeme pole a nacitame cisla
    int *a = new int[n];
    for (int i = 0; i < n; i++) {
        r = fscanf(fr, "%d", &a[i]);
        assert(r == 1);
    }
    fclose(fr);

    // vypiseme cisla odzadu
    for (int i = n-1; i >= 0; i--) {
        fprintf(fw, "%d ", a[i]);
    }
    fclose(fw);
    delete[] a;    
}

Štandardný vstup a výstup ako súbor

So štandardným vstupom a výstupom sa pracuje rovnako ako so súborom. V cstdio sú definované dva konštantné smerníky

FILE *stdin, *stdout;

pre štandardný vstupný a výstupný prúd. Tie tak môžu byť použité v ľubovoľnom kontexte, v ktorom sa očakáva súbor. Napríklad volanie fscanf(stdin, "%d", &x) je ekvivalentné volaniu scanf("%d", &x).

Ten istý kód sa tak dá použiť na prácu so súbormi aj so štandardným vstupom resp. výstupom – stačí len podľa potreby nastaviť premennú typu FILE *. Typické použitie je napríklad nasledovné:

    FILE *fr, *fw;
    ...
    fscanf(fr, "%s", str);
    if (strcmp(str, "-") == 0) {
        fw = stdout;
    } else {
        fw = fopen(str, "w");
    }
    fprintf(fw, "Hello world!\n");
    ...

Testovanie konca súboru

Existujú dve možnosti testovania „nárazu” na koniec súboru:

  • V knižnici cstdio je definovaná symbolická konštanta EOF, ktorá má väčšinou hodnotu -1. Ak sa funkcii fscanf nepodarí načítať žiadnu hodnotu, pretože načítavanie dospelo ku koncu súboru, vráti konštantu EOF ako svoj výstup.
  • Funkcia feof(subor) vráti true práve vtedy, keď sa funkcia fscanf (alebo nejaká iná funkcia) už niekedy pokúšala čítať za koncom súboru subor.

Spracovanie vstupu pozostávajúceho z postupnosti čísel

Často na vstupe očakávame postupnosť číselných hodnôt oddelených bielymi znakmi. Pozrime sa na tri obvyklé možnosti, ako môže byť takýto vstup zadaný a spracovaný pomocou funkcie fscanf.

Formát 1: N (počet čísel) a následne N ďalších čísel.

#include <cstdio>
#include <cassert>
using namespace std;

int main(void) {
    FILE *f;
    const int MAXN = 100;
    int a[MAXN], N, kod;

    f = fopen("vstup.txt", "r");
    assert(f != NULL);

    kod = fscanf(f, "%d ", &N);
    assert(kod == 1 && N >= 0 && N < MAXN);

    for (int i = 0; i < N; i++) {
        kod = fscanf(f, "%d ", &a[i]);
        assert(kod == 1);
    }
    fclose(f);

    // tu pride spracovanie dat v poli a
}

Formát 2: postupnosť čísel ukončená číslom -1 alebo inou špeciálnou hodnotu.

    // otvorime subor f ako vyssie
    N = 0;
    int x;
    kod = fscanf(f, "%d ", &x);
    assert(kod == 1);
    while (x != -1) {
        assert(N < MAXN);
        a[N] = x;
        N++;
        kod = fscanf(f, "%d ", &x);
        assert(kod == 1);
    }
    // zatvorime subor a spracujeme data

Formát 3: čísla, až kým neskončí súbor (najtypickejší prípad v praxi).

Priamočiary prístup nefunguje vždy správne:

    // otvorime subor f ako vyssie
    N = 0;
    while (!feof(f)) {
        assert(N < MAXN);
        kod = fscanf(f, "%d", &a[N]);
        assert(kod == 1);
        N++;
    }
    // zatvorime subor a spracujeme data

Po poslednom čísle v súbore často nasleduje ešte koniec riadku, v dôsledku čoho môže posledné volanie funkcie fscanf vyústiť v návratovú hodnotu -1 (predchádzajúce volanie fscanf totiž ešte „nenarazilo” na koniec súboru, v dôsledku čoho je pred čítaním posledného riadku hodnota feof(f) stále rovná false). Tým pádom program zlyhá na riadku assert(kod == 1). Tento nedostatok môžeme napraviť napríklad tak, že vo volaní funkcie fscanf dáme vo formátovacom reťazci za %d medzeru. Tá sa bude pokúšať preskočiť všetky biele znaky až po najbližší nebiely; pritom natrafí na koniec súboru a feof(f) už bude vracať true.

#include <cstdio>
#include <cassert>
using namespace std;

int main(void) {
    FILE *f;
    const int MAXN = 100;
    int a[MAXN], N, kod;

    f = fopen("vstup.txt", "r");
    assert(f != NULL);

    N = 0;
    while (!feof(f)) {
        assert(N < MAXN);
        kod = fscanf(f, "%d ", &a[N]);
        assert(kod == 1);
        N++;
    }
    fclose(f);

    // tu pride spracovanie dat v poli a
}