Programovanie (1) v C/C++
1-INF-127, ZS 2024/25
Prednáška 15
Obsah
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.
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é štandardné vstupno-výstupné prúdy cin a cout. V nasledujúcom 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. Rovnako ako nižšie sa tak so vstupom a výstupom dá pracovať aj v jazyku 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. Jej základné použitie môže vyzerať napríklad takto:
#include <cstdio>
int main(void) {
printf("Ahoj svet, este raz!\n");
return 0;
}
Funkciu printf možno volať aj s viac ako jedným argumentom. Vo všeobecnosti vyzerá jej volanie nasledovne:
printf(format, hodnota1, hodnota2, ...)
Prvým argumentom je takzvaný formátovací reťazec, za ním nasleduje niekoľko ďalších argumentov (prípadne aj žiaden). Formátovací reťazec pozostáva z dvoch druhov znakov: bežné znaky, ktoré sa priamo vypíšu na výstup a takzvané špecifikácie konverzií začínajúce symbolom % a končiace tzv. znakom konverzie, ktoré majú za následok vypísanie niektorého z ďalších argumentov funkcie printf (presnejšie prvého ešte nevypísaného argumentu). V rámci špecifikácie konverzie možno zadať formát, v ktorom sa má ten-ktorý argument vypísať.
Napríklad
#include <cstdio>
int main(void) {
int n = 7;
printf("Prve cislo je %d a druhe cislo je %d.\n",1+1,n);
return 0;
}
vypíše
Prve cislo je 2 a druhe cislo je 7.
Špecifikácia %d tu pozostáva iba zo znaku konverzie d, ktorý zodpovedá výpisu celého čísla v desiatkovej sústave.
Ďalšie príklady znakov konverzie:
- %f: reálne číslo.
- %e: reálne čislo vo vedeckej notácii, napr. 5.4e7.
- %x: celé číslo v šestnástkovej sústave.
- %c: znak (char).
- %s: reťazec (char *).
- %%: vypíše samotný znak %.
Pozor: typy jednotlivých argumentov musia byť v súlade s formátovacím reťazcom (pričom vo všeobecnosti nedôjde k automatickému pretypovaniu).
Pred samotný znak konverzie možno pridávať aj modifikátory l, ll, resp. h zodpovedajúce modifikátorom typov long, long long, resp. short. Napríklad
- %lld: vypíše „veľmi dlhé” celé číslo.
- %lf: pri printf by sa nemalo používať, zato však veľmi podstatné pri načítavaní.
Formátovanie výstupu
Formát vypísania daného argumentu možno zadať niekoľkými 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:
#include <cstdio>
long long int factorial(int n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n-1);
}
}
int main(void) {
for (int i = 1; i <= 20; i++) {
printf("%2d! = %22lld\n",i,factorial(i));
}
return 0;
}
Nasledujúci program vypíše vo vhodnom formáte zadaný dátum:
#include <cstdio>
void vypisDatum(int d, int m, int r) {
printf("%02d.%02d.%04d\n",d,m,r);
}
int main(void) {
vypisDatum(2,1,2019);
return 0;
}
Celá špecifikácia konverzie pozostáva z nasledujúcich častí:
- Z povinného úvodného znaku %.
- Z jedného alebo niekoľkých nepovinných príznakov, ako 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
Funkciu scanf s typickým volaním
scanf(format, adresa1, adresa2, ...)
možno využiť na načítanie dát z konzoly.
- 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.
Jednoduchý príklad použitia:
#include <cstdio>
void vypisDatum(int d, int m, int r) {
printf("%02d.%02d.%04d\n",d,m,r);
}
int main(void) {
int r;
printf("Zadaj rok: ");
scanf("%d", &r);
vypisDatum(1,1,r);
return 0;
}
Pomocou scanf možno načítať aj viacero premenných naraz:
#include <cstdio>
void vypisDatum(int d, int m, int r) {
printf("%02d.%02d.%04d\n",d,m,r);
}
int main(void) {
int d,m,r;
printf("Zadaj den, mesiac a rok: ");
scanf("%d %d %d", &d, &m, &r);
vypisDatum(d,m,r);
return 0;
}
Formátovací reťazec sa teraz interpretuje nasledovne:
- Špecifikácie formátov načítavaných premenných (začínajúce znakom %) možno zadávať podobne ako pri funkcii printf.
- Pre načítanie reálneho čísla typu double je potrebné použiť %lf. Použitie %f zodpovedá načítavaniu hodnoty „kratšieho” typu float. Pri funkcii printf je však žiadúce pre double aj float používať %f (hoci na mnohých systémoch bude fungovať aj %lf).
- 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 na výstupe 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. V prípade, že hneď na začiatku narazí na koniec súboru (ktorý na konzole možno zadať pod Linuxom ako Ctrl+D resp. pod Windowsom ako Ctrl+Z a Enter), vráti hodnotu EOF (typicky -1).
Príklad: zadávanie dátumu vo formáte deň.mesiac.rok s kontrolou vstupu:
#include <cstdio>
void vypisDatum(int d, int m, int r) {
printf("%02d.%02d.%04d\n",d,m,r);
}
int main(void) {
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");
}
return 0;
}
Ďalším príkladom môže byť program, ktorý počíta súčet postupne zadávaných čísel, až kým je zadané nekorektné číslo alebo koniec súboru:
#include <cstdio>
int main(void) {
double sum = 0;
double x;
while (scanf("%lf", &x) == 1) {
sum += x;
}
printf("Sucet je %.2f\n", sum);
return 0;
}
Textové súbory
Na načítavanie a vypisovanie dát sme doposiaľ používali výhradne konzolu. V praxi však často vzniká potreba spracovávať dáta uložené v súboroch. Zameriame sa teraz na súbory v textovom formáte, s ktorými sa pracuje podobne ako s konzolou.
Základy: typ FILE * a funkcie fopen, fclose, fprintf, fscanf
So súbormi sa pri použití knižnice cstdio pracuje pomocou typu FILE *. Ide tu o smerník na štruktúru typu FILE, ktorá obsahuje nejaké (pre programátora zväčša nepodstatné) 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 *f;
FILE *fr, *fw;
Pozor: v názve typu FILE treba dodržať veľké písmená (čiže treba písať FILE *, nie file *).
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 takto 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 menom 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 takto 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>
int main(void) {
FILE *fr = fopen("vstup.txt", "r");
FILE *fw = fopen("vystup.txt", "w");
assert(fr != NULL && fw != NULL);
int n,r;
r = fscanf(fr, "%d", &n);
assert(r == 1 && n >= 0);
int *a = new int[n];
for (int i = 0; i <= n-1; i++) {
r = fscanf(fr, "%d", &a[i]);
assert(r == 1);
}
fclose(fr);
for (int i = n-1; i >= 0; i--) {
fprintf(fw, "%d ", a[i]);
}
fclose(fw);
delete[] a;
return 0;
}
Š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>
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>
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
}