Programovanie (1) v C/C++
1-INF-127, ZS 2024/25
Prednáška 13: Rozdiel medzi revíziami
Riadok 360: | Riadok 360: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Cvičenia: | + | Cvičenia: |
* Prečo nemáme na konci programu <tt>delete[] a</tt>? | * Prečo nemáme na konci programu <tt>delete[] a</tt>? | ||
* Čo by sa stalo, ak by sme namiesto <tt>a[n] = new char[strlen(riadok)+1]; strcpy(a[n], riadok)</tt> dali <tt>a[n] = riadok</tt>? | * Čo by sa stalo, ak by sme namiesto <tt>a[n] = new char[strlen(riadok)+1]; strcpy(a[n], riadok)</tt> dali <tt>a[n] = riadok</tt>? |
Verzia zo dňa a času 14:00, 30. október 2021
Obsah
Opakovanie smerníkov
Smerníky na jednoduché premenné:
int n = 7; // premenná typu int int *p = NULL; // smerník na premennú typu int p = &n; // p ukazuje na n *p = 8; // v premennej n je teraz 8 n = (*p)+1; // v premennej n je teraz 9
Smerníky a polia, alokovanie poľa:
int a[3]; int *b = a; // a,b sú teraz takmer rovnocenné premenné *b = 3; b[1] = 4; a[2] = 5; // v poli sú teraz čísla 3,4,5 b = new int[a[1]]; // b teraz ukazuje na nové pole dĺžky 4 delete[] b; // uvoľníme pamäť alokovanú pre nové pole
Dvojrozmerné polia
- Doteraz sme stále pracovali s jednorozmerným poľom, čo však ak potrebujeme dvojrozmerné pole, maticu?
- Napríklad na matice z algebry, alebo na dvojrozmerné tabuľky napr. body študentov z domácich úloh
- Podobne môžeme potrebovať aj polia väčších rozmerov, pracuje sa s nimi analogicky ako s dvojrozmenrnými
Dvojrozmerné polia s konštantnou veľkosťou
- Ak je veľkosť dvojrozmerného poľa vopred známa konštanta, môžeme ho vytvoriť veľmi jednoducho
- napr. int a[2][5] vytvorí tabuľku s dvomi riadkami a piatimi stĺpcami
- a[i][j] je potom prvok na riadku i a stĺpci j
- Rozmery poľa musíme uviesť aj ak pole posielame do funkcie, napr. void vypis(int a[2][5])
- Tento spôsob však nebudeme ďalej používať, lebo väčšinou chceme rozmery prispôsobiť potrebám daného vstupu
#include <iostream>
using namespace std;
int main() {
/* Vytvorime pole s dvoma riadkami a piatimi stlpcami
* a rovno ho aj inicializujeme: */
int a[2][5] = {{1,2,3,4,5},{6,7,8,9,10}};
/* Vypiseme pole ako tabulku: */
for (int i = 0; i <= 1; i++) {
for (int j = 0; j <= 4; j++) {
cout << a[i][j] << " ";
}
cout << endl;
}
}
Dvojrozmerné pole pomocou poľa smerníkov
- Omnoho flexibilnejšou alternatívou sú polia smerníkov.
- Každý riadok tabuľky bude jedno dynamicky alokované pole a smerník na jeho začiatok si uložíme do poľa smerníkov
- Tabuľka s m riadkami a n slpcami bude v pamäti uložená nejako takto:
Nasledujúci program načíta rozmery dvojrotmernej tabuľky, potom jej prvky a spočíta priemer čísel v každom stĺpci.
#include <iostream>
using namespace std;
int main() {
int m, n;
cout << "Zadaj pocet riadkov: ";
cin >> m;
cout << "Zadaj pocet stlpcov: ";
cin >> n;
/* Alokuj pole smernikov na riadky: */
int **a;
a = new int *[m];
/* Alokuj jednotlive riadky: */
for (int i = 0; i <= m-1; i++) {
// a[i] je smernik na i-ty riadok
a[i] = new int[n];
}
/* Nacitanie prvkov tabulky: */
cout << "Zadaj cisla tabulky:" << endl;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// nacitaj j-ty prvok i-teho riadku
cin >> a[i][j];
}
}
/* Spocitaj a vypis priemery jednotlivych stlpcov: */
for (int j = 0; j < n; j++) {
int sum = 0;
for (int i = 0; i < m; i++) {
sum += a[i][j];
}
cout << "Priemer hodnot v stlpci " << j
<< " je " << (sum * 1.0)/m << endl;
}
/* Uvolnenie pamate: */
for (int i = 0; i <= m-1; i++) {
delete[] a[i];
}
delete[] a;
}
Cvičenie:
- Ako by ste spočítali priemery stĺpcov vstupnej tabuľky s využitím iba jednorozmerného poľa? (Celú tabuľku teda nechceme ukladať.)
- Ako by vyzeralo vytvorenie a použitie trojrozmernej tabuľky?
Príklad: výšková mapa
Pokračujme ukážkou o niečo väčšieho programu využívajúceho dvojrozmerné tabuľky (matice)
- Maticu uložíme ako dynamicky alokované pole smerníkov
- V programe je niekoľko funkcií, ktoré sa môžu zísť aj v iných programoch na prácu s maticami
- int ** vytvorMaticu(int m, int n) alokuje pamäť pre maticu s m riadkami a n stĺpcami a vráti smerník na pole smerníkov
- void zmazMaticu(int **a, int m) uvolní pamäť alokovanú pre maticu a s m riadkami (počet stĺpcov nepotrebujeme)
- void nacitajMaticu(int **a, int m, int n) dostane už alokovanú maticu s m riadkami a n stĺpcami a vyplní ju číslami načítanými zo vstupu
Všimnite si, že funkciám potrebujeme dávať aj rozmery matice. Namiesto toho by sme si mohli spraviť štruktúru podobne ako pri dynamickom poli:
struct matica {
int m, n;
int **a;
}
Cieľ programu
Náš program bude v obdĺžnikovej tabuľke celých čísel uchovávať výškovú mapu nejakého územia, v ktorom nadmorská výška nadobúda hodnoty medzi 0 a 2000 metrami nad morom.
Program na vstupe najprv dostane dvojicu prirodzených čísel m a n. Výškovou mapou potom bude obdĺžnik pozostávajúci z m krát n štvorčekov, kde každý zo štvorčekov bude mať danú nejakú nadmorskú výšku od 0 po 2000 metrov nad morom (nadmorská výška 0 znamená more a kladná nadmorská výška znamená pevninu). Následne program postupne prečíta zo vstupu nadmorské výšky všetkých štvorčekov.
Takto zadanú mapu program vykreslí pomocou knižnice SVGdraw, pričom každý štvorček dostane určitú farbu podľa svojej nadmorskej výšky. Následne zavolá funkciu najvyssiVrch, ktorá nájde najvyšší bod (resp. jeden z najvyšších bodov) vykresľovaného územia a v mape ho zvýrazní rámikom.
Príklad vstupu a výstupu:
22 11 0 0 0 0 0 0 0 0 0 0 0 0 20 40 60 80 100 120 140 120 0 0 0 40 80 120 160 200 240 280 190 100 0 0 60 120 180 240 300 360 420 260 100 0 0 80 160 240 320 400 480 560 260 100 0 0 100 200 300 400 500 600 700 330 100 0 0 120 240 360 480 600 720 840 400 100 0 0 140 280 420 560 700 840 980 470 100 0 0 160 320 480 640 800 960 700 200 0 0 0 180 360 540 720 900 700 500 0 0 0 0 200 400 600 800 1000 1200 1400 680 100 0 0 220 440 660 880 1100 1320 1540 750 100 0 0 240 480 720 960 1200 1440 1680 820 100 0 0 260 520 780 1040 1300 1560 1820 1200 400 0 0 280 560 840 1120 1400 1680 1960 1500 600 0 0 240 480 720 960 1200 1440 1680 1000 400 0 0 200 400 600 800 1000 1200 1400 680 100 0 0 160 320 480 640 800 960 1120 540 100 0 0 120 240 360 480 600 720 840 400 100 0 0 80 160 240 320 400 480 560 260 100 0 0 40 80 120 160 200 240 280 120 0 0 0 0 0 0 0 0 0 0 0 0 0
Program výšková mapa
#include <iostream>
#include "SVGdraw.h"
using namespace std;
/* velkost stvorceka mapy v pixeloch */
const int stvorcek = 15;
int ** vytvorMaticu(int m, int n) {
/* Vytvori a vrati maticu (obdlznikovu tabulku)
* s m riadkami a n stlpcami. */
int **a;
a = new int *[m];
for (int i = 0; i < m; i++) {
a[i] = new int[n];
}
return a;
}
void zmazMaticu(int **a, int m) {
/* Uvolni z pamate maticu
* s m riadkami a n stlpcami. */
for (int i = 0; i < m; i++) {
delete[] a[i];
}
delete[] a;
}
void nacitajMaticu(int **a, int m, int n) {
/* Nacita hodnoty do uz vytvorenej matice
* velkosti m krat n. */
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
cin >> a[i][j];
}
}
}
void nastavFarbu(int vyska, SVGdraw &drawing) {
/* podla nadmorskej vysky nastavi farbu ciary
* aj vyplne
* modra -- more (nadmorska vyska 0)
* zelena -- niziny (nadmorska vyska 1,...,200)
* hneda -- "pohoria" (nadmorska vyska 200,...,2000) */
// premenne pre cervenu, zelenu a modru zlozku farby
int r, g, b;
// nastavenie farby podla hodnoty
if (vyska == 0) { // modre more
r = 0;
g = 0;
b = 255;
} else if (vyska <= 200) { // zelena nizina
double x = vyska / 200.0;
r = x * 255;
g = 127 + x * 127;
b = 0;
} else { // zlto-hnede hory
double x = (vyska - 200) / 1800.0;
r = 255 - x * 150;
g = 255 - x * 200;
b = 0;
}
/* Nastavi farbu ciary aj vyplne na dane hodnoty. */
drawing.setLineColor(r, g, b);
drawing.setFillColor(r, g, b);
}
void vykresliStvorcek(int riadok, int stlpec, SVGdraw &drawing) {
/* Vykresli stvorcek pre dany riadok a stlpec mapy.
* Pouzije pri tom aktualne nastavene farby.
* Pozor, pri vykreslovani
* uvadzame najskor x (stlpec), potom y (riadok) */
drawing.drawRectangle(stlpec * stvorcek,
riadok * stvorcek,
stvorcek, stvorcek);
}
void vykresliMapu(int **a, int m, int n, SVGdraw &drawing) {
/* Vykresli mapu rozmerov m krat n do obrazku.
* Jednotlive stvorceky mapy ofarbi podla ich nadmorskej vysky */
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
nastavFarbu(a[i][j], drawing);
vykresliStvorcek(i, j, drawing);
}
}
}
void maximumMatice(int **a, int m, int n, int &riadok, int &stlpec) {
/* Najde v matici a o rozmeroch m krat n
* policko s maximalnou hodnotou
a jeho suradnice ulozi do premennych riadok, stlpec. */
riadok = 0;
stlpec = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (a[i][j] > a[riadok][stlpec]) {
riadok = i;
stlpec = j;
}
}
}
}
int main(void) {
/* nacitaj rozmery matice */
int m, n;
cin >> m >> n;
/* vytvor a nacitaj maticu */
int **a = vytvorMaticu(m, n);
nacitajMaticu(a, m, n);
/* zobraz maticu, pozor vymena suradnice */
SVGdraw drawing(n * stvorcek, m * stvorcek,
"mapa.svg");
vykresliMapu(a, m, n, drawing);
/* najdi najvyssi vrch a zvyrazni ho stvorcekom */
int riadok, stlpec;
maximumMatice(a, m, n, riadok, stlpec);
drawing.setLineColor("black");
drawing.setLineWidth(3);
drawing.setNoFill();
vykresliStvorcek(riadok, stlpec, drawing);
/* ukonci vykreslovanie */
drawing.finish();
/* uvolni pamat matice */
zmazMaticu(a, m);
}
Polia reťazcov
- Dvojrozmerné polia v C/C++ nemusia mať všetky riadky rovnako dlhé. To sa hodí napríklad na ukladanie viacerých reťazcov.
- Spomeňte si, že v C je reťazec jednoducho pole char-ov, kde za posledným znakom ide špeciálny znak 0
- Pole reťazcov bude teda dvojrozmerné pole char-ov
- Môžeme načítavať napr. vstup po riadkoch, pričom každý riadok načítame do dlhého poľa, ktoré by malo stačiť a potom do prekopírujeme do akurát veľkého riadku v poli
- Vstup je ukončený prázdnym riadkom
- Nakoniec program riadky vypíše odzadu
- Ak by sme vopred alokovali maxN riadkov, každý veľkosti maxRiadok, vyšlo by potenciálne na zmar oveľa viac pamäte.
#include <iostream>
#include <cstring>
using namespace std;
const int maxN = 1000;
const int maxRiadok = 1000;
int main() {
char *a[maxN]; // pole maxN smernikov na char
char riadok[maxRiadok];
int n = 0;
while (true) {
// nacitame jeden riadok
cin.getline(riadok, maxRiadok);
// ak je prazdny alebo sa minuli polozky pola A,
// koncime nacitavanie
if (strcmp(riadok, "") == 0 || n == maxN) {
break;
}
// alokujeme pamat pre n-ty retazec pola a
// pozor, je o jedna dlhsi ako dlzka retazca (kvoli 0 na konci)
a[n] = new char[strlen(riadok)+1];
// prekopirujeme riadok
strcpy(a[n], riadok);
n++;
}
// vypiseme riadky odzadu
for(int i = n-1; i >= 0; i--) {
cout << a[i] << endl;
}
// uvolnime pamat
for (int i = 0; i < N; i++) {
delete[] a[i];
}
}
Cvičenia:
- Prečo nemáme na konci programu delete[] a?
- Čo by sa stalo, ak by sme namiesto a[n] = new char[strlen(riadok)+1]; strcpy(a[n], riadok) dali a[n] = riadok?
- Prerobte program tak, aby namiesto poľa a fixnej veľkosti maxN používal dynamické pole.
- Vedeli by sme dynamické polia nejako použiť aj na načítavanie jednotlivých riadkov?
Vstupy do funkcie main
- Niekedy vidíte v programoch funkciu main s nasledujúcou hlavičkou:
int main(int argc, char** argv) {
</syntaxhighlight lang="C++">
Vstupné argumenty argc a argv sa používajú pri spúšťaní programu na príkazovom riadku.
* <tt>char **argv</tt> je pole C-čkových reťazcov a <tt>argc</tt> je počet reťazcov v tomto poli
* Prvý z nich, <tt>argv[0]</tt>, je meno samotného programu a ostatné sú argumenty programu
* Užitočné, ak spúšťame program z príkazového riadku
* Dá sa nastaviť v Netbeans, Properties, Run
* Tento program jednoducho argumenty vypíše
<pre>
#include <iostream>
using namespace std;
int main(int argc, char** argv) {
for (int i = 0; i < argc; i++) {
cout << argv[i] << endl;
}
}
</pre>
Polia reťazcov umožňujú okrem iného aj napísať program, ktorý dostane a spracuje jeden alebo viacero argumentov z príkazového riadku a na základe nich prípadne „upraví svoje správanie”. Príkladom programu využívajúcim túto funkcionalitu je aj samotný kompilátor <tt>g++</tt>. Jeho najjednoduchšie volanie
<pre>
g++ program.cpp
</pre>
obsahuje okrem názvu programu <tt>g++</tt> aj argument <tt>program.cpp</tt> – ten dáva kompilátoru informáciu o tom, ktorý zdrojový súbor má kompilovať.
Na písanie programov umožňujúcich spracovanie takýchto argumentov je potrebné využiť „jemne pokročilejšiu” verziu funkcie <tt>main</tt> s hlavičkou
<syntaxhighlight lang="C++">
int main(int argc, char **argv)
– tú automaticky generujú viaceré pokročilejšie textové editory alebo IDE (napríklad NetBeans). Význam parametrov argc a argv je nasledovný:
- argv je pole reťazcov (resp. pole smerníkov na char) a argc je počet reťazcov v tomto poli.
- Reťazec argv[0] je vždy názov programu.
- Reťazce argv[1],...,argv[argc-1] sú jednotlivé argumenty.
Nasledujúci jednoduchý program postupne vypíše všetky argumenty, ktoré dostal z príkazového riadku.
#include <iostream>
using namespace std;
int main(int argc, char **argv) {
for (int i = 0; i <= argc-1; i++) {
cout << argv[i] << endl;
}
return 0;
}
Deklarácie so smerníkmi a poľami
Príklady na prácu s maticami uvedené vyššie môžete modifikovať vo vlastných programoch, ale je dobré trochu lepšie rozumieť logike práce so smerníkmi v C/C++.
- Operátor [ ] je zľava asociatívny, t.j. a[2][3] je to isté ako (a[2])[3]
- Operátor * je sprava asociatívny, t.j. **p je to isté ako *(*p).
- Operátor [ ] má vyššiu prioritu ako *, t.j. *a[10] je to isté ako *(a[10]).
- Ak x je pole smerníkov na int, tak *(x[10]) znamená, že vezmeme pole x, pozrieme sa na jeho desiaty prvok a následne na tento desiaty prvok aplikujeme dereferenciu, čím dostaneme int.
- Ak x je smerník na pole int-ov, (*x)[10] znamená, že vezmeme x, aplikujeme dereferenciu, ktorej výsledkom je pole int-ov a pozrieme sa na desiaty prvok tohto poľa.
Cvičenie:
- Premennú x sme vytvorili príkazom int **x = vytvorMaticu(20,20). S ktorými prvkami tabuľky teda pracujú výrazy *(x[10]) a (*x)[10]?
- Ako by sme vymenili prvý a druhý riadok matice?
Komplikácie nastávajú aj pri pochopení typov premmenných. Pomôžu nám nasledujúce rady.
- Riadok int *p; môžeme čítať takto: Ak vezmeme smerník p a aplikujeme na neho operátor *, získame hodnotu typu int
- Riadok int *a[10]; je to isté ako int *(a[10]) znamená: Ak vezmeme a, pozrieme sa na niektorý z desiatich prvkov tohto poľa a nakoniec aplikujeme dereferenciu, dostaneme hodnotu typu int. Vytvorili sme teda desaťprvkové pole smerníkov na int.
- Riadok int (*a)[10];znamená: Ak vezmeme a, aplikujeme dereferenciu, dostaneme pole a keď sa pozrieme na niektorý prvok tohto poľa, dostaneme int. Riadok teda vytvorí smerník na pole desiatich celých čísel. Tento smerník však zatiaľ ukazuje na náhodné miesto pamäte, žiadne nové pole nevzniklo. To sa nám málokedy zíde, premennú a môžeme zadefinovať radšej ako int **a.
- Riadok int *(*(a[10])) vytvorí desaťprvkové pole smerníkov na smerníky na int, dá sa zapísať aj bez zátvoriek int **a[10]
Smerníková aritmetika
Na smerníkoch možno vykonávať určité operácie, ktoré sa zvyknú nazývať smerníková aritmetika. Nech p, p1, p2 sú smerníky definované ako
T *p;
T *p1;
T *p2;
kde T označuje nejaký typ. Nech n je typu int. Potom:
- p + n označuje smerník na n-tý pamäťový úsek (postačujúci práve na uchovanie hodnoty typu T) za adresou p.
- Napríklad p+n je to isté ako &p[n] a *(p+n) je to isté ako p[n].
- p++ je skratkou pre p = p+1, ...
- Operátor [ ] je teda nadbytočný – p[n] je len skratkou pre často používaný výraz *(p+n).
- p - n označuje smerník na n-tý pamäťový úsek (postačujúci práve na uchovanie hodnoty typu T) pred adresou p.
- p1 - p2 je celé číslo k také, že p1 == p2 + k. Zmysluplný výsledok možno očakávať len vtedy, keď p1 a p2 sú adresami prvkov v tom istom poli (v jedinom súvislom kuse pamäte).
- Smerníky tiež možno prirodzene porovnávať relačnými operátormi ==, <, >, <=, >=, !=. Výsledok je zmysluplný opäť len vtedy, keď p1 a p2 sú adresami prvkov v tom istom poli.
Program, ktorý najprv načíta pole a následne prvky tohto poľa vypíše od konca, tak možno napísať napríklad aj takto:
#include <iostream>
using namespace std;
const int maxN = 1000;
int main() {
int a[maxN];
int N;
cout << "Zadaj pocet cisel: ";
cin >> N;
for (int i = 0; i <= N-1; i++) {
cout << "Zadaj cislo " << i + 1 << ": ";
cin >> *(a + i);
}
for (int i = N-1; i >= 0; i--) {
cout << *(a + i) << " ";
}
cout << endl;
}